天天看點

Java多線程(一篇從0講透)

作者:monesy小孩

多線程

思維導圖看天下:

Java多線程(一篇從0講透)

1. 概述

并行與并發

并行 :指兩個或多個事件在同一時刻發生(同時發生)

并發 :指兩個或多個事件在同一個時間段内發生。(交替執行)

線程與程序

程序:是指一個記憶體中運作的程式,每個程序都有一個獨立的記憶體空間,一個應用程式可以同時運作多個程序

記憶:程序的英文為Process,Process也為過程,是以程序可以大概了解為程式執行的過程。

(程序也是程式的一次執行過程,是系統運作程式的基本機關; 系統運作一個程式即是一個程序從建立、運作到消亡的過程)

線程:程序中的一個執行單元,負責目前程序中程式的執行,一個程序中至少有一個線程。一個程序中是可以有多個線程的,這個應用程式也可以稱之為多線程程式。【java預設有兩個線程:main、GC】

程序與線程的差別:

  • 程序:有獨立的記憶體空間,程序中的資料存放空間(堆空間和棧空間)是獨立的,至少有一個線程。
  • 線程:堆空間是共享的,棧空間是獨立的,線程消耗的資源比程序小的多

2. 線程建立的五種方式

推薦使用Runnable接口的方式,因為Java是單繼承的,是以使用Thread有OPP單繼承局限性

Java多線程(一篇從0講透)

2.1 背景介紹

線程類

Java使用 java.lang.Thread 類代表線程,所有的線程對象都必須是Thread類或其子類的執行個體

每個線程的作用是完成一定的任務,實際上就是執行一段程式流即一段順序執行的代碼

Java使用線程執行體來代表這段程式流。

2.2 ① 繼承Thread類

2.2.1 線程實作

1)實作步驟

  1. 繼承Thread類的子類,并重寫該類的run()方法(該run()方法的方法體就代表了線程需要完成的任務,是以run()方法稱為線程執行體)
  2. 建立Thread子類的執行個體,即建立了線程對象
  3. 調用線程對象的start()方法來啟動該線程

2)實作案例

自定義線程類:

Java多線程(一篇從0講透)

主函數:

Java多線程(一篇從0講透)
public static void main(String[] args) {
    MyThread myThread = new MyThread("MyThread");
    myThread.start();
    for (int i = 0;i<1000;i++){
        System.out.println("main"+i);
    }
}
           

執行結果:

Java多線程(一篇從0講透)

3)執行過程分析

過程:程式啟動運作main時候,java虛拟機啟動一個程序,主線程main在main()調用時候被建立

随着調用Mt類的對象的start方法,另外一個新的線程也啟動了 ,這樣,整個應用就在多線程下運作。

運作時序圖:

Java多線程(一篇從0講透)

記憶體結構:

Java多線程(一篇從0講透)

4)調用start和run方法的差別

Java多線程(一篇從0講透)

2.2.2 構造方法

  1. public Thread()

    配置設定一個新的線程對象。

  2. public Thread(String name)

    配置設定一個指定名字的新的線程對象

  3. public Thread(Runnable target)

    配置設定一個帶有指定目标新的線程對象

  4. public Thread(Runnable target,String name)

    配置設定一個帶有指定目标新的線程對象并指定名字

2.2.3 常用方法

  1. public String getName() :擷取目前線程名稱。
  2. public void start() :導緻此線程開始執行; Java虛拟機調用此線程的run方法
  3. public void run() :此線程要執行的任務在此處定義代碼。
  4. public static void sleep(long millis) :使目前正在執行的線程以指定的毫秒數暫停(暫時停止執行)。
  5. public static Thread currentThread() :傳回對目前正在執行的線程對象的引用。

1)擷取線程名稱

  1. 可以使用Thread類中的方法getName,

    String getName() 傳回該線程的名稱。

  2. 可以先擷取目前正在執行的線程,再調用getName方法擷取線程名稱
  3. static Thread currentThread() 傳回對目前正在執行的線程對象的引用
  4. //1.可以使用Thread類中的方法getName String name = getName(); System.out.println(name);//建立時, 指定了名稱,擷取的就是指定的名稱 //如果沒有指定名稱,擷取的就是Thread-0 //2.可以先擷取目前正在執行的線程 Thread currentThread = Thread.currentThread(); System.out.println(currentThread);//Thread[Thread-0,5,main] String name2 = currentThread.getName(); System.out.println(name2);//Thread-0

2)設定線程名稱

  1. 方法一:可以使用Thread類中的方法setName

    void setName(String name) 改變線程名稱,使之與參數 name 相同。

MyThread myThread = new MyThread();
myThread.setName("myThreadName");
myThread.start();
           
  1. 方法二:添加一個帶參構造方法,參數傳遞線程的名稱;調用父類的帶參構造方法,把名字傳遞給父類,讓父親給兒子起名字

    Thread(String name) 配置設定新的 Thread 對象。

public class MyThread extends Thread{
    //定義指定線程名稱的構造方法
    public MyThread(String name) {
    super(name);
}
           
Java多線程(一篇從0講透)

3)線程休眠

public static void sleep(long millis)

使目前正在執行的線程以指定的毫秒數暫停(暫時停止執行)睡醒了,繼續執行

