基礎——線程

一、線程執行的內存原理

	public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

		t1.start();
        t2.start();
	}

對應的內存原理圖大致是這樣:

在這裏插入圖片描述

注意事項:

  1. 執行線程任務的run方法是線程私有的。
  2. 某個線程對象出現異常不會影響其他線程的執行。

二、創建線程的方式

(1)繼承Thread類

1、步驟

  1. 定義一個類,繼承Thread類。
  2. 重寫Thread類的run方法。
  3. 創建線程對象。
  4. 調用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、步驟

  1. 定義Runnable接口的實現類
  2. 實現類覆蓋重寫run方法,指定線程任務邏輯
  3. 創建Runnable接口實現類對象
  4. 創建Thread類對象,構造函數傳遞Runnable實踐類對象。

a) public Thread(Runnable target):分配一個帶有指定目標新的線程對象。
b) public Thread(Runnable target,String name):分配一個帶有指定目標新的線程對象並指定名字。

  1. 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接口來創建線程的好處

  1. 避免java中類單繼承的侷限性
  2. 降低線程任務對象和線程之間的耦合性

tip:換句話說,我們可以更加專注於線程的任務,先把線程的任務邏輯創建完畢。之後需要執行線程任務的地方就創建線程,執行需要的線程任務即可。並且線程任務可以多次反覆使用。有點像零件插拔一樣。

(3)匿名內部類方式

1、格式

new 父類/接口(){
	//覆蓋重寫抽象方法
};

2、作用

  1. 創建父類子類對象的快捷方式
  2. 創建接口的實現類對象的快捷方式

3、注意事項

  1. 使用匿名內部類創建的對象只能一次性使用
  2. 儘量使用lambda表達式進行書寫,提高代碼可讀性和編程效率。

三、(重點!)線程安全問題

(1)出現線程安全的情況

  1. 兩個以上線程同時操作共享數據

  2. 操作共享數據的語句兩條以上

  3. 線程調度是搶佔式調度模式。

(2)解決方案

  • 同一個線程,操作共享數據的多條語句全部執行,要麼多條語句全部不執行。故而可以使用同步技術。

  • 同步的原理:有鎖的線程執行,沒有鎖的線程等待。

(3)實際解決

1、同步代碼塊

  1. 作用:用來解決多線程訪問共享數據安全問題

  2. 格式

synchronized(任意對象){

}
  1. 注意事項
    (1)所有操作共享數據的代碼寫到同步代碼塊{}中。
    (2)任意對象:任意指的是類型可以任意,但要保證全局唯一,被多個線程共享使用
    1. 任意對象,也叫鎖對象。更加專業的術語:對象監視器。

2、同步方法

  1. 格式
修飾符 synchronized 返回值類型 方法名稱(參數列表...){
	...
}
  1. 注意事項
    (1)所有操作共享數據的代碼都在{}中間添加一個
    (2)同步方法的鎖對象就是this

3、使用Lock接口

  1. 方法:
    abstract void lock​() 獲得鎖。
    abstract void unlock​() 釋放鎖。

  2. 實現類
    java.util.concurrent.locks.ReentrantLock ,空參構造函數

  3. 注意事項:
    釋放鎖的動作必須被執行。

(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. 線程執行的前提:
    (1)cpu資源
    (2)鎖對象

  2. 基本規則:
    線程對象執行同步代碼塊中的內容,要麼全部執行,要麼全部不執行,不能夠被其他線程干擾。

  3. 拿買票案例舉例說明
    在這裏插入圖片描述
    現在存在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方法而死亡。

在這裏插入圖片描述
注意事項(一)

  1. sleep方法可以在同步中使用
  2. sleep方法可以在非同步中使用
  3. sleep方法與鎖對象無關(不會釋放鎖)

注意事項(二)

  1. Object類定義waitnotify方法
  2. 因此任意對象可以調用wait()notify()方法
  3. 鎖對象可以是任意的
  4. 但是鎖對象必須使用在同步中,因此waitnotify方法必須在同步中使用

案例分析

雙線程交替執行。有一個抽獎池,該抽獎池中存放了獎勵的金額,該抽獎池中的獎項爲 
{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();
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章