Java多執行緒詳解

小智學程式設計發表於2021-04-10

多執行緒概述

  • 執行緒簡介
  • 執行緒實現(重點)
  • 執行緒狀態
  • 執行緒同步(重點)
  • 執行緒通訊問題
  • 高階主題

執行緒、程式、多執行緒

  • 多工

    image

    • 現實生活中太多這樣同時做多件事情的例子了,看起來是多個任務在做,其實本質上我們的大腦在同一時間依舊只做一件事情
  • 多執行緒

    image

    • 原來是一條路,慢慢因為車太多,道路堵塞,效率極低。為了提高使用效率,能夠充分利用道路,於是加了多個車道。從此媽媽再也不用擔心道路堵塞了
    • 說說大家的多執行緒例子(生活、遊戲、程式設計)
  • 普通方法呼叫和多執行緒

    image

  • 程式.程式.執行緒

    image

Process和Thread

  • 說起來程式,就不得不說下程式。程式是指令和資料的有序集合,其本身沒有任何執行的含義,是一個靜態的概念

  • 而程式則是執行程式的一次執行過程,它是一個動態的概念。是系統資源分配的單位

  • 通常在一個程式中可以包含若干個執行緒,當然一個程式中至少有一個執行緒,不然沒有存在的意義。執行緒是CPU排程和執行的單位

    注意:很多多執行緒是模擬出來的,真正的多執行緒是指有多個CPU,即多核,如伺服器。如果是模擬出來的多執行緒,即在一個CPU的情況下,在同一時間點,CPU只能執行一個程式碼,因為切換的很快,所以就有同時執行的錯覺

本章核心概念

  • 執行緒就是獨立的執行路徑
  • 在程式執行時,即使沒有自己建立執行緒,後臺也會有多個執行緒,如主執行緒,gc執行緒
  • main()稱之為主執行緒,為系統入口,用於執行整個程式
  • 在一個程式中,如果開闢了多個執行緒,執行緒的執行由排程器安排排程,排程器是與作業系統緊密相關的,先後順序是不能人為干預的
  • 對同一份資源操作時,會存在資源搶奪問題,需要加入併發控制
  • 執行緒會帶來額外的開銷,如CPU排程時間,併發控制開銷
  • 每個執行緒在自己的工作記憶體互動,記憶體控制不當會造成資料不一致

執行緒建立

  • 三種建立方式
    • 繼承Thread類(重點)
    • 實現Runnable介面(重點)
    • 實現Callable介面(現階段瞭解)

Thread

  • 自定義執行緒類繼承Thread類
  • 重寫run()方法,編寫執行緒執行體
  • 建立執行緒物件,呼叫start()方法啟動執行緒
  • 執行緒不一定立即執行,CPU安排排程

學習提示:檢視JDK幫助文件

package tread;
//建立執行緒方式一:繼承Thread類,重寫run()方法,呼叫start開啟執行緒
public class TestThread extends Thread {
    //執行緒入口點
    @Override
    public void run() {
        //run方法執行緒體
        for (int i = 0; i < 20; i++) {
            System.out.println("我在看程式碼===========");
        }
    }

    //main執行緒,主執行緒
    public static void main(String[] args) {
        //建立執行緒物件
        TestThread thread1 = new TestThread();
        //呼叫start方法,開啟執行緒
        //thread1.start();//我在學習多執行緒迴圈和我在看程式碼迴圈同時執行,交替執行
        thread1.run();//我在看程式碼迴圈走完才執行我在學習多執行緒迴圈

        for (int i = 0; i < 2000; i++) {
            System.out.println("我在學習多執行緒=========="+i);
        }
    }
}
#注意:執行緒開啟不一定立即執行,由CPU排程

網圖下載

package tread;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;

//練習thread,實現多執行緒同步下載圖片
public class TestThread2 extends Thread{
    private String url;//網路圖片地址
    private String name;//儲存的檔名

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

    //下載圖片執行緒的執行體
    @Override
    public void run() {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downLoader(url,name);
        System.out.println("下載了檔名為" + name);
    }

    public static void main(String[] args) {
        TestThread2 t1 = new TestThread2(
                "https://gimg2.baidu.com/image_search/src=http%3A%2F%2F01.minipic.eastday.com%2F20171211%2F20171211185710_66c75b95e5421bbc73d255836976d21d_5.jpeg&refer=http%3A%2F%2F01.minipic.eastday.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1619060788&t=749e179f08ffeb44b9b6a502b68ed70a",
                "寶馬跑車.jpg");
        TestThread2 t2 = new TestThread2(
                "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fattach.bbs.miui.com%2Fforum%2F201205%2F03%2F01400598djmyeczcskh2yr.jpg&refer=http%3A%2F%2Fattach.bbs.miui.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1619061019&t=65367c12edf8aeb3224916794edfd831",
                "阿狸.jpg");
        TestThread2 t3= new TestThread2(
                "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Faliyunzixunbucket.oss-cn-beijing.aliyuncs.com%2Fjpg%2Fcd46effe3def00805ef90dd5b20b1b22.jpg%3Fx-oss-process%3Dimage%2Fresize%2Cp_100%2Fauto-orient%2C1%2Fquality%2Cq_90%2Fformat%2Cjpg%2Fwatermark%2Cimage_eXVuY2VzaGk%3D%2Ct_100&refer=http%3A%2F%2Faliyunzixunbucket.oss-cn-beijing.aliyuncs.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1619061081&t=a099d4bad9b5c8c4203427411c765365",
                "狼牙.jpg");
        //執行緒不是按順序執行的,是同時執行的
        t1.start();
        t2.start();
        t3.start();
    }
}

