Java多執行緒詳解(通俗易懂)

九八十發表於2022-12-16

一、執行緒簡介

1. 什麼是程式?

電腦中會有很多單獨執行的程式,每個程式有一個獨立的程式,而程式之間是相互獨立存在的。例如圖中的微信、酷狗音樂、電腦管家等等。
process

2. 什麼是執行緒?

程式想要執行任務就需要依賴執行緒。換句話說,就是程式中的最小執行單位就是執行緒,並且一個程式中至少有一個執行緒。

那什麼又是多執行緒呢?

提到多執行緒這裡要說兩個概念,就是序列和並行,搞清楚這個,我們才能更好地理解多執行緒。

  • 序列,其實是相對於單條執行緒來執行多個任務來說的,我們就拿下載檔案來舉個例子:當我們下載多個檔案時,在序列中它是按照一定的順序去進行下載的,也就是說,必須等下載完A之後才能開始下載B,它們在時間上是不可能發生重疊的。
    serial

  • 並行:下載多個檔案,開啟多條執行緒,多個檔案同時進行下載,這裡是嚴格意義上的,在同一時刻發生的,並行在時間上是重疊的。
    parallel

瞭解完這兩個概念之後,我們再來說什麼是多執行緒,舉個例子,比如我們開啟聯想電腦管家,電腦管家本身是一個程式,也可以說就是一個程式,它裡面包括很多功能,電腦加速、安全防護、空間清理等等功能,如果對於單執行緒來說,無論我們想要電腦加速,還是空間清理,那麼必須得一件事一件事的做,做完其中一件事再做下一件事,有一個執行順序。但如果是多執行緒的話,我們可以在清理垃圾的同時進行電腦加速,還可以病毒查殺等等其他操作,這個就是在嚴格意義上的同一時刻發生的,沒有先後順序。

二、執行緒的建立

Java 提供了三種建立執行緒的方法:

  • 透過繼承 Thread 類本身。(重點)
  • 透過實現 Runnable 介面。(重點)
  • 透過 Callable 和 Future 建立執行緒。(瞭解)

1.繼承Thread類

  • 自定義執行緒類繼承Thread類
  • 重寫run()方法,編寫執行緒執行體
  • 建立執行緒物件,呼叫start()方法啟動執行緒
/**
 * @ClassName ThreadDemo
 * @Description TODO 執行緒建立的第一種方式:繼承Thread類
 * @Author ZhangHao
 * @Date 2022/12/10 11:45
 * @Version: 1.0
 */
public class ThreadDemo extends Thread{
    @Override
    public void run() {
        //新執行緒入口點
        for (int i = 0; i < 100; i++) {
            System.out.println("我在玩手機:"+i);
        }
    }
    //主執行緒
    public static void main(String[] args) {
        //建立執行緒物件
        ThreadDemo demo = new ThreadDemo();
        demo.start();//啟動執行緒
        for (int i = 0; i < 1000; i++) {
            System.out.println("我在吃飯:"+i);
        }
        //主執行緒和多執行緒並行交替執行
        //總結:執行緒開啟不一定立即執行,由cpu排程執行
    }
}

寫一個小小的案例:使用多執行緒實現網圖下載

  • 需要匯入一個commons-io-2.11.0-bin.jar (百度搜尋下載,版本不限制)
/**
 * @ClassName ImageDownload
 * @Description TODO 網圖下載
 * @Author ZhangHao
 * @Date 2022/12/10 12:55
 * @Version: 1.0
 */
public class ImageDownload extends Thread{
    private String url;//網圖下載地址
    private String name;//網圖名稱
    
    public ImageDownload(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) {
        ImageDownload d1 = new ImageDownload("https://cn.bing.com/images/search?view=detailV2&ccid=64mezA1F&id=0567ED050842B109CEFE6D7C2E235E6513915D00&thid=OIP.64mezA1F6eYavcDWrgjHQgHaEK&mediaurl=https%3a%2f%2fimages.hdqwalls.com%2fwallpapers%2fcute-kitten-4k-im.jpg&exph=2160&expw=3840&q=Cat+Wallpaper+4K&simid=608031326407324483&FORM=IRPRST&ck=5E947A96CD5B48E39B116D48F58466AB&selectedIndex=12&ajaxhist=0&ajaxserp=0", "cat1.jpg");
        ImageDownload d2 = new ImageDownload("https://cn.bing.com/images/search?view=detailV2&ccid=qXtg4Nx0&id=A80C30163A6B55D16D61F27E632239424517705F&thid=OIP.qXtg4Nx0BUoeUP53fz_HKgHaFI&mediaurl=https%3a%2f%2fimages8.alphacoders.com%2f856%2f856433.jpg&exph=2658&expw=3840&q=Cat+Wallpaper+4K&simid=608046255722156270&FORM=IRPRST&ck=986D5F99CF8474477F4A1F2DB2850C9D&selectedIndex=25&ajaxhist=0&ajaxserp=0", "cat2.jpg");
        ImageDownload d3 = new ImageDownload("https://cn.bing.com/images/search?view=detailV2&ccid=kvYsfUHA&id=6311D8D1DC87AA4B69783A97020038B03827134D&thid=OIP.kvYsfUHAAQlEVW3Z3_EEWwHaEK&mediaurl=https%3a%2f%2fwallpapershome.com%2fimages%2fpages%2fpic_h%2f19418.jpg&exph=1080&expw=1920&q=Cat+Wallpaper+4K&simid=608016886736366855&FORM=IRPRST&ck=37C2818B80D19766E7A91B5BB7A060D6&selectedIndex=159&ajaxhist=0&ajaxserp=0", "cat3.jpg");
        d1.start();
        d2.start();
        d3.start();
        //每次執行結果有可能不一樣,再次證明執行緒之間是由cpu排程執行
    }
}
//下載器
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方法出現問題!");
        }
    }
}

2. 實現Runnable介面

  • 定義MyRunnable類實現Runnable介面
  • 實現run()方法,編寫執行緒執行體
  • 建立執行緒物件,呼叫start()方法啟動執行緒
/**
 * @ClassName RunnableDemo
 * @Description TODO 執行緒建立的第二種方式:實現Runnable介面 (推薦使用)
 * @Author ZhangHao
 * @Date 2022/12/10 15:07
 * @Version: 1.0
 */
//模擬搶火車票
public class RunnableDemo implements Runnable{
    //票數
    private int ticketNums = 10;

