Java多線程詳解(通俗易懂)

来源:https://www.cnblogs.com/ZhangHao-Study/archive/2022/12/16/16971276.html
-Advertisement-
Play Games

一、線程簡介 1. 什麼是進程? 電腦中會有很多單獨運行的程式,每個程式有一個獨立的進程,而進程之間是相互獨立存在的。例如圖中的微信、酷狗音樂、電腦管家等等。 2. 什麼是線程? 進程想要執行任務就需要依賴線程。換句話說,就是進程中的最小執行單位就是線程,並且一個進程中至少有一個線程。 那什麼又是多 ...


一、線程簡介

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
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文介紹如何使用 Pandas Profiling 的比較報告功能,分析兩個數據集的分佈差異,完成數據探索分析 (EDA) 的完整流程,為後續分析做準備。 ...
  • 作者:耿宗傑 前言 關於pprof的文章在網上已是汗牛充棟,卻是千篇一律的命令介紹,鮮有真正實操的,本文將參考Go社區資料,結合自己的經驗,實戰Go程式的性能分析與優化過程。 優化思路 首先說一下性能優化的一般思路。系統性能的分析優化,一定是從大到小的步驟來進行的,即從業務架構的優化,到系統架構的優 ...
  • 前言 網易雲的Vip音樂下載下來,格式不是mp3/flac這種通用的音樂格式,而是經過加密的ncm文件。只有用網易雲的音樂App才能夠打開。於是想到可不可以把.ncm文件轉換成mp3或者flac文件,上google查了一下,發現有不少人已經做了這件事,但沒有發現C語言版本的,就想著寫一個純C語言版本 ...
  • pandas 為什麼學習pandas numpy已經可以幫助我們進行數據的處理了,那麼學習pandas的目的是什麼呢? numpy能夠幫助我們處理的是數值型的數據,當然在數據分析中除了數值型的數據還有好多其他類型的數據(字元串,時間序列),那麼pandas就可以幫我們很好的處理除了數值型的其他數據! ...
  • 大家好,咱們前面通過十篇的文章介紹了docker的基礎篇,從本篇開始,咱們的《docker學習系列》將要進入到高級篇階段(基礎篇大家可以查看之前發佈的文章)。 咱們先來介紹:docker複雜方式安裝軟體。通過按照mysql\redis兩個案例來講解 Docker複雜安裝說明,兩個案例: 1:安裝my ...
  • 1 線程安全定義 含糊的定義:如果一個對象可以安全地被多個線程同時使用,那它就是線程安全的 嚴謹的定義: 當多個線程同時訪問一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步,或者在調用方進行任何其他的協調操作,調用這個對象的行為都可以獲得正確的結果,那就稱這個對 ...
  • 大家好,我是棧長,Nacos 2.2.0 正式發佈了! Nacos 2.2.0 還真是一個比較重要的版本,因為它涉及了太多重大更新,今天棧長給大家來解讀下。 Nacos 2.2.0 重大更新 1、刪除冗餘代碼 Nacos 2.2.0 刪除了 Nacos 1.x 版本中 Naming 和雙寫相關的舊冗 ...
  • 在作者之前的 十二條後端開發經驗分享,純乾貨 文章中介紹的 優雅得Springboot + mybatis配置多數據源方式 里有很多小伙伴在評論區留言詢問多個數據源同時在一個方法中使用時,事務是否會正常有效,這裡作者 理論 + 實踐 給大家解答一波,老規矩,附作者github地址: https:// ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...