//下載器
class WebDownloader{
    //下載方法
    public void downLoader(String url,String name){
        try {
            FileUtils.copyURLToFile(new URL(url),new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO異常,downloader方法出現問題");
        }
    }
}

實現Runnable

  • 定義MyRunnable類實現Runnable介面
  • 實現run()方法,編寫執行緒執行體
  • 建立執行緒物件,呼叫start()方法啟動執行緒
  • 推薦使用Runnable物件,因為Java單繼承的侷限性
package tread;

//建立執行緒方式2:實現Runnable介面,重寫run方法,執行執行緒需要丟入Runnable實現類,呼叫start方法
public class TestRunnable1 implements Runnable{
    @Override
    public void run() {
        //run方法執行緒體
        for (int i = 0; i < 200; i++) {
            System.out.println("我在看程式碼========="+i);
        }
    }

    public static void main(String[] args) {
        //建立runnbale介面的實現類物件
        TestRunnable1 testRunnable1 = new TestRunnable1();
        //建立執行緒物件,通過執行緒物件來開啟我們的執行緒,(代理類物件)
        /*Thread thread = new Thread(testRunnable1);
        thread.start();*/
        new Thread(testRunnable1).start();

        for (int i = 0; i < 2000; i++) {
            System.out.println("我在學習多執行緒===" + i);
        }
    }
}

小結:

  • 繼承Thread類

    • 子類繼承Thread類具備多執行緒能力
    • 啟動執行緒:子類物件.start()
    • 不建議使用:避免OOP單繼承侷限性
  • 實現Runnable介面

    • 實現介面Runnable具有多執行緒能力

    • 啟動執行緒:傳入目標物件+Thread物件.start()

    • 推薦使用:避免單繼承侷限性,靈活方便,方便同一個物件被多個執行緒使用

      //一份資源
      TestRunnable1 testRunnable1 = new TestRunnable1();
      //多個代理
      new Thread(testRunnable1,"小明").start();
      new Thread(testRunnable1,"小紅").start();
      new Thread(testRunnable1,"老師").start();
      

初始併發問題

package tread;

//多個執行緒同時操作同一個物件
//買火車票的例子

//發現問題:多個執行緒操作同一個資源時,執行緒不安全,資料紊亂
public class TestThread4 implements Runnable{
    //票數
    private int ticketNums =10;

    @Override
    public void run() {
        while (true){
            if (ticketNums<=0){
                break;
            }
            //模擬延時
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().
                    getName() +
                    "-->拿到了第" +
                    ticketNums-- +
                    "票");
        }
    }

    public static void main(String[] args) {
        TestThread4 ticket = new TestThread4();
        new Thread(ticket,"小明").start();
        new Thread(ticket,"小張").start();
        new Thread(ticket,"小紅").start();
    }
}

