主頁 > 後端開發 > Java多執行緒詳解(通俗易懂)

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

2022-12-17 06:33:00 後端開發

一、執行緒簡介

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;
    }
}

好文要頂!

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/540115.html

標籤:其他

上一篇:串口接收模塊——verilog實作

下一篇:多資料源事務處理-涉及分布式事務

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more