    @Override
    public void run() {
        while (ticketNums > 0){
            try {
                //讓執行緒睡眠一會
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"-->搶到了第"+ticketNums--+"張票");
        }
    }

    public static void main(String[] args) {
        //建立runnable介面的實現類物件
        RunnableDemo demo = new RunnableDemo();
        //建立執行緒物件,透過執行緒物件開啟執行緒(使用的代理模式)
        //Thread thread = new Thread(demo,"老王");
        //thread.start();
        //簡寫:new Thread(demo).start();
        new Thread(demo,"老王").start();
        new Thread(demo,"小張").start();
        new Thread(demo,"黃牛黨").start();

        //發現問題:多個執行緒操作同一個資源時,執行緒不安全,資料紊亂。(執行緒併發)
    }
}

再來一個小小的案例:模擬《龜兔賽跑》首先得有一個賽道,兔子天生跑得快,但是兔子跑一段路就偷懶睡覺,烏龜在不停的跑,最終烏龜取得勝利!

/**
 * @ClassName Race
 * @Description TODO 模擬龜兔賽跑
 * @Author ZhangHao
 * @Date 2022/12/11 9:25
 * @Version: 1.0
 */
public class Race implements Runnable {

    private static String winner;//勝利者

    @Override
    public void run() {
        //設定賽道
        for (int i = 1; i <= 100; i++) {
            //讓兔子跑得快一點
            if (Thread.currentThread().getName().equals("兔子")) {
                i += 4;
                System.out.println(Thread.currentThread().getName() + "跑了" + i + "步");
                if (i % 4 == 0) {
                    try {
                        //模擬兔子跑一段路就睡覺
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } else {
                System.out.println(Thread.currentThread().getName() + "跑了" + i + "步");
            }
            //判斷遊戲是否結束
            boolean flag = gameOver(i);
            if (flag) {
                break;
            }
        }
    }

    //判斷遊戲是否結束
    private boolean gameOver(int steps) {
        if (winner != null) {
            return true;
        }
        {
            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();
    }
}

3. 實現Callable介面

  • 實現Callable介面,需要返回值型別
  • 重寫Call方法,需要丟擲異常
  • 建立目標物件
  • 建立執行服務:ExecutorService es = Executors.newFixedThreadPool(1);
  • 提交執行:Future r1 = es.submit(d1);
  • 獲取結果:Boolean res1 = r1.get();
  • 關閉服務:es.shutdownNow();
/**
 * @ClassName CallableDemo
 * @Description TODO 執行緒建立的第三種方式:實現Callable介面(瞭解即可)
 * @Author ZhangHao
 * @Date 2022/12/11 10:24
 * @Version: 1.0
 */
public class CallableDemo implements Callable<Boolean> {
    private String url;//網圖下載地址
    private String name;//網圖名稱

    public CallableDemo(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) throws ExecutionException, InterruptedException {
        CallableDemo d1 = new  CallableDemo("https://cn.bing.com/images/search?view=detailV2&ccid=64mezA1F&id=0567ED050842B109CEFE6D7C2E235E6513915D00&thid=OIP.64mezA1F6eYavcDWrgjHQgHaEK&mediaurl=https%3a%2f%2fimages.hdqwalls.com%2fwallpapers%2fcute-kitten-4k-im.jpg&exph=2160&expw=3840&q=Cat+Wallpaper+4K&simid=608031326407324483&FORM=IRPRST&ck=5E947A96CD5B48E39B116D48F58466AB&selectedIndex=12&ajaxhist=0&ajaxserp=0", "cat1.jpg");
        CallableDemo d2 = new  CallableDemo("https://cn.bing.com/images/search?view=detailV2&ccid=qXtg4Nx0&id=A80C30163A6B55D16D61F27E632239424517705F&thid=OIP.qXtg4Nx0BUoeUP53fz_HKgHaFI&mediaurl=https%3a%2f%2fimages8.alphacoders.com%2f856%2f856433.jpg&exph=2658&expw=3840&q=Cat+Wallpaper+4K&simid=608046255722156270&FORM=IRPRST&ck=986D5F99CF8474477F4A1F2DB2850C9D&selectedIndex=25&ajaxhist=0&ajaxserp=0", "cat2.jpg");
        CallableDemo d3 = new  CallableDemo("https://cn.bing.com/images/search?view=detailV2&ccid=kvYsfUHA&id=6311D8D1DC87AA4B69783A97020038B03827134D&thid=OIP.kvYsfUHAAQlEVW3Z3_EEWwHaEK&mediaurl=https%3a%2f%2fwallpapershome.com%2fimages%2fpages%2fpic_h%2f19418.jpg&exph=1080&expw=1920&q=Cat+Wallpaper+4K&simid=608016886736366855&FORM=IRPRST&ck=37C2818B80D19766E7A91B5BB7A060D6&selectedIndex=159&ajaxhist=0&ajaxserp=0", "cat3.jpg");
        //建立執行任務
        ExecutorService es = Executors.newFixedThreadPool(3);
        //提交執行
        Future<Boolean> r1 = es.submit(d1);
        Future<Boolean> r2 = es.submit(d2);
        Future<Boolean> r3 = es.submit(d3);
        //獲取結果
        Boolean res1 = r1.get();
        Boolean res2 = r2.get();
        Boolean res3 = r3.get();
        System.out.println(res1);//列印結果
        System.out.println(res2);
        System.out.println(res3);
        //關閉服務
        es.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類
    • 子類繼承Thread類具備多執行緒能力
    • 啟動執行緒:子類物件.start()
    • 不建議使用:避免OOP單繼承侷限性
  • 實現Runnable介面
    • 實現介面Runnable具有多執行緒能力
    • 啟動執行緒:傳入目標物件+Thread物件.start()
    • 推薦使用:避免單繼承侷限性,靈活方面,方便同一個物件被多個執行緒使用

靜態代理

代理模式在我們生活中很常見,比如我們購物,可以從生產工廠直接進行購物,但是在生活中往往不是這樣,一般都是廠家委託給超市進行銷售,而我們不直接跟廠家進行關聯,這其中就引用了靜態代理的思想,廠家相當於真實角色,超市相當於代理角色,我們則是目標角色。代理角色的作用其實就是,幫助真實角色完成一些事情,在真實角色業務的前提下,還可以增加其他的業務。AOP切面程式設計就是運用到了這一思想。

寫一個小小的案例,透過婚慶公司,來實現靜態代理。

/**
 * @ClassName StaticProxy
 * @Description TODO 靜態代理(模擬婚慶公司實現)
 * @Author ZhangHao
 * @Date 2022/12/11 11:38
 * @Version: 1.0
 */
public class StaticProxy {
    public static void main(String[] args) {
        Marry marry = new WeddingCompany(new You());
        marry.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();
        target.happyMarry();
        after();
    }
    private void before(){
        System.out.println("結婚之前:佈置婚禮現場");
    }
    private void after(){
        System.out.println("結婚之後:收尾工作");
    }
}

Thread底層就使用的靜態代理模式,原始碼分析

//Thread類實現了Runnable介面
public class Thread implements Runnable{
  //引入了真實物件
  private Runnable target;
  //代理物件中的構造器
  public Thread(Runnable target, String name) {
         init(null, target, name, 0);
  }
}

當我們開啟一個執行緒,其實就是定義了一個真實角色實現了Runnable介面,重寫了run方法。

public void TestRunnable{
    public static void main(String[] args){
      MyThread myThread = new MyThread();
      new Thread(myThread,"張三").start();
      //Thread就是代理角色,myThread就是真實角色,start()就是實現方法
    }
}
class MyThread implements Runnable{
   @Override
   public void run() {
     System.out.println("我是子執行緒,同時是真實角色");
   }
}

動態代理

前面使用到了靜態代理,代理類是自己手工實現的,自己建立了java類表示代理類,同時要代理的目標類也是確定的,如果當目標類增多時,代理類也需要成倍的增加,代理類的數量過多,當介面中的方法改變或者修改時,會影響實現類,廠家類,代理都需要修改,於是乎就有了jdk動態代理。

動態代理的好處:

  • 代理類數量減少
  • 修改介面中的方法不影響代理類
  • 實現解耦合,讓業務功能和日誌、事務和非事務功能分離

實現步驟:

  1. 建立介面,定義目標類要完成功能。
  2. 建立目標類實現介面。
  3. 建立InvocationHandler介面實現類,在invoke()方法中完成代理類的功能。
  4. 使用Proxy類的靜態方法,建立代理物件,並且將返回值轉換為介面型別。
/**
 * @ClassName DynamicProxy
 * @Description TODO 動態代理
 * @Author ZhangHao
 * @Date 2022/12/11 15:11
 * @Version: 1.0
 */
public class DynamicProxy {
    public static void main(String[] args) {
        //建立目標物件
        Marry target = new You();

        //建立InvocationHandler物件
        MyInvocationHandler handler = new MyInvocationHandler(target);

        //建立代理物件
        Marry proxy = (Marry)handler.getProxy();
        //透過代理執行方法,會呼叫handle中的invoke()方法
        proxy.happyMarry();
    }
}
//建立結婚介面
interface Marry{
    void happyMarry();
}
//目標類實現結婚介面
class You implements Marry{
    @Override
    public void happyMarry() {
        System.out.println("張三結婚了!");
    }
}
//建立工具類,即方法增強的功能
class ServiceTools{
    public static void before(){
        System.out.println("結婚之前:佈置婚禮現場");
    }
    public static void after(){
        System.out.println("結婚之後:清理結婚現場");
    }
}
//建立InvocationHandler的實現類
class MyInvocationHandler implements InvocationHandler{

    //目標物件
    private Object target;

    public MyInvocationHandler(Object target){
        this.target = target;
    }

    //透過代理物件執行方法時,會呼叫invoke()方法
    /**
     * @Param [proxy:jdk建立的代理類的例項]
     * @Param [method:目標類中被代理方法]
     * @Param [args:目標類中方法的引數]
     * @return java.lang.Object
    **/
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //增強功能
        ServiceTools.before();
        //執行目標類中的方法
        Object obj = null;
        obj = method.invoke(target,args);
        ServiceTools.after();
        return obj;
    }

    //透過Proxy類建立代理物件(自己手寫的嗷)
    /**
     * @Param [ClassLoader loader:類載入器,負責向記憶體中載入物件的,使用反射獲取物件的ClassLoader]
     * @Param [Class<?>[] interfaces: 介面, 目標物件實現的介面,也是反射獲取的。]
     * @Param [InvocationHandler h: 我們自己寫的,代理類要完成的功能。]
     * @return java.lang.Object
    **/
    public Object getProxy(){
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),this);
    }
}

總結

  1. 代理分為靜態代理和動態代理
  2. 靜態代理需要手動書寫代理類,動態代理透過Proxy.newInstance()方法生成
  3. 不管是靜態代理還是動態代理,代理與被代理者都要實現兩樣介面,本質面向介面程式設計
  4. 代理模式本質上的目的是為了在不改變原有程式碼的基礎上增強現有程式碼的功能

三、執行緒的狀態

都知道人有生老病死,執行緒也不例外。

Java中執行緒的狀態分為 6種,可以在Thread類的State列舉類檢視 。

ThreadState

  1. 新建(NEW):使用new關鍵字建立一個執行緒的時候,就進入新建狀態。

  2. 執行(RUNNABLE):Java執行緒中將就緒(ready)和執行中(running)兩種狀態統的稱為“執行”。
    2.1 就緒(ready):執行緒物件建立後,其他執行緒(比如main執行緒)呼叫了該物件的start()方法。該狀態的執行緒位於可執行執行緒池中,等待被執行緒排程選中,獲取CPU的使用權,此時處於就緒狀態。
    2.2 執行中(running):就緒狀態的執行緒在獲得CPU時間片後變為執行狀態。

  3. 阻塞(BLOCKED):阻塞狀態是指執行緒因為某些原因放棄CPU,暫時停止執行。當執行緒處於阻塞狀態時,Java虛擬機器不會給執行緒分配CPU。直到執行緒重新進入就緒狀態,它才有機會轉到執行狀態。

    • 阻塞情況又分為三種:
      • 等待阻塞:當執行緒執行wait()方法時,JVM會把該執行緒放入等待佇列(waitting queue)中。
      • 同步阻塞:當執行緒在獲取synchronized同步鎖失敗(鎖被其它執行緒所佔用)時,JVM會把該執行緒放入鎖池(lock pool)中。
      • 其他阻塞:當執行緒執行sleep()或join()方法,或者發出了 I/O 請求時,JVM會把該執行緒置為阻塞狀態。 當sleep()狀態超時、join()等待執行緒終止或者超時、或者 I/O 處理完畢時,執行緒重新轉入就緒狀態。
  4. 等待(WAITING):進入該狀態的執行緒需要等待其他執行緒做出一些特定動作(通知或中斷)。

  5. 超時等待(TIMED_WAITING):該狀態不同於WAITING,它可以在指定的時間後自行返回。

  6. 終止(TERMINATED):當執行緒的run()方法完成時,或者主執行緒的main()方法完成時,我們就認為它終止了。執行緒一旦終止了,就不能復生。

四、執行緒方法

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

1. 停止執行緒

  • 不推薦使用JDK提供的stop()、destroy()方法。【過期】
  • 建議讓執行緒自己停下來
  • 建議使用一個標誌位進行終止變數
/**
 * @ClassName TestStop
 * @Description TODO 測試停止執行緒
 * @Author ZhangHao
 * @Date 2022/12/12 20:39
 * @Version: 1.0
 */
public class TestStop implements Runnable {
    //設定一個標誌位
    private boolean flag = true;

    @Override
    public void run() {
        int i = 0;
        while (flag) {
            System.out.println("子執行緒" + i++);
        }
    }
    //設定公開的方法,轉換標誌位
    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("主執行緒:" + i);
            if (i == 700) {
                testStop.stop();
                System.out.println("執行緒停止了!");
            }
        }
        //主執行緒和子執行緒並行交替執行,當主執行緒i=700時,子執行緒停止執行,主執行緒繼續執行直到執行完成。
    }
}