案例:龜兔賽跑

  • 要求:

    1. 首先來個賽道距離,然後要離終點越來越近
    2. 判斷比賽是否結束
    3. 列印出勝利者
    4. 龜兔賽跑開始
    5. 故事中是烏龜贏的,兔子需要睡覺,所以我們來模擬兔子睡覺
    6. 終於烏龜贏得比賽
    package tread;
    
    public class Race implements Runnable {
    
        //勝利者
        private static String winner;
        @Override
        public void run() {
            for (int i = 0; i <= 100; i++) {
                //模擬兔子休息
                if (Thread.currentThread().getName().equals("兔子")&&i%10==0){
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //判斷比賽是否結束
                boolean flag=gameOver(i);
                //如果比賽結束,就停止程式
                if (flag){
                    break;
                }
                System.out.println(Thread.currentThread().getName() + "-->跑了 " + i + " 步");
            }
        }
    
        //判斷是否完成比賽
        private boolean gameOver(int steps){
            //判斷是否有勝利者
            if (winner!=null){//已經有勝利者
                return true;
            }else {
                if (steps>=100){
                    winner =Thread.currentThread().getName();
                    System.out.println("winner is " + winner);
                    return true;
                }
            }
            return false;
        }
    
        public static void main(String[] args) {
            Race race = new Race();
            new Thread(race,"兔子").start();
            new Thread(race,"烏龜").start();
        }
    }
    

實現Callable介面(瞭解即可)

  1. 實現Callable介面,需要返回值型別

  2. 重寫call方法,需要丟擲異常

  3. 建立目標物件

  4. 建立執行任務:ExecutorService ser = Executors.newFixedThreadPool(1);

  5. 提交執行任務:Future result1 = ser.submit(t1);

  6. 獲取結果:Boolean r1 = result1.get();

  7. 關閉伺服器:ser.shutdownNow();

    演示:利用Callable改造下載圖片案例

    package tread.demo2;
    
    import org.apache.commons.io.FileUtils;
    import tread.TestThread2;
    
    import java.io.File;
    import java.io.IOException;
    import java.net.URL;
    import java.util.concurrent.*;
    
    //建立方式3:實現Callable介面
    /*
      好處:
      1.可以定義返回值
      2.可以丟擲異常
     */
    public class TestCallable implements Callable<Boolean> {
        private String url;//網路圖片地址
        private String name;//儲存的檔名
    
        public TestCallable(String url,String name){
            this.url=url;
            this.name=name;
        }
    
        //下載圖片執行緒的執行體
        @Override
        public Boolean call() {
            WebDownloader webDownloader = new WebDownloader();
            webDownloader.downLoader(url,name);
            System.out.println("下載了檔名為" + name);
            return true;
        }
    
        public static void main(String[] args) {
            TestCallable t1 = new TestCallable(
                    "https://gimg2.baidu.com/image_search/src=http%3A%2F%2F01.minipic.eastday.com%2F20171211%2F20171211185710_66c75b95e5421bbc73d255836976d21d_5.jpeg&refer=http%3A%2F%2F01.minipic.eastday.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1619060788&t=749e179f08ffeb44b9b6a502b68ed70a",
                    "寶馬跑車.jpg");
            TestCallable t2 = new TestCallable(
                    "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fattach.bbs.miui.com%2Fforum%2F201205%2F03%2F01400598djmyeczcskh2yr.jpg&refer=http%3A%2F%2Fattach.bbs.miui.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1619061019&t=65367c12edf8aeb3224916794edfd831",
                    "阿狸.jpg");
            TestCallable t3= new TestCallable(
                    "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Faliyunzixunbucket.oss-cn-beijing.aliyuncs.com%2Fjpg%2Fcd46effe3def00805ef90dd5b20b1b22.jpg%3Fx-oss-process%3Dimage%2Fresize%2Cp_100%2Fauto-orient%2C1%2Fquality%2Cq_90%2Fformat%2Cjpg%2Fwatermark%2Cimage_eXVuY2VzaGk%3D%2Ct_100&refer=http%3A%2F%2Faliyunzixunbucket.oss-cn-beijing.aliyuncs.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1619061081&t=a099d4bad9b5c8c4203427411c765365",
                    "狼牙.jpg");
            //執行緒不是按順序執行的,是同時執行的
            //建立執行任務:
            ExecutorService ser = Executors.newFixedThreadPool(3);//執行緒池,放入3個執行緒
            // 提交執行任務:
            Future<Boolean> r1 = ser.submit(t1);
            Future<Boolean> r2 = ser.submit(t2);
            Future<Boolean> r3 = ser.submit(t3);
            //獲取結果:
            try {
                Boolean rs1 = r1.get();
                Boolean rs2 = r2.get();
                Boolean rs3 = r3.get();
                System.out.println(rs1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
            //關閉伺服器:
            ser.shutdownNow();
        }
    }
    
    //下載器
    class WebDownloader{
        //下載方法
        public void downLoader(String url,String name){
            try {
                FileUtils.copyURLToFile(new URL(url),new File(name));
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("IO異常,downloader方法出現問題");
            }
        }
    }
    
    

靜態代理模式

  • 結婚案例

    • 你:真實角色
    • 婚慶公司:代理你,幫你處理結婚的事
    • 結婚:都實現結婚介面即可

    演示:實現靜態代理對比Thread

    package tread;
    /*
    靜態代理模式總結:
        真實物件和代理物件都要實現同一個介面
        代理物件要代理真實角色
    好處:
        代理物件可以做好多真實物件做不了的事情
        真實物件專注自己的事情
     */
    public class StaticProxy {
        public static void main(String[] args) {
            You you = new You();//真實結婚物件
            new Thread(()->System.out.println("我愛你")).start();
    
    
            //you.HappyMarry();//之前呼叫方式
            /*WeddingCompany weddingCompany = new WeddingCompany(you);//代理方式,交給婚慶公司呼叫
            weddingCompany.HappyMarry();*/
            new WeddingCompany(you).HappyMarry();
        }
    }
    //結婚類
    interface Marry{
        void HappyMarry();
    }
    
    //真實角色,你去結婚
    class You implements Marry{
        @Override
        public void HappyMarry() {
            System.out.println("小王要結婚了,超級開心");
        }
    }
    
    //婚慶:代理角色,幫助你結婚
    class WeddingCompany implements Marry{
        //代理誰———>真實目標角色
        private Marry target;
    
        public WeddingCompany(Marry target) {
            this.target = target;
        }
    
        @Override
        public void HappyMarry() {
            before();
            this.target.HappyMarry();//這就是真實物件
           after();
        }
    
        private void before() {
            System.out.println("結婚之前,佈置現場");
        }
    
        private void after() {
            System.out.println("結婚之後,收尾款");
        }
    }
    

Lambda表示式

image

  • λ 希臘字母表中排序第十一位的字母,英文名稱為Lambda

  • 避免匿名內部類定義過多

  • 其實質屬於函數語言程式設計的概念

    (params)->expression[表示式]
    (params)->statement[語句]
    (params)->{statements}
    
    a->System.out.println("i like lambda-->" + a);
    new Thread(()->System.out.println("多執行緒學習。。。")).start();
    
    
  • 為什麼要使用lambda表示式

    • 避免匿名內部類定義過多
    • 可以讓你的程式碼看起來很簡潔
    • 去掉一堆沒有意義的程式碼,只留下核心的邏輯
  • 也許你會說,我看了Lambda表示式,不但不覺得簡潔,反而覺得更亂,看不懂了。那是因為我們還沒習慣,用的多了,看習慣了,就好了

  • 理解Functional Interface(函式式介面)是學習Java8 lambda表示式的關鍵所在

  • 函式式介面的定義:

    • 任何介面,如果只包含唯一一個抽象方法,那麼它就是一個函式式介面

      public interface Runnable{
          public abstract void run();
      }
      
    • 對於函式式介面,我們可以通過lambda表示式來建立該介面的物件

案例一:

package tread.lambda;

/**
 * 推導lambda表示式
 * 實現類的一步一步簡化
 * jdk1.8新效能
 */
public class TestLambda1 {
    //3.靜態內部類
    static class Like2 implements ILike{
        @Override
        public void lambda() {
            System.out.println("i like lambda2");
        }
    }
    public static void main(String[] args) {
        ILike like = new Like();
        like.lambda();

        like = new Like2();
        like.lambda();

        //4.區域性內部類
        class Like3 implements ILike{
            @Override
            public void lambda() {
                System.out.println("i like lambda3");
            }
        }
        like = new Like3();
        like.lambda();

        //5.匿名內部類:沒有類的名稱,必須藉助介面或父類
        like=new ILike() {
            @Override
            public void lambda() {
                System.out.println("i like lambda4");
            }
        };
        like.lambda();

        //6.用lambda簡化
        like=()-> {
            System.out.println("i like lambda5");
        };
        like.lambda();
    }
}

//1.定義一個函式式介面
interface ILike{
    void lambda();
}

//2.實現類
class Like implements ILike{
    @Override
    public void lambda() {
        System.out.println("i like lambda");
    }
}

案例二:

package tread.lambda;

/**
 * 簡化lambda表示式
 * 總結:
 *  lambda表示式只能有一行程式碼的情況下才能簡化成一行,如果有多行,那麼就用程式碼塊包裹;
 *  前提是介面為函式式介面
 *  多個引數也可以去掉引數型別,要去就全去,必須加上括號
 */
public class TestLambda2 {
    public static void main(String[] args) {
        Ilove love=null;
        
        love=(int a)->{
            System.out.println("i love you-->" + a);
        };
        love.love(520);
        //簡化1:引數型別    主要使用
        love=(a)->{
            System.out.println("i love you-->" + a);
        };
        love.love(521);
        //簡化2:簡化括號
        love=a->{
            System.out.println("i love you-->" + a);
        };
        love.love(522);
        //簡化3:去掉花括號
        love=a->System.out.println("i love you-->" + a);
        love.love(523);
    }
}
interface Ilove{
    void love(int a);
}

執行緒五大狀態

image

image

執行緒方法

方法 說明
setPriority(int newPriority) 更改執行緒的優先順序
static void sleep(long millis) 在指定的毫秒數內讓當前正在執行的執行緒休眠
void join() 等待該執行緒終止
static void yield() 暫停當前正在執行的執行緒物件,並執行其他執行緒
void interrupt() 中斷執行緒,別用這個方式
boolean isAlive() 測試執行緒是否處於活動狀態

停止執行緒

  • 不推薦使用JDK提供的stop()、destroy()方法。【已廢棄】
  • 推薦執行緒自己停下來
  • 建議使用一個標誌位進行終止變數當flag=false,則終止執行緒執行
package tread;

/**
 * 測試停止執行緒
 *     1.建議執行緒正常停止-->利用次數,不建議死迴圈
 *     2.建議使用標誌位-->設定一個標誌位
 *     3.不要使用stop或destroy等過時或者JDK不建議使用的方法
 */
public class TestStop implements Runnable{
    //1.執行緒中定義執行緒體使用的標識
    private boolean flag=true;
    @Override
    public void run() {
        int i=0;
        while (flag){
            System.out.println("run...Thread"+i++);
        }
    }
    //2.設定一個公開的方法停止執行緒,轉換標誌位
    public void stop(){
        this.flag=false;
    }

    public static void main(String[] args) {
        TestStop testStop = new TestStop();
        new Thread(testStop).start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("main" + i);
            if (i==900){
                //呼叫stop方法切換標誌位,讓執行緒停止
                testStop.stop();
                System.out.println("執行緒停止了");
            }
        }
    }

    /* private boolean flag=true;
    //1.執行緒中定義執行緒體使用的標識
    @Override
    public void run() {
        //2.執行緒體使用該標識
        while (flag){
            System.out.println("run...Thread");
        }
    }
    //3.對外提供方法改變標識
    public void stop(){
        this.flag=false;
    }*/
}

執行緒的禮讓-yield

  • 禮讓執行緒,讓當前正在執行的執行緒暫停,但不阻塞

  • 將執行緒從執行狀態轉為就緒狀態

  • 讓CPU重新排程,禮讓不一定成功!看CPU心情

    package tread;
    
    /**
     * 測試禮讓執行緒
     *    1.禮讓不一定成功,看CPU心情
     *    2.如有ab兩個執行緒,CPU將資源分配給a,那麼a處於執行狀態,
     *      b處於就緒狀態禮讓為讓a釋放資源回到就緒狀態,讓CPU重
     *      新給ab分配資源
     */
    public class TestYield {
        public static void main(String[] args) {
            MyYield myYield = new MyYield();
            new Thread(myYield,"a").start();
            new Thread(myYield,"b").start();
            /*禮讓成功:
                a執行緒開始執行
                b執行緒開始執行
                a執行緒結束了
                b執行緒結束了
              禮讓失敗:
                a執行緒開始執行
                a執行緒結束了
                b執行緒開始執行
                b執行緒結束了
             */
        }
    }
    
    class MyYield implements Runnable{
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "執行緒開始執行");
            Thread.yield();//執行緒禮讓
            System.out.println(Thread.currentThread().getName() + "執行緒結束了");
        }
    }
    

