一、線程執行的內存原理
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t2.start();
}
對應的內存原理圖大致是這樣:
注意事項:
- 執行線程任務的run方法是線程私有的。
- 某個線程對象出現異常不會影響其他線程的執行。
二、創建線程的方式
(1)繼承Thread類
1、步驟
- 定義一個類,繼承Thread類。
- 重寫Thread類的
run
方法。 - 創建線程對象。
- 調用
start
方法開啓新線程,內部會執行run
方法。
2、代碼示例
public class MyThread extends Thread {
@Override
public void run() {
// 獲取線程名稱
String threadName = this.getName();
for (int i=0;i<100;i++) {
// 複習異常拋出的方法,拋出一個運行時異常
if("Thread-1".equals(threadName) && i == 3){
throw new RuntimeException(threadName + "出問題了");
}
System.out.println(threadName+"..."+i);
}
}
}
3、Thread類的構造函數
public Thread()
:分配一個新的線程對象。public Thread(String name):
分配一個指定名字的新的線程對象。public Thread(Runnable target)
:分配一個帶有指定目標新的線程對象。public Thread(Runnable target,String name)
:分配一個帶有指定目標新的線程對象並指定名字。
4、常用成員方法
public String getName():
獲取當前線程名稱。public String start();
導致此線程開始執行;java虛擬機調用此線程的run
方法。public void run():
定義線程的任務邏輯。
5、常用靜態方法
public static void sleep(long millis)
: 讓當前正在執行的進程暫停指定毫秒數。public static Thread currentThread()
:返回對當前正在執行的線程對象的引用。
(2)實現Runnable接口
1、步驟
- 定義Runnable接口的實現類
- 實現類覆蓋重寫
run
方法,指定線程任務邏輯 - 創建Runnable接口實現類對象
- 創建Thread類對象,構造函數傳遞Runnable實踐類對象。
a) public Thread(Runnable target):分配一個帶有指定目標新的線程對象。
b) public Thread(Runnable target,String name):分配一個帶有指定目標新的線程對象並指定名字。
- Thread類對象調用
start
方法,內部自動調用run
方法。
2、代碼展示
public class MyTask implements Runnable{
@Override
public void run() {
//任務邏輯
}
}
MyTask myTask = new MyTask();
Thread thread = new Thread(myTask);
thread.start();
2、(重點!)實現Runnable接口來創建線程的好處
- 避免java中類單繼承的侷限性
- 降低線程任務對象和線程之間的耦合性
tip:換句話說,我們可以更加專注於線程的任務,先把線程的任務邏輯創建完畢。之後需要執行線程任務的地方就創建線程,執行需要的線程任務即可。並且線程任務可以多次反覆使用。有點像零件插拔一樣。
(3)匿名內部類方式
1、格式
new 父類/接口(){
//覆蓋重寫抽象方法
};
2、作用
- 創建父類子類對象的快捷方式
- 創建接口的實現類對象的快捷方式
3、注意事項
- 使用匿名內部類創建的對象只能一次性使用
- 儘量使用lambda表達式進行書寫,提高代碼可讀性和編程效率。
三、(重點!)線程安全問題
(1)出現線程安全的情況
-
有兩個以上線程同時操作共享數據
-
操作共享數據的語句有兩條以上
-
線程調度是搶佔式調度模式。
(2)解決方案
-
同一個線程,操作共享數據的多條語句全部執行,要麼多條語句全部不執行。故而可以使用同步技術。
-
同步的原理:有鎖的線程執行,沒有鎖的線程等待。
(3)實際解決
1、同步代碼塊
-
作用:用來解決多線程訪問共享數據安全問題
-
格式
synchronized(任意對象){
}
- 注意事項:
(1)所有操作共享數據的代碼寫到同步代碼塊{}中。
(2)任意對象:任意指的是類型可以任意,但要保證全局唯一,被多個線程共享使用。- 任意對象,也叫鎖對象。更加專業的術語:對象監視器。
2、同步方法
- 格式
修飾符 synchronized 返回值類型 方法名稱(參數列表...){
...
}
- 注意事項
(1)所有操作共享數據的代碼都在{}中間添加一個
(2)同步方法的鎖對象就是this
3、使用Lock接口
-
方法:
abstract void lock()
獲得鎖。
abstract void unlock()
釋放鎖。 -
實現類
java.util.concurrent.locks.ReentrantLock ,空參構造函數 -
注意事項:
釋放鎖的動作必須被執行。
(4)實際案例
1、賣票案例分析
(1)總共有3種途徑賣票,每個途徑,相當於一個線程對象
(2)每個線程對象要執行的任務: 都是在賣票
(3)3個線程對象,操作的資源 100 張票 是被共享的
2、解決策略:
(1) 定義實現類,實現Runnable接口
(2) 覆蓋重寫Runnable接口中的run方法.指定線程任務——賣票
(2.1)判斷是否有票
(2.2)有: 出一張票
(2.3)票的數量減少1
(3) 創建Runnable接口的實現類對象
(4) 創建3個Thread對象,傳遞Runnable接口的實現類對象,代表,賣票的3種途徑
(5) 3個Thread對象分別調用start方法,開啓售票
3、代碼實現
public class MyTicket implements Runnable{
private int tickets= 100;
Object obj = new Object();
@Override
public void run() {
while (true){
// sellTicketB();
sellTicketA();
}
}
// 同步函數
private synchronized void sellTicketA(){
if(tickets>0){
System.out.println(Thread.currentThread().getName() + " 賣出第" + tickets-- + "張票");
}else {
return;
}
}
//同步進程快
private void sellTicketB() {
synchronized(obj){
if(tickets>0){
System.out.println(Thread.currentThread().getName() + " 賣出第" + tickets-- + "張票");
}else {
return;
}
}
}
}
public class synchronizedTest {
public static void main(String[] args) {
MyTicket task = new MyTicket();
// 三個線程任務來出票
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
}
}
4、線程同步的原理
-
線程執行的前提:
(1)cpu資源
(2)鎖對象 -
基本規則:
線程對象執行同步代碼塊中的內容,要麼全部執行,要麼全部不執行,不能夠被其他線程干擾。 -
拿買票案例舉例說明
現在存在t0、t1和t2三個線程。
假設一:
假設t0線程獲取cpu資源,執行線程任務遇到同步代碼塊,判斷是否具有鎖對象。
有:獲取鎖對象
進入同步代碼塊,執行同步代碼,,假設t0在執行過程中沒有被t1或者t2搶奪cpu資源,那麼t0或順利執行完同步代碼塊內代碼,退出同步代碼塊,釋放鎖資源,繼續和其他線程搶奪cpu資源和鎖對象。
假設二:
假設t0線程獲取cpu資源,執行線程任務遇到同步代碼塊,判斷是否具有鎖對象
有:獲取鎖對象
進入同步代碼塊,執行同步代碼,假設t0在執行過程中被t1搶奪了cpu資源,那麼t0線程將不能繼續執行。t1線程執行任務,遇到同步代碼塊,判斷是否具有鎖對象,因爲鎖已經被t0拿了,因此t1進入阻塞狀態,等待獲取鎖對象被釋放。
假設三
假設t0執行完成了同步代碼塊的內容,釋放了鎖對象,t1處於阻塞狀態,但此時t2線程搶到了cpu資源,執行代碼到同步代碼塊,然後順利獲取鎖對象,進入同步代碼塊執行。這種情況下,t1將繼續等待t2在同步代碼塊執行完畢,然後再去搶奪cpu資源和鎖資源。
可以發現,線程如果不進行調度的管理可能會出現長時間等待的問題,因爲搶佔式調度具有隨機性,不能獲得最大的性能。
(持續更新…)
四、線程狀態
java.lang.Thread.State給出了六種
線程狀態
線程狀態 | 導致狀態發生條件 |
---|---|
NEW(新建) | 線程剛被創建,但是並未啓動,還沒有調用start方法 |
Runnable(可運行) | 線程可以再java虛擬機中運行的狀態,可能正在運行自己代碼,也可能沒有,這取決於cpu |
Blocked(鎖阻塞) | 當一個線程試圖獲取一個獨享鎖,而該對象被其他的線程持有,則該線程進入Blocked狀態;當該對象持有鎖時,該線程將變成Runnable狀態 |
Waiting(無限等待) | 一個線程在等待另一個線程執行一個(喚醒)動作時,該線程進入Waiting狀態。進入這個狀態後是不能自動喚醒的,必須等待另一個線程調用notify或者notifyAll方法才能夠喚醒。 |
Timed Waiting(計時等待 | 同waiting狀態,有幾個方法有超時參數,調用他們將進入Timed Waiting狀態。這一狀態將一直保持到超時期滿或者接收到喚醒通知。帶有超時參數的常用方法有Thread.sleep 、Object.wait。 |
Teminated(被終止) | 因爲run方法正常退出而死亡,或者因爲沒有捕獲的異常終止了run方法而死亡。 |
注意事項(一)
sleep
方法可以在同步中使用sleep
方法可以在非同步中使用sleep
方法與鎖對象無關(不會釋放鎖)
注意事項(二)
Object類
定義wait
和notify
方法- 因此任意對象可以調用
wait()
和notify()
方法 - 鎖對象可以是任意的
- 但是鎖對象必須使用在同步中,因此
wait
和notify
方法必須在同步中使用
案例分析
雙線程交替執行。有一個抽獎池,該抽獎池中存放了獎勵的金額,該抽獎池中的獎項爲
{10,5,20,50,100,200,500,800,2,80,300,3000};
創建兩個抽獎箱(線程)設置線程名稱分別爲“抽獎箱1”,“抽獎箱2”,隨機從抽獎池中完成抽獎。
兩個線程輪流交替抽獎,每抽出一個獎項就打印出來。
【輸出示例】
抽獎箱1...抽出了10元...
抽獎箱2...抽出了20元...
抽獎箱1...抽出了50元...
抽獎箱2...抽出了800元...
... ...
每次抽的過程中,不打印,抽完時一次性打印。
【輸出示例】
在此次抽獎過程中,抽獎箱1總共產生了6個獎項,分別爲:10,5,20,50,100,200最高獎項爲200元,總計額爲385元
在此次抽獎過程中,抽獎箱2總共產生了6個獎項,分別爲:500,800,2,80,300,3000最高獎項爲3000元,總計額爲4682元
在此次抽獎過程中,抽獎項2中產生了最高獎項,該最高獎項爲3000元
1. 分析
兩個線程的任務都是抽獎,因此很明顯只需要定義一個線程任務對象“抽獎”即可。由於兩個抽獎箱共享一個獎池,且要求兩個抽獎箱交替進行,很明顯需要用到線程的等待(wait)和喚醒(notify)操作。因此,兩個線程對於獎池中獎金的操作需要同步。前面已經說明,線程任務只有一個,故同步代碼塊的鎖對象使用線程任務對象自身(this)即可。
2、實現思路
3、代碼實現
public class RunnableImpl implements Runnable{
private List list;
private Map<String,List> mp = new HashMap<>();
private int count = 0;
public RunnableImpl(List list) {
this.list = list;
}
@Override
public void run() {
List subList = new ArrayList();
while (true){
synchronized (this){
if(list.size()<=0){
this.notifyAll();
mp.put(Thread.currentThread().getName(),subList);
count++;
if(count == 2){
Integer max = 0;
String max_name = "";
for (Map.Entry<String, List> entry : mp.entrySet()) {
List t = entry.getValue();
String s = entry.getKey();
Integer tmax = 0;
Integer sum = 0;
StringBuilder sb = new StringBuilder();
for (Object o : t) {
sb = sb.append(o).append(",");
sum += (Integer)o;
tmax = tmax < (Integer)o ? (Integer)o : tmax;
}
if(max < tmax){
max = tmax;
max_name = s;
}
//sb = sb.deleteCharAt(sb.length()-1);
String seq =sb.toString();
System.out.println("在此次抽獎過程中,"+ s +"總共產生了"+t.size()+"個獎項,分別爲:" + seq +"最高獎項爲"+ tmax +"元,總計額爲"+ sum +"元");
}
System.out.println("在此次抽獎過程中,"+max_name+"中產生了最高獎項,該最高獎項爲"+max+"元");
}
break;
}
if(list.size() > 0){
Object remove = list.remove(new Random().nextInt(list.size()));
System.out.println(Thread.currentThread().getName() + "..." + "抽出了"+ remove +"元...");
subList.add(remove);
this.notify();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
public class Test {
public static void main(String[] args) {
List<Integer> list = new ArrayList(Arrays.asList(10,5,20,50,100,200,500,800,2,80,300,3000));
RunnableImpl runnable = new RunnableImpl(list);
new Thread(runnable,"抽獎箱1").start();
new Thread(runnable,"抽獎箱2").start();
}
}
五、生產者消費者模式
案例分析
包子鋪賣包子,喫貨喫飽做,包子鋪包一個包子,喫貨喫一個包子。
1、圖解分析
2、多個線程間的通信
多個不同任務的線程要操作共享數據,一定需要統一的協調,也就是說需要一個統一的鎖對象。在這種情況下,不能直接使用線程自己(this)作爲鎖對象,因爲線程任務不同,必然也是各自獨立的。因此,這個場景下,選擇了盤子(List對象)來作爲鎖,因爲他是全局唯一個,和生產者與消費者都有着密切關係。
3、代碼實現
生產者
/*
包子鋪:
1.判斷盤子中有沒有包子
2.無:生產一個包子,直到裝滿盤子,喚醒等待鎖的線程
3.有:釋放鎖
*/
public class Productor implements Runnable{
private List list;
private int count = 0;
public Productor(List list) {
this.list = list;
}
@Override
public void run() {
while (true){
synchronized (list){
try {
//盤子有了包子,進入等待
if(list.size() > 0){
list.wait();
}
//沒有進入等待說明盤子空了,開始製作包子
String bz ="皮多肉韭菜雞蛋包子";
list.add(bz);
System.out.println(Thread.currentThread().getName() + " 包了" + bz + count++);
// 喚醒喫貨喫包子
list.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
消費者
/*
喫貨:
1.看盤子裏有沒有包子
2.有:喫掉
3.無:釋放鎖,喚醒正在等待鎖的包子鋪接着做包子
*/
public class Consumer implements Runnable{
private List list;
private int count=0;
public Consumer(List list) {
this.list = list;
}
@Override
public void run() {
while (true) {
synchronized (list) {
// 盤子中沒有包子了,進入等待階段
if(list.size() == 0){
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 沒有進入等待說明盤子裏還有包子
while (list.size()>0){
String bz = (String)list.remove(0) + count++;
System.out.println(Thread.currentThread().getName() + "吃了" + bz);
}
// 包子被喫完了,喚醒袍子鋪做包子
list.notify();
}
}
}
}
public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Productor productor = new Productor(list);
Consumer consumer = new Consumer(list);
Thread t0 = new Thread(productor);
t0.setName("慶豐包子鋪");
t0.start();
Thread t1 = new Thread(consumer);
t1.setName("喫貨A");
t1.start();
Thread t2 = new Thread(consumer);
t2.setName("喫貨B");
t2.start();
}
}