2. 執行緒休眠

  • sleep(long millis):以毫秒為單位休眠
  • sleep()到達指定時間後就會進入就緒狀態
  • sleep()可以模擬網路延時,計時器等等
  • sleep()存在異常InterruptedException
  • 每個物件都有一把鎖,sleep不會釋放鎖
/**
 * @ClassName TestSleep
 * @Description TODO 執行緒休眠
 * @Author ZhangHao
 * @Date 2022/12/12 21:31
 * @Version: 1.0
 */
public class TestSleep implements Runnable{
    //票數
    private int ticketNums = 10;

    @Override
    public void run() {
        while (ticketNums > 0){
            try {
                Thread.sleep(10);//模擬網路延時,放大問題的發生性
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"-->搶到了第"+ticketNums--+"張票");
        }
    }

    public static void main(String[] args) {
        RunnableDemo demo = new RunnableDemo();
        new Thread(demo,"老王").start();
        new Thread(demo,"小張").start();
        new Thread(demo,"黃牛黨").start();
    }
}

寫一個小小的案例:使用sleep()完成倒數計時和時間播報的功能

/**
 * @ClassName TestSleep2
 * @Description TODO 倒數計時,時間播報
 * @Author ZhangHao
 * @Date 2022/12/12 21:39
 * @Version: 1.0
 */