/*程式在執行第二秒時, 會暫停2秒,2秒後,繼續執行後面程式*/
for (int i = 1; i <=60; i++) {
    System.out.println(i);
    /*讓程式睡眠1秒鐘   1秒=1000毫秒*/
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
           

2.2.4 Thread構造方法底層原理——靜态代理

隻針對有參且參是Runnable類的構造方法:public Thread(Runnable target)

由于Thread和target頂層都是Runnable接口,是以Thread是使用了靜态代理的方式代理參數target。

2.3 ② 實作Runnable接口

優勢:

  1. 避免單繼承的局限性

    一個類繼承了Thread類就不能繼承其他的類

    一個類實作了Runnable接口,還可以繼續繼承别的類,實作其他的接口

  2. 增強了程式的擴充性,降低程式的耦合度

    使用Runnable接口把設定線程任務和開啟線程相分離

    實作類當中,重寫run方法,設定線程任務

    建立Thread類對象,調用 start方法,開啟新線程

如果一個類繼承Thread,則不适合資源共享。但是如果實作了Runable接口的話,則很容易的實作資源共享

2.3.1 實作步驟

1.建立一個RunnableImpl類實作Runnable接口

2.重寫Runnable接口中的run方法,設定線程任務

3.建立Runnable接口的實作類RunnableImpl的對象t

4.建立Thread類對象,構造方法中傳遞Runnable接口的實作類RunnableImpl的對象t

5.調用Thread類中的start方法,開啟新的線程,執行run方法

示例:

//實作Runnable接口
public class RunnableImpl implements Runnable{
    //2.重寫Runnable接口中的run方法,設定線程任務
    @Override
    public void run() {
        //新線程執行的代碼
        for (int i = 0; i <20; i++) {
            System.out.println(Thread.currentThread().getName()+"===>"+i);
        }
    }
}
public static void main(String[] args) {
        //3.建立Runnable接口的實作類對象
        RunnableImpl r = new RunnableImpl();
        //4.建立Thread類對象,構造方法中傳遞Runnable接口的實作類對象
        Thread t = new Thread(r);//列印20次i
        //5.調用Thread類中的start方法,開啟新的線程,執行run方法
        t.start();  //【一般16-18行簡寫為:new Thread(r,"線程名").start();】
        //主線程開啟新線程之後繼續執行的代碼
        for (int i = 0; i <20; i++) {
            System.out.println(Thread.currentThread().getName()+"===>"+i);
        }
    }
           

2.3.2 構造方法

  1. Thread(Runnable target) 配置設定新的 Thread對象
  2. Thread(Runnable target, String name) 配置設定新的 Thread對象【推薦該方法,因為可以自定義線程名】

2.4 ③ 實作Callable接口

十分重要,但本篇隻簡單介紹了一下,請去看下一篇JUC

2.4.1 實作步驟

  1. 實作Callable接口,需要傳回值類型
  2. 重寫call方法,需要抛出異常
  3. 建立目标對象
  4. 建立執行服務:ExecutorService ser = Executors.newFixedThreadPool(1); //1為開辟的線程池中線程的數量
  5. 送出執行Future result1 = ser.submit(t1); //線程
  6. 擷取結果:boolean r1 = resut1.get() //指定線程的傳回結果
  7. 關閉服務:ser.shutdownNow()

代碼:

public class MyCallableImpl implements Callable<Boolean> {
    @Override
    public Boolean call() throws Exception {
        PictureCatch t = new PictureCatch();
        t.test(url,name);
        System.out.println("下載下傳了檔案名:"+name);
        return true;
    }

    String url; //網址
    String name;    //儲存的檔案名

    MyCallableImpl(String url,String name){
        this.url=url;
        this.name=name;
    }

    public static void main(String[] args) {
        MyCallableImpl t1 = new MyCallableImpl("https://img0.baidu.com/it/u=1151663768,725447312&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500","t4");
        MyCallableImpl t2 = new MyCallableImpl("https://img0.baidu.com/it/u=1648512719,1593015989&fm=253&fmt=auto&app=120&f=JPEG?w=891&h=500","t5");
        MyCallableImpl t3 = new MyCallableImpl("https://img2.baidu.com/it/u=863703859,746061395&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500","t6");

        ExecutorService ser = Executors.newFixedThreadPool(3);
        Future<Boolean> result1 = ser.submit(t1);
        Future<Boolean> result2 = ser.submit(t2);
        Future<Boolean> result3 = ser.submit(t3);
        try {
            boolean r1 = result1.get();
            boolean r2 = result2.get();
            boolean r3 = result3.get();

            System.out.println(r1);
            System.out.println(r2);
            System.out.println(r3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        ser.shutdownNow();
    }


    class PictureCatch{
        void test(String url, String name){
            try {
                FileUtils.copyURLToFile(new URL(url), new File(name));
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("擷取檔案出錯!");
            }
        }
    }
}
           

2.5 ④ 線程池Executor

這裡就不講了,放在了JUC并發程式設計那篇裡詳細講解了

2.6 ⑤ Timer

使用 Timer 的方式如下:

public class MyTimer {

    public static void main(String[] args) {
        timer();
    }

    /**
     * 指定時間 time 執行 schedule(TimerTask task, Date time)
     */
    public static void timer() {
        Timer timer = new Timer();
        // 設定指定的時間time,此處為2000毫秒
        timer.schedule(new TimerTask() {
            public void run() {
                System.out.println("執行定時任務");
            }
        }, 2000);
    }

}
           

2.7 拓展:匿名内部類,實作Thread/Runnable多現程

2.7.1 匿名内部類

作用

把子類繼承父類,重寫父類的方法,建立子類對象,合成一步完成

把實作類實作接口,重寫接口庫的方法,建立實作類對象,合成一步完成

最終得要子類對象或實作類對象

格式

new 父類/接口(){
    重寫父類/接口中的方法
};
           

2.7.2 Thread

Java多線程(一篇從0講透)
public static void main(String[] args) {
    new Thread(){    //new 沒有名稱的類 繼承Thread
        //重寫run方法,設定線程任務
        @Override
        public void run() {
            for (int i = 0; i <20 ; i++) {
                System.out.println(Thread.currentThread().getName()+"==>"+i);
            }
        }
    }.start();
}
           

2.7.3 Runnable

Java多線程(一篇從0講透)
new Thread(new Runnable() {  //new沒有名稱的類實作了Runnable接口
    //重寫run方法,設定線程任務
    @Override
    public void run() { //實作接口當中run方法
        for (int i = 0; i <20 ; i++) {
            System.out.println(Thread.currentThread().getName()+"-->"+i);
        }
    }
}).start();
           

3. 線程使用

3.1 六種線程狀态

  1. NEW(建立)

    線程剛被建立,但是并未啟動。還沒調用start方法

  2. Runnable(可運作)

    線程可以在java虛拟機中運作的狀态,可能正在運作自己代碼,也可能沒有,這取決于操 作系統處理器

  3. Blocked(鎖阻塞)

    當一個線程試圖擷取一個對象鎖,而該對象鎖被其他的線程持有,則該線程進入Blocked狀 态;當該線程持有鎖時,該線程将變成Runnable狀态。

  4. Waiting(無限等待)

    一個線程在等待另一個線程執行一個(喚醒)動作時,該線程進入Waiting狀态。

    進入這個 狀态後是不能自動喚醒的,必須等待另一個線程調用notify或者notifyAll方法才能夠喚醒。

  5. Timed Waiting(計時等待)

    同waiting狀态,有幾個方法有逾時參數,調用他們将進入Timed Waiting狀态。

    這一狀态 将一直保持到逾時期滿或者接收到喚醒通知。帶有逾時參數的常用方法有Thread.sleep 、 Object.wait

  6. Teminated(被終止)

    因為run方法正常退出而死亡,或者因為沒有捕獲的異常終止了run方法而死亡。

Java多線程(一篇從0講透)
Java多線程(一篇從0講透)

3.2 線程的常用操作

線程方法

方法 說明
setPriority(int newPriority) 更改線程的優先級
static void sleep(long millis) 在指定的毫秒數内讓目前正在執行的線程休眠
void join() 等待該線程終止
static void yield() 暫停目前正在執行的線程對象,并執行其他線程
void interrupt() 中斷線程,别用這個方式
boolean isAlive() 測試線程是否處于活動狀态

3.2.1 線程停止

  • 不推薦使用JDK提供的stop()、destory()方法。【已廢棄】
  • 推薦線程自己停止下來
  • 建議使用一個标志位進行終止變量,當flag=false(flag做while的條件),則終止線程運作

以下舉例是使用一個标志位falg來終止變量

public class ThreadStopDemo implements Runnable {
    private boolean flag = true;
    @Override
    public void run() {
        int i = 1;
        while (flag) {
            System.out.println("run..."+(i++));
        }
    }
	
    //設定一個專門修改标志位的方法來停止線程
    public void stop(){
        flag = false;
    }

    public static void main(String[] args) {
        ThreadStopDemo demo = new ThreadStopDemo();
        new Thread(demo).start();
        for (int i = 1; i <= 500; i++) {
            System.out.println("main..."+i);
            if (i == 300) {
                demo.stop();
                System.out.println("線程該停止了");
            }
        }
    }
}
           

3.2.2 線程休眠_sleep

  • sleep(long millis) 指定目前線程阻塞的毫秒數
  • sleep存在異常InterruptException,是以需要抛出異常
  • sleep時間達到後線程進入就緒狀态
  • sleep可以模拟網絡延時,倒計時等
  • 每一個對象都有一個鎖,sleep不會釋放鎖

模拟倒計時+列印目前系統時間

public static void main(String[] args) {
    //模拟倒計時
    System.out.println("開始倒計時");
    int num = 10;
    while (true) {
        System.out.println(num--);
        Thread.sleep(1000);
        if (num <= 0) {
            break;
        }
    }
    System.out.println("開始報時");
    //列印目前系統時間
    int count = 10;
    DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
    while (true) {
        System.out.println(LocalDateTime.now().format(dateTimeFormatter));
        Thread.sleep(1000);
        count--;
        if (count <= 0) {
            break;
        }
    }
}
           

3.2.3 線程禮讓_yield

禮讓不一定成功,因為cpu重新排程,可能會再次選到之前的線程
  • Thread.yield();禮讓線程,讓目前正在執行的線程暫停,但不阻塞
  • 将線程從記憶體中的運作狀态轉為就緒狀态并拿出記憶體
  • 讓cup重新排程選擇線程進入記憶體,禮讓不一定成功,看cup排程

代碼示範:結果可能有三種:aabb,abab,abba

public class ThreadYieldDemo implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"開始!");
        Thread.yield();
        System.out.println(Thread.currentThread().getName()+"結束!");
    }

    public static void main(String[] args) {
        ThreadYieldDemo demo = new ThreadYieldDemo();
        new Thread(demo, "a").start();
        new Thread(demo, "b").start();
    }
}
           