執行緒強制執行-join

  • Join合併執行緒,待此執行緒執行完成後,再執行其他執行緒,其他執行緒阻塞

  • 可以想成插隊

    package tread;
    
    /**
     * 測試join介面
     *     想象成插隊
     */
    public class TestJoin implements Runnable{
        public static void main(String[] args) throws InterruptedException {
            //啟動我們的執行緒
            TestJoin testJoin = new TestJoin();
            Thread thread = new Thread(testJoin);
            thread.start();
            //主執行緒
            for (int i = 0; i < 100; i++) {
                if (i==50){
                    thread.join();//當main執行到第50次時,thread執行緒強制執行,main執行緒阻塞
                }
                System.out.println("main。。。" + i);
            }
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 3000; i++) {
                System.out.println("join......" + i);
            }
        }
    }
    

觀測執行緒狀態

  • Thread.State

    執行緒狀態。執行緒可以處於一下(JDK幫助文件)狀態之一:

    • NEW

      尚未啟動的執行緒處於此狀態(新生狀態)

    • RUNNABLE

      在Java虛擬機器中執行的執行緒處於此狀態(執行狀態)

    • BLOCKED

      被阻塞等待監視器鎖定的執行緒處於此狀態(阻塞狀態)

    • WAITING

      正在等待另一個執行緒執行特定動作的執行緒處於此狀態(阻塞狀態)

    • TIMED_WAITING

      正在等待另一個執行緒執行動作達到指定等待時間的執行緒處於此狀態(阻塞狀態)

    • TERMINATED

      已退出的執行緒處於此狀態(死亡狀態)