public class TestSleep2 {
    public static void main(String[] args) {
        //tenDown();
        printNowDate();
    }

    //倒數計時
    public static void tenDown(){
        int i = 10;
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(i--);
            if (i <= 0) {
                break;
            }
        }
    }

    //時間播報
    public static void printNowDate(){
        //獲取當前時間
        Date date = new Date(System.currentTimeMillis());
        while (true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(date);
            //更新當前時間
            date = new Date(System.currentTimeMillis());
            System.out.println(format);
        }
    }
}

3. 執行緒禮讓

  • 讓正在執行的執行緒停止,從執行狀態轉換為就緒狀態,重寫競爭時間片。
  • 禮讓不一定成功,由CPU重新排程,看CPU心情!
/**
 * @ClassName TestYield
 * @Description TODO 執行緒禮讓
 * @Author ZhangHao
 * @Date 2022/12/13 12:46
 * @Version: 1.0
 */
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執行緒處於就緒狀態等待被cpu排程執行,
        //當其中有一個執行緒被cpu排程執行了,則當前這個執行緒再退回就緒狀態重新和另外一個執行緒競爭
    }
}
class MyYield implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"執行緒開始!");
        Thread.yield();//禮讓
        System.out.println(Thread.currentThread().getName()+"執行緒停止!");
    }
}

4. 執行緒插隊

  • 非常霸道的一個方法,相當於其他執行緒正在執行,相當於一個vip執行緒直接插隊執行完,其他執行緒阻塞,再執行其他的執行緒。
/**
 * @ClassName TestJoin
 * @Description TODO 執行緒插隊
 * @Author ZhangHao
 * @Date 2022/12/13 13:03
 * @Version: 1.0
 */
public class TestJoin implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("vip執行緒" + i);
        }
    }

    public static void main(String[] args) {
        TestJoin testJoin = new TestJoin();
        Thread thread = new Thread(testJoin);

        for (int i = 0; i < 500; i++) {
            if (i == 200) {
                try {
                    thread.start();
                    thread.join();//插隊執行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("主執行緒" + i);
        }
    }
}

5. 執行緒狀態

/**
 * @ClassName TestState
 * @Description TODO 執行緒狀態
 * @Author ZhangHao
 * @Date 2022/12/13 13:22
 * @Version: 1.0
 */
public class TestState {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(200);//執行緒睡眠
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("執行緒體執行完畢~");
        });

        Thread.State state = thread.getState();//觀察執行緒狀態
        System.out.println(state);//NEW:沒有呼叫start()方法之前都是new

        thread.start();
        state = thread.getState();
        System.out.println(state);//RUNNABLE:進入執行狀態

        //只要執行緒還沒有死亡,就列印執行緒狀態
        while (state != Thread.State.TERMINATED){
            try {
                Thread.sleep(100);//100毫秒列印一次狀態
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            state = thread.getState();//更新狀態
            System.out.println(state);
        }
    }
    /*
        NEW
        RUNNABLE
        TIMED_WAITING
        TIMED_WAITING
        TIMED_WAITING
        TIMED_WAITING
        TIMED_WAITING
        TIMED_WAITING
        TIMED_WAITING
        TIMED_WAITING
        TIMED_WAITING
        執行緒體執行完畢~
        TERMINATED
     */
}

6. 執行緒的優先順序

  • Java提供一個執行緒排程器來監控程式中啟動後進入就緒狀態的所有執行緒,執行緒排程器按照優先順序決定應該排程哪個執行緒來執行。

  • 執行緒的優先順序用數字表示,範圍從1~10,不在這個範圍內的都會報出異常.

    • Thread.MIN_PRIORITY = 1;
    • Thread.MAX_PRIORITY = 10;
    • Thread.NORM_PRIORITY = 5;
  • 使用以下方式改變和獲取優先順序

    • setPriority(int xxx)
    • getPriority()
  • 優先順序的設定建議在start()之前