3.2.4 線程強制執行_join

  • Join合并線程,待此線程執行完成後,再執行其他線程,其他線程阻塞(可以想象成插隊)

代碼示範:結果是正常排隊執行到200後,得等強制執行走完200次後,才會繼續執行正常排隊201...

//插隊線程
public class 線程強制執行 {

    public static void main(String[] args) {
        forceThread forceThread = new forceThread();
        Thread thread = new Thread(forceThread, "強制線程");


        for (int i = 0; i < 500; i++) {
            System.out.println("正常排隊:"+i);
            if (i==200){
                thread.start();
                thread.join();
            }
        }
    }
}

class forceThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println("強制執行——"+i);
        }
    }
}
           

3.2.5 線程狀态檢視_getState

六種線程狀态看前面寫的

代碼示範:

public static void main(String[] args) {
    Thread thread = new Thread(()->{
        for (int i = 0; i < 20; i++) {
            Thread.sleep(500);
        }
    },"線程");


    System.out.println("線程start前的狀态:"+thread.getState());
    thread.start();
    System.out.println("線程start後的狀态:"+thread.getState());
    while (!Thread.State.TERMINATED.equals(thread.getState())){
        System.out.println("線程terminated之前的狀态:"+thread.getState());
        Thread.sleep(500);
    }
    System.out.println("線程的狀态:"+thread.getState());

}
           