一個執行緒可以在給定時間點處於一個狀態。這些狀態是不反映任何作業系統執行緒狀態的虛擬機器狀態

package tread;
//觀察測試執行緒的狀態
public class TestState {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("////");
        });
        //觀察狀態
        Thread.State state = thread.getState();
        System.out.println(state);//NEW
        //觀察啟動後的狀態
        thread.start();//啟動執行緒
        state  = thread.getState();
        System.out.println(state);//Run

        while (state!=Thread.State.TERMINATED){//只要執行緒不停止,就一直輸出狀態
            Thread.sleep(100);
            state=thread.getState();//更行執行緒狀態
            System.out.println(state);//列印執行緒狀態
        }
        
        //thread.start();//這裡執行緒無法啟動,因為執行緒不能啟動兩次。死亡以後的執行緒無法再次啟動
    }
}

執行緒優先順序

  • Java提供一個執行緒排程器來監控程式中啟動後進入就緒狀態的所有執行緒,執行緒排程器按照優先順序決定應該排程那個執行緒來執行
  • 執行緒的優先順序用數字表示,範圍從1~10
    • Thread.MIN_PRIORITY=1;
    • Thread.MAX_PRIORITY=10;
    • Thread.NORM_PRIORITY=5;
  • 使用以下方式改變或獲取優先順序
    • getPriority() setPriority(int xxx)
  • 優先順序的設定建議在start()排程前
  • 優先順序低只是意味著獲得排程的概率低。並不是優先順序低就不會被呼叫了。這都是看CPU的排程
package tread;

/**
 * 測試執行緒優先順序
 *    執行緒優先順序不一定高的就先跑,但是大多數時候優先順序高的先跑
 */
public class TestPriority {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + "--->" + Thread.currentThread().getPriority());
        MyPriority myPriority = new MyPriority();
        Thread t1 = new Thread(myPriority);
        Thread t2 = new Thread(myPriority);
        Thread t3 = new Thread(myPriority);
        Thread t4 = new Thread(myPriority);
        Thread t5 = new Thread(myPriority);
        Thread t6 = new Thread(myPriority);
        //先設定優先順序,再啟動
        t1.start();

        t2.setPriority(1);
        t2.start();

        t3.setPriority(4);
        t3.start();

        t4.setPriority(Thread.MAX_PRIORITY);//MAX_PRIORITY==10
        t4.start();

        /*報錯
        t5.setPriority(-1);
        t5.start();

        t6.setPriority(11);
        t6.start();*/
    }
}
class MyPriority implements Runnable{

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

守護(daemon)執行緒

  • 執行緒分為使用者執行緒和守護執行緒
  • 虛擬機器必須確保使用者執行緒(main)執行完畢
  • 虛擬機器不用等待守護執行緒(gc—垃圾回收機制)執行完畢
    • 如:後臺記錄操作日誌,監控記憶體,垃圾回收等待..
ackage tread;

/**
 * 測試守護執行緒
 * 上帝守護你
 */
public class TestDaemon {
    public static void main(String[] args) {
        God god = new God();
        You2 you2 = new You2();
        Thread thread = new Thread(god);
        thread.setDaemon(true);//預設是false表示是使用者執行緒,正常的執行緒都是使用者執行緒。。。
        thread.start();//上帝守護執行緒啟動

        new Thread(you2).start();//你 使用者執行緒啟動
    }
}
//上帝
class God implements Runnable{

    @Override
    public void run() {
        while (true){
            System.out.println("上帝保護你");
        }
    }
}
//你
class You2 implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 36500; i++) {
            System.out.println("你一生都開心的活著"+i);
        }
        System.out.println("good bye! world!===============================================");
    }
}

執行緒同步

多個執行緒操作同一個資源

併發

  • 併發:同一個物件被多個執行緒同時操作(之前程式碼TestThread4)

image