/**
 * @ClassName TestPriority
 * @Description TODO 執行緒的優先順序
 * @Author ZhangHao
 * @Date 2022/12/13 13:46
 * @Version: 1.0
 */
public class TestPriority {
    public static void main(String[] args) {
        //主執行緒預設優先順序:5
        System.out.println(Thread.currentThread().getName()+"---->"+Thread.currentThread().getPriority());

        MyPriority myPriority = new MyPriority();
        Thread t1 = new Thread(myPriority);
        t1.start();//預設優先順序是5

        Thread t2 = new Thread(myPriority);
        t2.setPriority(3);
        t2.start();

        Thread t3 = new Thread(myPriority);
        t3.setPriority(8);
        t3.start();

        Thread t4 = new Thread(myPriority);
        t4.setPriority(Thread.MAX_PRIORITY);//最大優先順序10
        t4.start();

        Thread t5 = new Thread(myPriority);
        //t5.setPriority(-1);//Exception in thread "main" java.lang.IllegalArgumentException
        //t5.start();

        //優先順序越大代表被排程的可能性越高,優先順序低不代表不會被排程,還是看CPU心情
    }
}
class MyPriority implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"---->"+Thread.currentThread().getPriority());
    }
}

7. 守護執行緒

  • 執行緒分為使用者執行緒守護執行緒
  • 虛擬機器必須確保使用者執行緒執行完畢,如main()
  • 虛擬機器不用等待守護執行緒執行完畢,如gc()
  • 如:後臺記錄操作日誌,監控記憶體,垃圾回收等等
/**
 * @ClassName TestDaemon
 * @Description TODO 守護執行緒
 * @Author ZhangHao
 * @Date 2022/12/13 14:09
 * @Version: 1.0
 */
public class TestDaemon {
    public static void main(String[] args) {
        God god = new God();
        You you = new You();

        Thread thread = new Thread(god);
        thread.setDaemon(true);//預設是false使用者執行緒,正常的執行緒都是使用者執行緒
        thread.start();

        new Thread(you).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 < 36500; i++) {
            System.out.println("開心的活著!"+i);
        }
        System.out.println("------goodbye!------");
    }
}

五、執行緒同步

併發

同一個物件被多個執行緒同時操作,併發程式設計又叫多執行緒程式設計。生活中的例子很常見,比如過年了學生都需要在手機上搶火車票,幾萬個人同時去搶那10張票,最終只有10個幸運兒搶到,手速慢的學生是不是就沒有搶到呀。

  • 併發針對單核CPU處理器,它是多個執行緒被一個CPU輪流非常快速切換執行的,邏輯上是同步執行。

並行

同一時刻多個任務(程式or執行緒)同時執行,真正意義上做到了同時執行,但是這種情況往往只體現在多核CPU,單核CPU是做不到同時執行多個任務的,多核CPU內部整合了多個計算機核心(Core),每個核心相當於一個簡單的CPU,多核CPU中的每個核心都可以獨立執行一個任務,並且多個核心之間互不干擾,在不同核心上執行的多個任務,稱為並行。

  • 並行針對多核CPU處理器,它是在不同核心執行的多個任務完成的,物理上是同步執行。

序列

多個任務按順序依次執行,就比如小學在學校上廁所,小學的學校一般都是公共的廁所,而且是固定的坑位,大家按照提前排好的次序依次進行上廁所,也就是多個任務之間一個一個按順序的執行。

執行緒同步

現實生活中,我們會遇到“同一個資源,多個人都想使用”的問題,比如,食堂排隊打飯,每個人都想快速吃到飯,然後幾萬個學生就一擁而上,全部擠在打飯的視窗,最後飯不僅沒吃到,還捱了一頓打,這也就是併發問題引起的,所以我們需要一種解決方案,最天然的解決辦法就是,排隊一個個來。排隊在程式設計中叫:佇列。這種解決辦法就叫執行緒同步。

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

由於同一程式的多個執行緒共享同一塊儲存空間,會出現衝突問題,所以為了保證安全性,還加入了機制。synchronized關鍵字,當一個執行緒獲得物件之後需要上鎖,獨佔著資源,其他執行緒必須等待,等當前執行緒使用完釋放鎖即可。解決了執行緒安全的問題,同樣也帶來了一些問題:

  • 一個執行緒拿到鎖之後,其他需要這把鎖的執行緒掛起。

  • 在多執行緒競爭下,加鎖和釋放鎖會導致頻繁上下切換帶來排程延遲和效能問題。

  • 如果優先順序高的執行緒等待優先順序低的執行緒釋放鎖,會導致優先順序倒置,引發效能問題。

要想保證安全,就一定會失去效能,要想保證效能,就一定會失去安全。魚和熊掌不可兼得的道理。

執行緒同步:佇列 + 鎖

用三個小小的案例演示併發引起的問題:

/**
 * @ClassName Ticket
 * @Description TODO 模擬買票
 * @Author ZhangHao
 * @Date 2022/12/14 10:40
 * @Version: 1.0
 */
public class Ticket {
    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();

        new Thread(buyTicket,"張三").start();
        new Thread(buyTicket,"李四").start();
        new Thread(buyTicket,"王五").start();
        //多執行緒同時搶票,加入延遲之後,會出現買到重複票和負數票。
    }
}
class BuyTicket implements Runnable{

    private int ticketNum = 10;//票數
    private boolean flag = true;//設定標誌位