結果:

線程start前的狀态:NEW
線程start後的狀态:RUNNABLE
線程terminated之前的狀态:RUNNABLE
線程terminated之前的狀态:TIMED_WAITING
線程terminated之前的狀态:TIMED_WAITING
線程terminated之前的狀态:TIMED_WAITING
線程terminated之前的狀态:TIMED_WAITING
線程terminated之前的狀态:TIMED_WAITING
線程terminated之前的狀态:TIMED_WAITING
線程terminated之前的狀态:TIMED_WAITING
線程terminated之前的狀态:TIMED_WAITING
線程terminated之前的狀态:RUNNABLE
線程terminated之前的狀态:TIMED_WAITING
線程terminated之前的狀态:TIMED_WAITING
線程terminated之前的狀态:TIMED_WAITING
線程terminated之前的狀态:TIMED_WAITING
線程terminated之前的狀态:RUNNABLE
線程terminated之前的狀态:TIMED_WAITING
線程terminated之前的狀态:TIMED_WAITING
線程terminated之前的狀态:TIMED_WAITING
線程terminated之前的狀态:TIMED_WAITING
線程terminated之前的狀态:RUNNABLE
線程terminated之前的狀态:RUNNABLE
線程的狀态:TERMINATED

           

3.2.6 線程優先級_Priority

源碼中所有線程的優先級預設為5

優先級的設定建議在start()排程前

優先級低隻是意味着擷取排程的機率低,并不是優先級低就不會被調用了,這都是看cup的排程

  • java提供一個線程排程器來監控程式中啟動後進入就緒狀态的所有線程,線程排程器按照優先級決定應該排程哪個線程
  • 線程的優先級用數字表示,範圍從1~10【越大優先級最高】Thread.MIN_PRIORITY =1Thread.MAX_PRIORITY =10Thread.NORM_PRIORITY =5
  • 使用以下方式改變或擷取優先級getPriority() setPriority(int x)

代碼示範:

public class 線程優先級 {
    public static void main(String[] args) {
        //列印主線程的優先級(也是所有線程預設的優先級)
        System.out.println(Thread.currentThread().getName() + "--->" + Thread.currentThread().getPriority());

        MyPriority myPriority = new MyPriority();

        Thread t1 = new Thread(myPriority,"線程1");
        Thread t2 = new Thread(myPriority,"線程2");
        Thread t3 = new Thread(myPriority,"線程3");
        Thread t4 = new Thread(myPriority,"線程4");
        Thread t5 = new Thread(myPriority,"線程5");
        Thread t6 = new Thread(myPriority,"線程6");

        t1.setPriority(Thread.MIN_PRIORITY);
        t2.setPriority(7);
        t3.setPriority(Thread.MAX_PRIORITY);
        t4.setPriority(4);
        t5.setPriority(9);
        t6.setPriority(2);

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
        t6.start();
    }
}

class MyPriority implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"--->"+Thread.currentThread().getPriority());
    }
}

           

3.2.7 守護線程_daemon

  • 線程分為使用者線程和守護線程(daemon)
  • 虛拟機需要確定使用者線程執行完畢
  • 虛拟機不用等待守護線程執行完畢(如,背景記錄記錄檔,監控記憶體,垃圾回收等)
  • 也就是說可以做到主線程結束了,但守護線程還沒結束

代碼示範:

public class 守護線程 {
    public static void main(String[] args) {
        God god = new God();
        You you = new You();

        Thread godThread = new Thread(god, "守護線程");
        Thread youThread = new Thread(you, "普通線程");
        godThread.setDaemon(true);	//設定為守護線程

        godThread.start();
        youThread.start();
    }

}

class God implements Runnable{
    @Override
    public void run() {
        while (true){
            System.out.println("我是上帝,是你的守護線程");
        }
    }
}

class You implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 30; i++) {
            System.out.println("我是普通人,隻有三萬多天的日子");
        }
        System.out.println("======一個月完美的結束了======");
    }
}
           

3.2.8 線程存儲ThreadLocal

用于存儲一個線程專有的值【對象方法】

ThreadLocal類,來建立工作記憶體中的變量,它将我們的變量值存儲在内部(隻能存儲一個變量),不同的變量通路到ThreadLocal對象時,都隻能擷取到自己線程所屬的變量。【每個線程的工作記憶體空間不同,是以線程之間互相獨立,互不相關】