執行緒同步

  • 現實生活中,我們會遇到“同一個資源,多個人都想使用”的問題,比如,食堂排隊打飯,每個人都想吃飯,最天然的解決辦法就是排隊,一個個來

    image

  • 處理多執行緒問題時,多個執行緒訪問同一個物件,並且某些執行緒還想修改這個物件。這時候我們就需要執行緒同步。執行緒同步其實就是一種等待機制,多個需要同時訪問此物件的執行緒進入這個物件的等待池形成佇列,等待前面執行緒使用完畢,下一個執行緒再使用

  • 由於同意程式的多個執行緒共享同一塊儲存空間,在帶來方便的同時,也帶來了訪問衝突問題,為了保證資料在方法中被訪問時的正確性,在訪問時加入鎖機制synchronized,當一個執行緒獲得物件的排它鎖,獨佔資源,其他執行緒必須等待,使用後釋放鎖即可,存在以下問題:

    • 一個執行緒持有鎖會導致其他所有需要此鎖的執行緒掛起
    • 在多執行緒競爭下,加鎖,釋放鎖會導致比較多的上下文切換和排程延時,引起效能問題
    • 如果一個優先順序高的執行緒等待一個優先順序低的執行緒釋放鎖會導致優先順序倒置,引起效能問題
    • 安全性好,效能低;安全性差,效能高

佇列和鎖

形成安全的執行緒同步的條件:佇列+鎖