    @Override
    public void run() {
        while(flag){
            try {
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    //買票
    private void buy() throws InterruptedException {
        if(ticketNum<=0){
            flag = false;
            return;
        }
        Thread.sleep(100);//模擬延時,放大問題的發生性
        System.out.println(Thread.currentThread().getName()+"搶到了第"+ticketNum--+"張票");
    }
}
李四搶到了第10張票
張三搶到了第10張票
王五搶到了第9張票
王五搶到了第8張票
張三搶到了第8張票
李四搶到了第8張票
張三搶到了第7張票
王五搶到了第7張票
李四搶到了第7張票
張三搶到了第6張票
王五搶到了第6張票
李四搶到了第6張票
張三搶到了第4張票
李四搶到了第5張票
王五搶到了第5張票
王五搶到了第3張票
李四搶到了第2張票
張三搶到了第1張票
李四搶到了第0張票
王五搶到了第-1張票

Process finished with exit code 0
/**
 * @ClassName Bank
 * @Description TODO 銀行取錢
 * @Author ZhangHao
 * @Date 2022/12/14 11:05
 * @Version: 1.0
 */
public class Bank {
    public static void main(String[] args) {
        Card card = new Card(200);
        new MyThread(card,100,"老婆").start();
        new MyThread(card,150,"我").start();
        //出現將卡的餘額取成負數
    }
}

//銀行卡
class Card {
    public int money;//餘額

    public Card(int money) {
        this.money = money;
    }
}

//取錢
class MyThread extends Thread {

    //卡號
    private Card card;
    //要取的錢
    private int takeMoney;
    //手裡的錢
    private int nowMoney;

    public MyThread(Card card,int takeMoney,String name){
        super(name);
        this.card = card;
        this.takeMoney = takeMoney;
    }

    @Override
    public void run() {
        if (card.money - takeMoney < 0) {
            System.out.println(Thread.currentThread().getName() + "--->餘額不足");
            return;
        }

        try {
            Thread.sleep(1000);//放大問題的發生性
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //取錢
        card.money = card.money - takeMoney;
        //手裡的錢
        nowMoney += takeMoney;

        //this.getName() = Thread.currentThread().getName()
        //因為本類繼承了Thread類可以直接使用其方法
        System.out.println(Thread.currentThread().getName() + "取了" + takeMoney + "w,手裡還有" + nowMoney + "w,銀行卡餘額還剩" + card.money);
    }
}
我取了150w,手裡還有150w,銀行卡餘額還剩-50
老婆取了100w,手裡還有100w,銀行卡餘額還剩-50
/**
 * @ClassName UnsafeList
 * @Description TODO 多執行緒不安全的集合
 * @Author ZhangHao
 * @Date 2022/12/14 11:20
 * @Version: 1.0
 */
public class UnsafeList {
    public static void main(String[] args) throws InterruptedException {
        List<String> strList = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                strList.add(Thread.currentThread().getName());
                System.out.println(strList);
            }).start();
        }
        Thread.sleep(1000);
        System.out.println("集合大小:"+strList.size());
    }
}
//ConcurrentModificationException異常(併發修改異常)
java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at java.util.AbstractCollection.toString(AbstractCollection.java:461)
	at java.lang.String.valueOf(String.java:2994)
	at java.io.PrintStream.println(PrintStream.java:821)
	at com.hnguigu.demo06.UnsafeList.lambda$main$0(UnsafeList.java:19)
	at java.lang.Thread.run(Thread.java:748)
集合大小:9997

Process finished with exit code 0

1. 同步方法

public synchronized void method(int args) {}

由於我們可以透過private關鍵字來保證資料物件只被封裝的方法訪問(get/set),所以我們只需要針對方法提供一套機制,這套機制就是synchronized關鍵字,包括兩種用法synchronized方法和synchronized塊。

  • synchronized方法:共享的資源是透過方法來實現的。
  • synchronized塊:共享的資源是一個物件。

同步方法中的同步監視器就是this,這個物件的本身。

synchronized關鍵字是一個修飾符,直接加入在方法返回值前面就可以實現同步。

同步方法的弊端:

  • 方法裡面需要修改的內容才需要鎖,鎖得太多,浪費資源

2. 同步塊

同步塊:synchronized(obj){}

  • obj稱為同步監視器

    • obj可以是任何物件,但是推薦使用共享資源作為同步監視器
  • 同步監視器的執行流程

    • 第一個執行緒訪問:鎖定同步監視器,執行程式碼
    • 第二個執行緒訪問:發現同步監視器被鎖,無法訪問
    • 第一個執行緒訪問完畢,解鎖同步監視器
    • 第二個執行緒訪問:發現同步監視器沒有鎖,執行程式碼

使用執行緒同步解決併發帶來的問題

/**
 * @ClassName Ticket
 * @Description TODO 模擬買票
 * @Author ZhangHao
 * @Date 2022/12/14 10:40
 * @Version: 1.0
 */
public class Ticket {
    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();

        new Thread(buyTicket,"張三").start();
        new Thread(buyTicket,"李四").start();
        new Thread(buyTicket,"王五").start();
    }
}
class BuyTicket implements Runnable{

    private int ticketNum = 10;//票數
    private boolean flag = true;//設定標誌位

    @Override
    public void run() {
        while(flag){
            try {
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    //買票
    //加入了synchronized關鍵字就是同步方法,鎖的物件是this
    private synchronized void buy() throws InterruptedException {
        if(ticketNum<=0){
            flag = false;
            return;
        }
        Thread.sleep(100);//模擬延時,放大問題的發生性
        System.out.println(Thread.currentThread().getName()+"搶到了第"+ticketNum--+"張票");
    }
}
/**
 * @ClassName Bank
 * @Description TODO 銀行取錢
 * @Author ZhangHao
 * @Date 2022/12/14 11:05
 * @Version: 1.0
 */
public class Bank {
    public static void main(String[] args) {
        Card card = new Card(200);
        new MyThread(card,100,"老婆").start();
        new MyThread(card,150,"我").start();
    }
}

//銀行卡
class Card {
    public int money;//餘額

    public Card(int money) {
        this.money = money;
    }
}

//取錢
class MyThread extends Thread {
    //卡號
    private Card card;
    //要取的錢
    private int takeMoney;
    //手裡的錢
    private int nowMoney = 0;

    public MyThread(Card card,int takeMoney,String name){
        super(name);
        this.card = card;
        this.takeMoney = takeMoney;
    }

    @Override
    public void run() {
        //如果在這裡加上synchronized關鍵字來修飾這個方法,鎖的是this也就是MyThread,而真正操作的物件是Card,所以需要使用同步塊實現
        //鎖的是需要變化的量,需要增刪改的物件
        synchronized (card){
            if (card.money - takeMoney < 0) {
                System.out.println(Thread.currentThread().getName() + "--->餘額不足");
                return;
            }
            try {
                Thread.sleep(1000);//放大問題的發生性
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //取錢
            card.money = card.money - takeMoney;
            //手裡的錢
            nowMoney += takeMoney;

            //this.getName() = Thread.currentThread().getName()
            //因為本類繼承了Thread類可以直接使用其方法
            System.out.println(Thread.currentThread().getName() + "取了" + takeMoney + "w,手裡還有" + nowMoney + "w,銀行卡餘額還剩" + card.money);
        }
    }
}
/**
 * @ClassName UnsafeList
 * @Description TODO 多執行緒不安全的集合
 * @Author ZhangHao
 * @Date 2022/12/14 11:20
 * @Version: 1.0
 */
public class UnsafeList {
    public static void main(String[] args) throws InterruptedException {
        List<String> strList = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                //鎖住需要變化的物件,這裡就是list
                synchronized(strList){
                    strList.add(Thread.currentThread().getName());
                    System.out.println(strList);
                }
            }).start();
        }
        Thread.sleep(1000);
        System.out.println("集合大小:"+strList.size());
    }
}

補充:juc(java.util.concurrent)包下的執行緒安全的集合

/**
 * @ClassName CopyOnWriteArrayList
 * @Description TODO 測試JUC併發程式設計下執行緒安全的ArrayList集合
 * @Author ZhangHao
 * @Date 2022/12/14 13:19
 * @Version: 1.0
 */
public class TestCopyOnWriteArrayList {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        //這裡加入sleep()方法是防止在子執行緒還沒完成之前,就列印了集合大小
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("集合大小:"+list.size());
    }
}