public static void main(String[] args) throws InterruptedException {
    ThreadLocal<String> local = new ThreadLocal<>();  //注意這是一個泛型類,存儲類型為我們要存放的變量類型
    Thread t1 = new Thread(() -> {
        local.set("lbwnb");   //将變量的值給予ThreadLocal
        System.out.println("線程1變量值已設定!");
        try {
            Thread.sleep(2000);    //間隔2秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("線程1讀取變量值:");
        System.out.println(local.get());   //嘗試擷取ThreadLocal中存放的變量
    });
    Thread t2 = new Thread(() -> {
        local.set("yyds");   //将變量的值給予ThreadLocal
        System.out.println("線程2變量值已設定!");
    });
    t1.start();
    Thread.sleep(1000);    //間隔1秒
    t2.start();
}

//結果:lbwnb。就算t2也設定了值,但不影響t1的值
           

拓展:子類線程也獲得不了父類線程設定的值,但可以通過用InheritableThreadLocal方法來解決這個問題。(在InheritableThreadLocal存放的内容,會自動向子線程傳遞)

public static void main(String[] args) {
    ThreadLocal<String> local = new InheritableThreadLocal<>();
    Thread t = new Thread(() -> {
       local.set("lbwnb");
        new Thread(() -> {
            System.out.println(local.get());
        }).start();
    });
    t.start();
}
           

3.2.9 等待與喚醒

等待wait和喚醒notify、notifyall都需要在同步代碼内(鎖方法 or 鎖代碼塊)等待和喚醒隻能由鎖對象調用。(鎖代碼塊的鎖對象容易看出,鎖方法的鎖對象一般是this或方法所在的類)

public void wait() : 讓目前線程進入到等待狀态 此方法必須鎖對象調用.

public void notify() : 喚醒目前鎖對象上等待狀态的線程 此方法必須鎖對象調用.會繼續執行wait()方法之後的代碼

方法名 作用
wait() 表示線程一直等待,直到其他線程通知,與sleep不同,會釋放鎖
wait(long timeout) 指定等待的毫秒數
notify() 喚醒一個處于等待狀态的線程
notifyAll() 喚醒同一個對象上所有調用wait()方法的線程,優先級别高的線程優先排程

注意:均是Object類的方法,都隻能在同步方法或者同步代碼塊中使用,否則會抛出異常IllegalMonitorStateException

示例:

顧客與老闆線程:

建立一個顧客線程(消息者):告訴老闆要吃什麼 調用wait方法,放棄cpu的執行,進入wating狀态(無限等待)

建立一個老闆線程(生産者):花5秒做好 做好後 調用notify方法 喚醒顧客 開吃

注意

  • 顧客與老闆線程必須使用同步代碼塊包裹起來,保證等待和喚醒隻能有一個在執行同步使用的鎖必須要保證唯一,
  • 隻有鎖對象才能調用wait和notify方法

顧客線程

Java多線程(一篇從0講透)

老闆線程

Java多線程(一篇從0講透)
Object obj = new Object();
new Thread(){
    @Override
    public void run() {
        synchronized (obj){
            System.out.println("告訴老闆要吃餃子");
            try {
                obj.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("做好===開始吃餃子");
        }
    }
}.start();
new Thread(){
    @Override
    public void run() {
        synchronized (obj){
            try {
                Thread.sleep(3000);
                System.out.println("老闆餃子已經做好");
                obj.notify();//喚醒目前鎖對象上的等待線程
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}.start();
           

3.2.10 小結

  1. 進入計時等待狀态的兩種方式
  2. 使用sleep(long m)方法,在毫秒值結束後,線程睡醒,進入Runnable/Blocked狀态(抱着鎖睡覺,不放鎖)
  3. 使用wait(long m)方法wait方法如果在毫秒值結束之後,還沒有被喚醒,就會自動醒來,進入Runnable/Blocked狀态(等待的時候會釋放鎖)
  4. 兩種喚醒的方法
  5. public void notify()

    随機喚醒1個

  6. public void notifyall()

    喚醒鎖對象上所有等待的線程.

4. 線程安全

4.0 線程同步機制

多個線程操作同一個資源

并發:同一個對象被多個線程同時操作

處理多線程問題時,多個線程通路同一個對象,并且某些線程還想修改這個對象,這時候我們就需要線程同步。線程同步其實就是一個等待機制,多個需要同時通路此對象的線程進入這個對象的等待池形成隊列,等待前面的線程時候完畢,下一個線程再使用。

線程同步

  • 由于同一程序的多個線程共享同一塊存儲空間,在帶來友善的同時,也帶來了通路沖突問題,為了保證資料在方法中被通路時的正确性,在通路時加入鎖機制synchronized,當一個線程獲得對象的排它鎖,獨占資源,其他線程必須等待,使用後釋放鎖即可。存在以下問題一個線程持有鎖會導緻其他所有需要此鎖的線程挂起在多線程競争下,加鎖,釋放鎖會導緻比較多的上下文切換和排程延時,引起性能問題如果一個優先級高的線程等待一個優先級低的線程釋放鎖會導緻優先級倒置,引起性能問題

4.1 什麼是線程安全

多線程通路了共享的資料,就會産生線程的安全

舉例:

  1. 多個視窗,同時賣一種票. 如果不進行控制, 可以會出現賣重複的現象
  2. 多個視窗,同時在銀行同一賬戶取錢,銀行不進行控制就會虧錢
  3. ArrayList線程不安全

4.1.1 買票問題

解決措施:可鎖代碼塊可鎖方法,後面的解決方案是以買票問題為例

代碼示範:

//買票問題
public class UnsafeTicket implements Runnable{

    private static Boolean falg = true;
    private int ticket =10;//票數

    public static void main(String[] args) {
        UnsafeTicket demo1 = new UnsafeTicket();
        new Thread(demo1,"小紅").start();
        new Thread(demo1,"小明").start();
        new Thread(demo1,"黃牛").start();
    }

    @Override
    public void run() {
        while (falg){
            buy();
        }
    }

    //買票
    public void buy(){
        //沒票了就停止線程
        if (ticket<=0) {
            falg = false;
            return;
        }
        //還有票就繼續買
        System.out.println(Thread.currentThread().getName()+"買到了第"+ticket+"張票");
        ticket--;
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
           

結果:會出現三個人同時去了第5張票、也可能會出現有人取0 -1張票

4.1.2 銀行取錢問題

解決措施:使用代碼塊鎖account

代碼示範:

//銀行取錢問題
public class UnsafeBank {
    public static void main(String[] args) {
        Account account = new Account(100, "結婚基金");
        Bank drawMoney1 = new Bank(account, 50, "新一");
        Bank drawMoney2 = new Bank(account, 100, "小蘭");

        drawMoney2.start();
        drawMoney1.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("卡内餘額:" + account.money);
    }

}

//賬戶
class Account{
    int money;//賬戶内的錢
    String name;//卡名

    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }
}

//銀行
class Bank extends Thread{
    Account account;//操縱的賬戶
    int drawingMoney;//取了多少錢

    public Bank(Account account, int drawingMoney,String who) {
        super(who);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public void run() {
        drawing();
    }
    //取錢
    private void drawing() {
        if (this.account.money - drawingMoney < 0) {
            System.out.println("餘額不足," + Thread.currentThread().getName() + "取錢失敗");
            return;
        }
        //sleep可以提高問題的發生的機率!
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.account.money = this.account.money - drawingMoney;
        System.out.println(Thread.currentThread().getName() + "取了" + drawingMoney);
    }
}
           

結果:

Java多線程(一篇從0講透)

4.1.3 ArrayList問題

解決措施:鎖代碼塊

代碼示範:

public class UnsafeArrayList {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();

        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                int x = 10;
                list.add(x++);
            },"list線程不安全").start();
        }
        System.out.println(list.size());
    }
}
           

結果:9997,少了三個,是因為前面插入資料的時候有三個下标被重複指派,導緻有三次指派被覆寫了。

4.2 解決線程安全

鎖類模闆 和 鎖用該類模闆建立出來的對象 兩者之間互不影響!

4.2.1 synchronized鎖代碼塊

同步代碼塊synchronized的格式:

synchronized(鎖對象obj){
            出現安全問題的代碼(通路了共享資料的代碼)
 }
           

注意

1.鎖對象可以是任意對象 new Person new Student ...(一般是鎖變化的對象,需要增删改的對象)

2.必須保證多個線程使用的是同一個鎖對象

3.鎖對象的作用:把{}中代碼鎖住,隻讓一個線程進去執行

1)鎖執行個體對象

适用于使用同一個Runnable對象建立多個線程的情況,不适用于多個Runnable對象分别建立多個線程的情況

作用範圍是對象執行個體,不可跨對象,是以多個線程不同對象執行個體通路此方法,互不影響,無法産生互斥。由于本題搶票中是多個線程使用同一個Runnable對象,是以得到的鎖是同一個對象産生的obj,可以實作線程隔離。但銀行例子中是多個線程分别使用不同的Runnable對象,是以使用鎖執行個體對象是沒用的。

示例

Java多線程(一篇從0講透)
public class TicketRunnableImpl implements Runnable {
    //定義共享的票源
    private int ticket = 100;
    private Object obj = new Object(); //鎖對象
    //線程任務:賣票
    @Override
    public void run() {
        synchronized (obj){
            while (ticket > 0) {
                /*為了提高線程安全問題出現的幾率
                  讓線程睡眠10毫秒,放棄cpu的執行權*/
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //賣票操作,ticket--
                    System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
                    ticket--;
            }
        }
    }
    
    
    public static void main(String[] args) {
        UnsafeTicket demo1 = new UnsafeTicket();
        new Thread(demo1,"小紅").start();
        new Thread(demo1,"小明").start();
        new Thread(demo1,"黃牛").start();
    }
}
           

總結:

同步螢幕的執行過程(同步方法中無需指定同步螢幕,因為同步方法的同步螢幕就是this,就是這個對象本身,或者是class)

第一個線程通路,鎖定同步螢幕,執行其中的代碼第二個線程通路,發現同步螢幕被鎖定,無法通路,處于阻塞狀态,一直等待第一個線程通路完畢,解鎖同步螢幕第二個線程通路,發現同步螢幕沒有鎖,然後鎖定并通路

2)鎖類

适用于使用同一個Runnable對象建立多個線程的情況,也适用于多個Runnable對象分别建立多個線程的情況

雖然是通過對象通路的此方法,但是加鎖的代碼塊是類級别的跨對象的,是以鎖的範圍是針對類,多個線程通路互斥。

public class SynchronizedDemo {
    // 代碼塊鎖(類):鎖的應用對象是User類,可以稱之為類鎖
    public void method2() {
        synchronized (User.class) {
            // TODO 業務邏輯
        }
    }

    public static void main(String[] args) {
        SynchronizedDemo obj1 = new SynchronizedDemo();
        SynchronizedDemo obj2 = new SynchronizedDemo();
        new Thread(() ->{
            obj1.method2(); //代碼塊鎖,後面是類,多線程通路互斥
        }).start();
        new Thread(() ->{
            obj2.method2();
        }).start();
    }
}
           

4.2.2 synchronized鎖方法

鎖的是this,也就是主方法裡調用該方法的對象

同步方法解決線程安全的格式:

修飾符 synchronized 傳回值類型 方法名(參數清單){
    出現安全問題的代碼(通路了共享資料的代碼)
}
           

使用步驟

1.建立一個方法,方法的修飾符添加上synchronized

2.把通路了共享資料的代碼放入到方法中

3.調用同步方法

1)鎖普通方法(對象鎖)