三大不安全案例

  • 案例一:不安全的買票

    package tread.syn;
    
    /**
     * 不安全的買票
     * 執行緒不安全,出現了負數
     */
    public class UnsafeBuyTicket {
        public static void main(String[] args) {
            BuyTicket station = new BuyTicket();
            new Thread(station,"小王").start();
            new Thread(station,"小李").start();
            new Thread(station,"小張").start();
        }
    }
    
    class BuyTicket implements Runnable{
        //票
        private int ticketNums=10;
        boolean flag=true;//外部停止方式
        @Override
        public void run() {
            //買票
            while (flag){
                try {
                    buy();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        private void buy() throws InterruptedException {
            //判斷是否有票
            if (ticketNums<=0){
                flag=false;
                return;
            }
            //模擬延時
            Thread.sleep(100);
            //買票
            System.out.println(Thread.currentThread().getName() + "買到了" + ticketNums--);
        }
    }
    
  • 案例二:不安全的取錢

    package tread.syn;
    
    /**
     * 不安全的取錢
     * 兩個人去銀行取錢,同一賬戶
     * 出現了餘額負數
     */
    public class UnsafeBank {
        public static void main(String[] args) {
            Account account = new Account(100, "結婚基金");
            Drawing you = new Drawing(account, 50, "你");
            Drawing girlFriend = new Drawing(account, 100, "妻子");
            you.start();
            girlFriend.start();
        }
    }
    
    //賬戶
    class Account{
         int money;//餘額
         String name;//卡名
    
        public Account(int money, String name) {
            this.money = money;
            this.name = name;
        }
    }
    
    //銀行:模擬取款
    class Drawing extends Thread{
        Account account;//賬戶
        int drawingMoney;//取了多少錢
        int nowMoney;//現在手裡還有多少錢
    
        //構造方法
        public Drawing(Account account,int drawingMoney,String name){
            super(name);
            this.account=account;
            this.drawingMoney=drawingMoney;
        }
        //取錢
        @Override
        public void run() {
            //判斷有沒有錢
            if (account.money-drawingMoney<0){
                System.out.println(Thread.currentThread().getName() + "錢不夠了,取不了");
            }
            //sleep可以放大問題的發生性
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //卡內餘額=餘額-你取的錢
            account.money=account.money-drawingMoney;
            //你手裡的錢
            nowMoney=nowMoney+drawingMoney;
    
            System.out.println(account.name + "餘額為:" + account.money);
            //Thread.currentThread().getName()=this.getName()
            System.out.println(this.getName()+"手裡的錢"+nowMoney);
        }
    }
    
  • 案例三:不安全的集合

    package tread.syn;
    
    import java.util.ArrayList;
    
    /**
     * 執行緒不安全的集合
     */
    public class UnsafeList {
        public static void main(String[] args) {
            ArrayList<String> list = new ArrayList<>();
            for (int i = 0; i < 10000; i++) {
                new Thread(()->{
                    list.add(Thread.currentThread().getName());
                }).start();
            }
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(list.size());//理想情況為10000
        }
    }
    

同步方法及同步塊

同步方法

  • 由於我們可以通過private關鍵字來保證資料物件只能被方法訪問,所以我們只需要針對方法提出一套機制,這套機制就是synchronized關鍵字,它包括兩種用法:synchronized方法和synchronized塊

    public synchronized void method(int args){
        
    }
    
  • synchronized方法控制對"物件"的訪問,每個物件對應一把鎖,每個synchronized方法都必須獲得呼叫該方法的物件的鎖才能執行,否則執行緒會阻塞,方法一旦執行,就獨佔該鎖,直到該方法返回才釋放鎖,後面被阻塞的執行緒才能獲得這個鎖,繼續執行

    # 缺陷:若將一個大的方法申明為synchronized將會影響效率
    
  • 方法裡面需要修改的內容才需要鎖,鎖的太多,浪費資源

同步塊

  • 同步塊:synchronized(Obj){}
  • Obj稱之為同步監視器
    • Obj可以是任何物件,但是推薦使用共享資源作為同步監視器
    • 同步方法中無需指定同步監視器,因為同步方法的同步監視器就是this,就是這個物件本身,或者事class(反射中講解)
  • 同步監視器的執行過程
    1. 第一個執行緒訪問,鎖定同步監視器,執行其中程式碼
    2. 第二個執行緒訪問,發現同步監視器被鎖定,無法訪問
    3. 第一個執行緒訪問完畢,解鎖同步監視器
    4. 第二個執行緒訪問,發現同步監視器沒有鎖,然後鎖定並訪問

CopyOnWriteArrayList

package tread;

import java.util.concurrent.CopyOnWriteArrayList;

/**
 * 測試JUC中安全型別的集合
 */
public class TestJUC {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

死鎖

  • 多個執行緒各自佔有一些共享資源,並且相互等待其他執行緒佔有的資源才能執行,而導致兩個或者多個執行緒都在等待對方釋放資源,都停止執行的情形。某一個同步塊同時擁有"兩個以上物件的鎖"時,就可能會發生"死鎖"的問題

  • 產生死鎖的四個必要條件:

    • 互斥條件:一個資源每次只能被一個程式使用
    • 請求與保持條件:一個程式因請求資源而阻塞時,對已獲得的資源保持不放
    • 不剝奪條件:程式以獲得的資源,在未使用完之前,不能強行剝奪
    • 迴圈等待條件:若干程式之間形成一種頭尾相接的迴圈等待資源關係

    上面列出了死鎖的四個必要條件,我們只要想辦法破其中的任意一個或多個條件就可以避免死鎖發生

Lock鎖

  • 從JDK1.5開始,Java提供了更強大的執行緒同步機制——通過顯式定義同步鎖物件來實現同步。同步鎖使用Lock物件充當

  • java.util.concurrent.locks.Lock介面是控制多個執行緒對共享資源進行訪問的工具。鎖提供了對共享資源的獨佔訪問,每次只能有一個執行緒對Lock物件加鎖,執行緒開始訪問共享資源之前應先獲得Lock物件

  • ReentrantLock(可重入鎖)類實現了Lock,它擁有與synchronized相同的併發性和記憶體語義,在實現執行緒安全的控制中,比較常用的是ReentrantLock,可以顯式加鎖、釋放鎖

  • 程式碼

    class A{
        private final ReentrantLock lock=new ReentrantLock();
        public void m(){
            lock.lock();
            try {
                //保證執行緒安全的程式碼;
            }finally {
                lock.unlock();
                //如果同步程式碼有異常,要將unlock()寫入finally語句塊
            }
        }
    }
    

使用案例:

package tread.syn.gaoji;

import java.util.concurrent.locks.ReentrantLock;

/**
 * 測試Lock鎖
 */
public class TestLock {
    public static void main(String[] args) {
        TestLock2 testLock2 = new TestLock2();
        new Thread(testLock2,"小明").start();
        new Thread(testLock2,"小紅").start();
        new Thread(testLock2,"小張").start();
    }
}
class TestLock2 implements Runnable{
    int ticketNums=10;
    //定義lock鎖
    private final ReentrantLock lock=new ReentrantLock();
    @Override
    public void run() {
        while (true){
            try {
                lock.lock();//加鎖
                if (ticketNums>0){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"買了第"+ticketNums--);
                }else {
                    break;
                }
            }finally {
                //解鎖
                lock.unlock();
            }

        }
    }
}

synchronized與Lock的對比

  • Lock是顯式鎖(手動開啟和關閉鎖,別忘記關閉鎖);synchronized是隱式鎖,出了作用域自動釋放
  • Lock只有程式碼塊鎖,synchronized有程式碼塊鎖和方法鎖
  • 使用Lock鎖,JVM將花費較少的時間來排程執行緒,效能更好。並且具有更好的擴充套件性(提供更多的子類)
  • 優先使用順序:
    • Lock > 同步程式碼塊(已經進入了方法體,分配相應資源) > 同步方法(在方法體之外)

執行緒協作

生產消費者問題

  • 這是一個問題,不是23種模式之一

執行緒通訊

  • 應用場景:生產者和消費者問題

    • 假設倉庫中只能存放意見產品,生產者將生產出來的產品放入倉庫,消費者將倉庫中產品取走消費

    • 如果倉庫中沒有產品,則生產者將產品放入倉庫,否則停止生產並等待,直到倉庫中的產品被消費者取走為止

    • 如果長褲中放有產品,則消費者可以將產品取走消費,否則停止消費並等待,直到倉庫中再次放入產品為止

      image

  • 分析

    這是一個執行緒同步問題,生產者和消費者共享同一個資源,並且生產者和消費者之間相互依賴,互為條件

    • 對於生產者,沒有生產產品之前,要通知消費者等待,而生產了產品之後,又需要馬上通知消費者消費

    • 對於消費者,在消費之後,要通知生產者已經結束消費,需要生產新產品以供消費

    • 在生產者消費問題中,僅有synchronized是不夠的

      • synchronized可阻止併發更新同一個共享資源,實現同步

      • synchronized不能用來實現不同執行緒之間的訊息傳遞(通訊)

  • Java提供了幾個方法解決執行緒之間的通訊問題

    方法名 作用
    wait() 表示執行緒一致等待,直到其他執行緒通知,與sleep不同,會釋放鎖
    wait(long timeout) 指定等待的毫秒數
    notify() 喚醒一個處於等待狀態的執行緒
    notifyAll() 喚醒同一個物件上所有呼叫wait()方法的執行緒,優先順序別高的執行緒優先排程

注意:均是Object類的方法,都只能在同步方法或者同步程式碼塊中使用,否則會丟擲異常IllegalMonitorStateException

  • 解決方式一

    併發協作模型"生產者/消費者模式"--->管程法

    • 生產者:負責生產資料的模組(可能是方法,物件,執行緒,程式)
    • 消費者:負責處理資料的模組(可能是方法,物件,執行緒,程式)
    • 緩衝區:消費者不能直接使用生產者的資料,他們之間有個"緩衝區"

    生產者將生產好的資料放入緩衝區,消費者從緩衝區拿出資料

    package tread.syn.gaoji;
    
    /**
     * 測試生產者消費者模式-->利用緩衝區解決:管程法
     * 生產者,消費者,產品,緩衝區
     */
    public class TestPC {
        public static void main(String[] args) {
            SynContainer container = new SynContainer();
            new Productor(container).start();
            new Consumer(container).start();
        }
    }
    //生產者
    class Productor extends Thread{
        SynContainer container;
    
        public Productor(SynContainer container) {
            this.container = container;
        }
        //生產
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                container.push(new Chicken(i));
                System.out.println("生產了" + i + "只雞");
            }
        }
    }
    //消費者
    class Consumer extends Thread{
        SynContainer container;
    
        public Consumer(SynContainer container) {
            this.container = container;
        }
        //消費
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("消費了——>第" + container.pop().id + "只雞");
            }
        }
    }
    //產品
    class Chicken{
        int id;//產品編號
    
        public Chicken(int id) {
            this.id=id;
        }
    }
    //快取區
    class SynContainer{
        //需要一個容器大小
        Chicken[] chickens=new Chicken[10];
        //容器計數器
        int count=0;
        //生產者放入產品
        public synchronized void push(Chicken chicken){
            //如果容器滿了,就需要等待消費者消費
            if (count==chickens.length){
                //通知消費者消費,生產者等待
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //如果容器沒滿,我們就需要丟入產品
            chickens[count]=chicken;
            count++;
            //可以通知消費者消費了
            this.notifyAll();
    
        }
        //消費者消費產品
        public synchronized Chicken pop(){
            //判斷能否消費
            if (count==0){
                //等待生產者生產,消費者等待
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //如果可以消費
            count--;
            Chicken chicken=chickens[count];
            //吃完了,通知生產者生產
            this.notifyAll();
            return chicken;
    
        }
    }
    
  • 解決方式二

    • 併發協作模型"生產者/消費者模式"--->訊號燈法
    package tread.syn.gaoji;
    
    /**
     *測試生產者消費者問題2:訊號燈法,標誌位解決
     */
    public class TestPC2 {
        public static void main(String[] args) {
            TV tv = new TV();
            new Player(tv).start();
            new Watcher(tv).start();
        }
    }
    //生產者--?演員
    class Player extends Thread{
        TV tv;
        public Player(TV tv){
            this.tv=tv;
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                if (i%2==0){
                    this.tv.play("快樂大本營播放中");
                }else {
                    this.tv.play("抖音:記錄美好生活");
                }
            }
        }
    }
    //消費者-->觀眾
    class Watcher extends Thread{
        TV tv;
        public Watcher(TV tv){
            this.tv=tv;
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
               tv.watch();
            }
        }
    }
    //產品-->節目
    class TV{
        //演員表演,觀眾等待 T
        //觀眾觀看,演員等待 F
        String voice;//表演的節目
        boolean flag=true;//標誌位
        //表演
        public synchronized void play(String voice){
            if (!flag){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("演員表演了" + voice);
            //通知觀眾觀看
            this.notify();//通知喚醒
            this.voice=voice;
            this.flag=!this.flag;
        }
        //觀看
        public synchronized void watch(){
            if (flag){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("觀看了:" + voice);
            //通知演員表演
            this.notifyAll();
            this.flag=!this.flag;
        }
    }
    

執行緒池

  • 背景:經常建立和銷燬、使用量特別大的資源,比如併發情況下的執行緒,對效能影響很大
  • 思路:提前建立好多個執行緒,放入執行緒池中,使用時直接獲取,使用完放回池中。可以避免頻繁建立銷燬、實現重複利用。類似生活中的公共交通工具
  • 好處:
    • 提高響應速度(減少了建立新執行緒的時間)
    • 降低資源消耗(重複利用執行緒池中執行緒,不需要每次都建立)
    • 便於執行緒管理(...)
      • corePoolSize:核心池的大小
      • maximumPoolSize:最大執行緒數
      • keepAliveTime:執行緒沒有任務時最多保持多長時間後會終止
  • JDK5.0起提供了執行緒池相關API:ExecutorService和Executors
  • ExecutorService:真正的執行緒池介面。常見子類ThreadPoolExecutor
    • void execute(Runnable command):執行任務/命令,沒有返回值,一般用來執行Runnable
    • Futuresubmit(Callabletask):執行任務,有返回值,一般用來執行Callable
    • void shutdown():關閉連線池
  • Executors:工具類、執行緒池的工廠類,用於建立並返回不同型別的執行緒池
package tread.syn.gaoji;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 測試執行緒池
 */
public class TestPool {
    public static void main(String[] args) {
        //1.建立服務,建立執行緒池
        //newFixedThreadPool 引數為:執行緒池大小
        ExecutorService service = Executors.newFixedThreadPool(10);
        //執行
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        //2.關閉連線
        service.shutdown();
    }
}
class MyThread implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

總結

package tread.syn.gaoji;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * 回顧總結執行緒的建立
 */
public class ThreadNew {
    public static void main(String[] args) {
        new MyThread1().start();
        new Thread(new MyThread2()).start();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyThread3());
        new Thread(futureTask).start();
        try {
            Integer integer = futureTask.get();
            System.out.println(integer);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}
//1.繼承Thread類
class MyThread1 extends Thread{
    @Override
    public void run() {
        System.out.println("MyThread1");
    }
}
//2.實現Runnable介面
class MyThread2 implements Runnable{
    @Override
    public void run() {
        System.out.println("MyThread2");
    }
}
//2.實現Runnable介面
class MyThread3 implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("MyThread3");
        return 100;
    }
}

注:本文是我在再次溫習多執行緒的過程中記下的筆記。謝謝大家的閱讀。如有不足之處,請留言,QQ:825888114!

相關文章