3. 死鎖

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

/**
 * @ClassName DeadLock
 * @Description TODO 死鎖
 * @Author ZhangHao
 * @Date 2022/12/14 16:15
 * @Version: 1.0
 */
public class DeadLock {
    public static void main(String[] args) {
        Makeup makeup1 = new Makeup(0, "灰姑娘");
        Makeup makeup2 = new Makeup(1, "白雪公主");
        
        makeup1.start();
        makeup2.start();
        //最終結果:程式僵持執行著
    }
}
//口紅
class Lipstick{
    String name = "迪奧口紅";
}
//鏡子
class Mirror{
    String name = "魔鏡";
}
//化妝
class Makeup extends Thread{

    //使用static保證只有一份
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    int choice;//選擇
    String girlName;//選擇化妝的人

    Makeup(int choice,String girlName){
        this.choice = choice;
        this.girlName = girlName;
    }

    @Override
    public void run() {
        try {
            makeup();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //化妝
    private void makeup() throws InterruptedException {
        if(choice==0){
            synchronized (lipstick){//獲得口紅的鎖
                System.out.println(this.girlName + "--->獲得" + lipstick.name);
                Thread.sleep(1000);
                synchronized (mirror){//一秒鐘之後想要鏡子的鎖
                    System.out.println(this.girlName + "--->獲得" + mirror.name);
                }
            }
        }else{
            synchronized (mirror){//獲得鏡子的鎖
                System.out.println(this.girlName + "--->獲得" + mirror.name);
                Thread.sleep(2000);
                synchronized (lipstick){//兩秒鐘之後想要口紅的鎖
                    System.out.println(this.girlName + "--->獲得" + lipstick.name);
                }
            }
        }

    }
}

灰姑娘拿著口紅的鎖不釋放,隨後一秒鐘後又要魔鏡的鎖,白雪公主拿著魔鏡的鎖不釋放,兩秒鐘後又要口紅的鎖,雙方都不釋放已經使用完了的鎖資源,僵持形成死鎖。
解決辦法就是用完鎖就釋放。

/**
 * @ClassName DeadLock
 * @Description TODO 死鎖
 * @Author ZhangHao
 * @Date 2022/12/14 16:15
 * @Version: 1.0
 */
public class DeadLock {
    public static void main(String[] args) {
        Makeup makeup1 = new Makeup(0, "灰姑娘");
        Makeup makeup2 = new Makeup(1, "白雪公主");

        makeup1.start();
        makeup2.start();
    }
}
//口紅
class Lipstick{
    String name = "迪奧口紅";
}
//鏡子
class Mirror{
    String name = "魔鏡";
}
//化妝
class Makeup extends Thread{

    //使用static保證只有一份
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    int choice;//選擇
    String girlName;//選擇化妝的人

    Makeup(int choice,String girlName){
        this.choice = choice;
        this.girlName = girlName;
    }

    @Override
    public void run() {
        try {
            makeup();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //化妝
    private void makeup() throws InterruptedException {
        if(choice==0){
            synchronized (lipstick){//獲得口紅的鎖
                System.out.println(this.girlName + "--->獲得" + lipstick.name);
                Thread.sleep(1000);
            }
            synchronized (mirror){//一秒鐘之後想要鏡子的鎖
                System.out.println(this.girlName + "--->獲得" + mirror.name);
            }
        }else{
            synchronized (mirror){//獲得鏡子的鎖
                System.out.println(this.girlName + "--->獲得" + mirror.name);
                Thread.sleep(2000);
            }
            synchronized (lipstick){//兩秒鐘之後想要口紅的鎖
                System.out.println(this.girlName + "--->獲得" + lipstick.name);
            }
        }
    }
}

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

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

上面就是形成死鎖的必要條件,只需要解決其中任意一個或者多個條件就可以避免死鎖的發生。

4. Lock鎖

從JDK5.0開始,Java提供了更強大的執行緒同步機制,透過顯示定義同步鎖物件來實現同步。同步鎖使用Lock物件充當。
java.util.concurrent.locks.Lock介面是控制多個執行緒對共享資源進行訪問的工具。鎖提供了對共享資源的獨佔訪問,每次只能有一個執行緒對Lock物件加鎖,執行緒開始訪問共享資源之前應先獲得Lock物件。
ReentrantLock 類實現了 Lock ,它擁有與 synchronized 相同的併發性和記憶體語義,在實現執行緒安全的控制中,比較常用的是ReentrantLock,可以顯式加鎖、釋放鎖。

/**
 * @ClassName TestLock
 * @Description TODO Lock鎖
 * @Author ZhangHao
 * @Date 2022/12/14 16:47
 * @Version: 1.0
 */
public class TestLock {
    public static void main(String[] args) {
        MyLock myLock = new MyLock();

        new Thread(myLock, "張三").start();
        new Thread(myLock, "老王").start();
        new Thread(myLock, "黃牛").start();
    }
}

class MyLock 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鎖的區別:

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

六、執行緒通訊

應用場景:生產者和消費者的問題。
假設有一個倉庫只能放一件產品,生產者將生產出來的產品放到倉庫,消費者從倉庫取走商品消費。如果倉庫中沒有產品,則消費者等待生產者生產商品,有商品則通知消費則取走商品。
這是一個執行緒同步的問題,生產者和消費者共享同一個資源,並且生產者和消費者之間互相依賴,互成條件。

  • 對於生產者,沒有生產產品之前,需要通知消費者等待,而生產了產品之後需要通知消費者取走消費。
  • 對於消費者,在消費完之後,要通知生產者繼續生產。
  • 在生產者消費者問題中,僅有synchronized是不夠的
    • synchronized可阻止併發更新同一個共享資源,實現了同步。
    • synchronized不能實現不同執行緒之間訊息傳遞(通訊)。

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

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

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

解決辦法:

1. 管城法

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

/**
 * @ClassName TestPC
 * @Description TODO 生產者和消費者模型
 * @Author ZhangHao
 * @Date 2022/12/14 20:28
 * @Version: 1.0
 */
public class TestPC {
    public static void main(String[] args) {
        SynContainer container = new SynContainer();

        new Producer(container).start();
        new Consumer(container).start();
    }
}

//定義兩個執行緒:生產者和消費者
class Producer extends Thread {

    //生產者需要將生產的雞丟入倉庫
    SynContainer container;

    Producer(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 Consumer extends Thread {

    //消費者需要從倉庫裡面取雞
    SynContainer container;

    Consumer(SynContainer container) {
        this.container = container;
    }

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

//商品
class Chicken {
    int count;//數量

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

//緩衝區
class SynContainer {
    //需要一個容器裝載,假如一個倉庫只能裝10只雞
    Chicken[] chickens = new Chicken[10];
    //計數
    int count = 0;

    //生產者放入容器的方法
    public synchronized void push(Chicken chicken) {
        //如果倉庫裝滿了雞,就通知消費者消費,生產者等待。
        while (count == chickens.length - 1) {
            try {
                this.wait();//生產者等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //生產者生產商品丟入倉庫
        chickens[count] = chicken;
        count++;
        //通知消費者消費,喚醒消費者。
        this.notifyAll();

    }

    //消費者消費產品的方法
    public synchronized Chicken pop() {
        //倉庫裡面沒有雞,就通知生產者生產,消費者等待
        while (count == 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //消費產品
        count--;
        Chicken chicken = chickens[count];
        //通知生產者生產,喚醒生產者。
        this.notifyAll();
        return chicken;
    }
}

注意:這裡如果使用if判斷邏輯上是完全沒問題的,但是這裡會出現一個虛假喚醒,通俗的說如果某個執行緒處於wait()狀態,如果用if判斷的話,喚醒後執行緒會直接從wait方法後執行,不會重新進行if判斷,但如果使用while來作為判斷語句的話,也會從wait之後的程式碼執行,但是喚醒後會重新判斷迴圈條件。

2. 訊號燈法

透過設定標誌位來完成執行緒之間的通訊

/**
 * @ClassName TestTV
 * @Description TODO 訊號燈法
 * @Author ZhangHao
 * @Date 2022/12/14 21:33
 * @Version: 1.0
 */
public class TestTV {
    public static void main(String[] args) {
        Tv tv = new Tv();

        new Thread(new Player(tv)).start();
        new Thread(new Watcher(tv)).start();
    }
}

//定義兩個執行緒:生產者和消費者
//表演者
class Player implements Runnable {

    Tv tv;

    Player(Tv tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 1; i < 20; i++) {
            if (i%2==0){
                tv.play("光頭強");
            }else{
                tv.play("喜洋洋");
            }
        }
    }
}

//觀眾
class Watcher implements Runnable {

    Tv tv;

    Watcher(Tv tv) {
        this.tv = tv;
    }

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

//表演
class Tv {
    //演員表演,觀眾觀看
    String program;//節目
    boolean flag = true;//設定標誌位,預設是沒有節目觀看

    //演員表演
    public synchronized void play(String program) {
        //如果有節目觀看,演員就等待觀眾觀看
        if (!flag) {
            try {
                this.wait();//等待觀看
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("演員表演了:" + program);
        this.notifyAll();//喚醒觀眾
        this.program = program;
        this.flag = !this.flag;
    }

    //觀眾觀看
    public synchronized void watch() {
        //如果沒有節目觀看,就通知演員表演
        if (flag) {
            try {
                this.wait();//等待演出
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("觀眾觀看了:" + program);
        this.notifyAll();//喚醒演員表演
        this.flag = !this.flag;
    }
}

七、執行緒池

經常建立和銷燬、使用量特別大的資源,比如併發情況下的執行緒,對效能影響很大。

思路:提前建立好多個執行緒,放入執行緒池中,使用時直接獲取,使用完放回池中。

可以避免頻繁建立銷燬、實現重複利用。類似生活中的公共交通工具。

  • 好處:
    • 提高響應速度(減少了建立新執行緒的時間)
    • 降低資源消耗(重複利用執行緒池中執行緒,不需要每次都建立)
    • 便於執行緒管理(…)
      • corePoolSize:核心池的大小
      • maximumPoolSize:最大執行緒數
      • keepAliveTime:執行緒沒有任務時最多保持多長時間後會終止

JDK 5.0起提供了執行緒池相關API:ExecutorService 和 Executors

  • ExecutorService:真正的執行緒池介面。常見子類ThreadPoolExecutor
    • void execute(Runnable command) :執行任務/命令,沒有返回值,一般用來執行Runnable
    • Future submit(Callable task):執行任務,有返回值,一般又來執行Callable
    • void shutdown() :關閉連線池

Executors:工具類、執行緒池的工廠類,用於建立並返回不同型別的執行緒池

/**
 * @ClassName TestPool
 * @Description TODO 執行緒池
 * @Author ZhangHao
 * @Date 2022/12/14 21:58
 * @Version: 1.0
 */
public class TestPool {
    public static void main(String[] args) {
        //建立執行緒池,引數是執行緒池的大小,決定了能裝多少個執行緒
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        
        executorService.execute(new ThreadPool());
        executorService.execute(new ThreadPool());
        executorService.execute(new ThreadPool());
        executorService.execute(new ThreadPool());
        executorService.execute(new ThreadPool());
        
        //關閉連線
        executorService.shutdown();
    }
}
class ThreadPool implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

完結撒花!

/**
 * @ClassName TestCallable
 * @Description TODO 補充Callable啟動執行緒
 * @Author ZhangHao
 * @Date 2022/12/14 22:28
 * @Version: 1.0
 */
public class TestCallable  {
    public static void main(String[] args){
        MyCallable callable = new MyCallable();
        //Runnable實現類
        FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
        new Thread(futureTask).start();

        //獲取callable返回值
        try {
            Integer integer = futureTask.get();
            System.out.println(integer);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}
class MyCallable implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        System.out.println("完結撒花!");
        return 100;
    }
}

好文要頂!

相關文章