适用于使用同一個Runnable對象建立多個線程的情況,不适用于多個Runnable對象分别建立多個線程的情況

普通方法作用範圍是對象執行個體,不可跨對象,是以多個線程不同對象執行個體通路此方法,互不影響,無法産生互斥。由于本題搶票中是多個線程使用同一個Runnable對象,是以得到的鎖是同一個類對象this,可以實作線程隔離。但銀行例子中是多個線程分别使用不同的Runnable對象最後鎖的this也是不同類對象的this,是以使用鎖普通方法是沒用的。

示例

Java多線程(一篇從0講透)
@Override
public void run() {
    ticketMethods();
}
public synchronized void ticketMethods(){
    while (ticket > 0) {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //賣票操作,ticket--
        System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
        ticket--;
    }
}
           

鎖對象是誰???

鎖對象為this

Java多線程(一篇從0講透)
public  void ticketMethods(){
    synchronized(this){
        while (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //賣票操作,ticket--
            System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
            ticket--;
        }
    }
}
           

2)鎖靜态方法(類鎖)

适用于使用同一個Runnable對象建立多個線程的情況,也适用于多個Runnable對象分别建立多個線程的情況

靜态方法是通過類通路,是類級别的跨對象的,是以鎖的範圍是針對類,多個線程通路互斥。

示例:變化的量記得也要static

Java多線程(一篇從0講透)
public class TicketRunnableImpl implements Runnable {
    //定義共享的票源
    private static int ticket = 100;
    private Object obj = new Object(); //鎖對象
    //線程任務:賣票
    @Override
    public void run() {
        ticketMethods();
    }
    public static synchronized void ticketMethods(){
        while (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //賣票操作,ticket--
            System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
            ticket--;
        }
    }
}
           

鎖對象是誰???

對于static方法,我們使用目前方法所在類的位元組碼對象(類名.class)

Java多線程(一篇從0講透)

4.2.3 Lock鎖

概述

  • 從jdk5.0開始,java提供了更強大的線程同步機制——通過顯示定義同步鎖對象來實作同步。同步鎖使用Lock對象充當
  • java.util.concurrent.locks.Lock接口是控制多個線程對共享資源進行通路的工具。鎖提供了對共享資源的獨占通路,每次隻能有一個線程對Lock對象加鎖,線程開始通路共享資源之前先獲得Lock對象
  • 。ReetrantLock(可重入鎖)類實作了Lock,它擁有與synchronized相同的并發性和記憶體語義,在實作線程安全的控制中,比較常見的是ReetrantLock,可以顯示加鎖、釋放鎖

Lock接口中的方法

void lock() 擷取鎖。
void unlock() 釋放鎖。//如果有try/catch的話,一般unlock是放在finally裡
           

使用步驟

1.在成員位置建立一個Lock接口的實作類對象ReentrantLock

2.在可能會出現安全問題的代碼前,調用lock方法擷取鎖對象

3.在可能會出現安全問題的代碼後,調用unlock方法釋放鎖對象

示例

Java多線程(一篇從0講透)
public class TicketRunnableImpl implements Runnable {
    //定義共享的票源
    private  int ticket = 100;
    //1.在成員位置建立一個Lock接口的實作類對象ReentrantLock
    Lock l = new ReentrantLock();
    //線程任務:賣票
    @Override
    public void run() {
        while (true) {
            l.lock();
            if (ticket > 0){
                try {
                    Thread.sleep(10);
                    //賣票操作,ticket--
                    System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
                    ticket--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //3.在可能會出現安全問題的代碼後,調用unlock方法釋放鎖對象
                    l.unlock(); //無論程式是否異常,都會把鎖對象釋放,節約記憶體提高程式的效率
                }
            }
        }
    }
}
           

4.2.4 synchronized與Lock對比

  • Lock是顯式鎖(手動開啟和關閉鎖,别忘記關閉鎖)synchronized是隐式鎖,除了作用域自動釋放
  • Lock隻有代碼塊鎖,synchronized有代碼塊鎖和方法鎖
  • 使用Lock鎖,JVM将花費較少的時間來排程線程,性能更好。并且具有更好的擴充性(提供更多的子類)
  • 優先使用順序:Lock > 同步代碼塊(已經進入了方法體,配置設定了響應資源)> 同步方法(在方法體之外)

4.2.5 判斷鎖的對象是誰

8鎖現象:

1)标準情況下,一個對象 兩個同步方法 第一個線程先拿到鎖 誰先執行

2)一個對象 兩個同步方法 第一個線程先拿到鎖 第一個方法延遲4S 誰先執行

3)一個對象 一個同步方法一個普通方法 第一個線程先拿到鎖 誰先執行

4)兩個對象 兩個同步方法 第一個線程先拿到鎖 第一個方法延遲4S 誰先執行

5)一個對象 兩個靜态同步方法 第一個線程先拿到鎖 第一個方法延遲4S 誰先執行

6)兩個對象 兩個靜态同步方法 第一個線程先拿到鎖 第一個方法延遲4S 誰先執行

7)一個對象 一個靜态同步方法一個普通同步方法 第一個線程先拿到鎖 第一個方法延遲4S 誰先執行

8)兩個個對象 一個靜态同步方法一個普通同步方法 第一個線程先拿到鎖 第一個方法延遲4S 誰先執行

package com.ambition;
import java.util.concurrent.TimeUnit;
/**
 * 同一個對象  兩個線程  兩個同步方法 誰先執行?
 **/
public class Question1 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> phone.sendMsg()).start();
//        延遲的目的是控制哪個線程先拿到鎖
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> phone.call()).start();
    }
}
class Phone {
//    synchronized 鎖的對象是方法的調用者
//    兩個方法用的是同一個鎖 誰先拿到誰先執行
    public synchronized void sendMsg() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("發短信");
    }
    public synchronized void call() {
        System.out.println("打電話");
    }
}
           

小結:

對于普通同步方法,鎖是目前new執行個體對象。對于static 靜态同步方法,鎖是目前類模闆的Class對象。

5. 生産者與消費者

5.1 問題介紹與分析

1.線程通信

  • 應用場景:生産者和消費者問題假設倉庫中隻能存放一件産品,生産者将生産出來的産品放入倉庫,消費者将倉庫中的産品取走消費如果倉庫中沒有産品,則生産者将産品放入倉庫,否則停止生産并等待,直到倉庫中的産品被消費者取走為止如果倉庫中放有産品,則消費者可以将産品取走消費,否則停止消費并等待,直到倉庫中再次放入産品為止
Java多線程(一篇從0講透)

2.線程通訊-分析

  • 這個一個線程同步問題,生産者和消費者共享同一個資源,并且生産者和消費者之間互相依賴,互為條件。對于生産者,沒有生産産品之前,要通知消費着等待,而生産了産品之後,有需要馬上通知消費者消費對于消費者,在消費之後,要通知生産者已經結束消費,需要生産新的産品以供消費在生産者消費者問題中,僅有synchronized是不夠的,就需要用到之前講的等待與喚醒。synchronized可以阻止并發更新同一個共享資源,實作了同步synchronized不能用來實作不同線程之前的消息傳遞(通信)

5.2 解決方法

5.2.1 管程法

生産者——緩存區——消費者

并發協作模型”生産者/消費者模式“-->管程法

  • 生産者:負責生産資料的子產品(可能是方法,對象,線程,程序)
  • 消費者:負責處理資料的子產品(可能是方法,對象,線程,程序)
  • 緩沖區:消費者不能直接使用生産者的資料,他們之間有個“緩沖區”

生産者将生産好的資料放入緩沖區,消費者從緩沖區拿出資料

代碼:

//餐廳模式:生産者————廚師、消費者————顧客
public class 管程法 {
    public static void main(String[] args) {
        SynContainer container = new SynContainer();
        new Productor(container).start();
        new Cousumer(container).start();
    }
}

/**
 * 生産者
 */
class Productor extends Thread {
    /**
     * 緩沖區
     */
    private SynContainer container;

    public Productor(SynContainer container) {
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            container.push(new Chicken(i));
//                System.out.println("生産了" + i + "隻雞");
        }
    }
}

/**
 * 消費者
 */
class Cousumer extends Thread {
    /**
     * 緩沖區
     */
    private SynContainer container;

    public Cousumer(SynContainer container) {
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            Chicken pop = container.pop();
//                System.out.println("消費了" + pop.getId() + "隻雞");
        }
    }

}

/**
 * 雞(食物)
 */
class Chicken {
    /**
     * 雞的編号
     */
    private int id;

    public Chicken(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }
}

/**
 * 緩沖區
 */
class SynContainer {
    /**
     * 緩沖區的容器大小(十隻雞)
     */
    private Chicken[] chickens = new Chicken[10];
    /**
     * 計數器
     */
    private int count = 0;
    /**
     * 生産者往容器中放入産品
     */
    public synchronized void push(Chicken chicken) {
        //如果容器滿了,生産者就需要等待消費者消費
        if (count == 10) {
            //生産者開始等待消費者消費
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //如果沒有滿,生産者往容器中繼續放入産品
        chickens[count] = chicken;
        count++;
        System.out.println("生産了" + chicken.getId() + "隻雞");
        //生産者通知消費者消費
        this.notifyAll();
        //模拟生産者要休息一下下
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 消費者消費容器中的産品
     */
    public synchronized Chicken pop() {
        //消費者判斷容器中是否有産品
        if (count == 0) {
            //消費者等待生産者生産
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //消費者開始消費容器中的産品
        count--;
        Chicken chicken = chickens[count];
        System.out.println("消費了" + chicken.getId() + "隻雞");
        //消費者通知生産者繼續生産
        this.notifyAll();
        return chicken;
    }
}
           

5.2.2 信号燈法(常用)

flag标志位來告訴消費者繼續/停止消費,告訴生産者繼續/停止生産

并發協作模型”生産者/消費者模式“-->信号燈法

public class 信号燈法 {
    public static void main(String[] args) {
        TV tv = new TV();
        new Actor(tv).start();
        new Audience(tv).start();
    }
}

//演員
class Actor extends Thread{
    TV tv = new TV();

    public Actor(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 20; i++) {
            if (i % 2 == 0) {
                tv.push("快樂大學營");
            } else {
                tv.push("抖音");
            }
        }
    }
}

//聽衆
class Audience extends Thread{
    TV tv = new TV();

    public Audience(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 20; i++) {
            tv.pop();
        }
    }
}

//電視節目
class TV {
    String name;//節目名稱
    boolean flag=true; //标志位 T生産  F觀看

    //生産節目
    public synchronized void push(String name){
        //判斷要不要生産
        //不生産就等待
        if (!flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //生産
        this.name=name;
        flag=!flag;
        System.out.println("我生産了"+name);
        //生産完就喚醒觀衆
        this.notifyAll();
    }

    //消費節目
    public synchronized void pop(){
        //判斷有沒有節目看
        //沒有節目看就等待
        if (flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //有節目就消費
        flag=!flag;
        System.out.println("我看完了"+name);
        //看完就讓演員再演
        this.notifyAll();
    }
}