1. 程式人生 > 實用技巧 >Java基礎(12)— 多執行緒

Java基礎(12)— 多執行緒

多執行緒

程序Process與執行緒Thread

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

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

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

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

核心概念

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

執行緒建立的三種方式

Thread 類

  • 自定義執行緒類繼承Thread類

  • 重寫run(),編寫執行緒執行體

  • 建立執行緒物件,呼叫start()啟動執行緒

    • 案例練習
    /*
    練習Thread:
        實現多執行緒同步下載圖片
     */
    public class ThreadDownload extends Thread{//Thread類是實現了 Runnable 介面的
        private String url; //網路圖片地址
        private String name;//儲存的檔名
        //構造器
        public ThreadDownload(String url,String name){
            this.url = url;
            this.name = name;
        }
        //下載圖片執行緒的執行體
        @Override
        public void run() {
            WebDownload wd = new WebDownload();
            wd.downloader(url,name);
            System.out.println("下載了檔名為:"+name);
        }
        public static void main(String[] args) {
            //建立執行緒
            ThreadDownload td1 = new ThreadDownload("https://images.cnblogs.com/cnblogs_com/liuzd-2020/1802878/o_200709094656preparatory002.jpg","1.馮諾依·曼.jpg");
            ThreadDownload td2 = new ThreadDownload("https://images.cnblogs.com/cnblogs_com/liuzd-2020/1802878/o_200713134132introduction006.jpg","2.jvm.jpg");
            ThreadDownload td3 = new ThreadDownload("https://images.cnblogs.com/cnblogs_com/liuzd-2020/1802878/o_200716093402basicGrammar002.jpg","3.關鍵字.jpg");
            //啟動執行緒
            td1.start();
            td2.start();
            td3.start();
        }
    }
    //下載器
    class WebDownload{
        //下載方法
        public void downloader(String url,String name){
            try {
                //commons-io-2.6.jar中的工具類
                FileUtils.copyURLToFile(new URL(url),new File(name));
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("IO異常,downloader()出現問題");
            }
        }
    }
    

Runnable 介面

  • 定義MyRunnable類實現Runnable介面

  • 實現run(),編寫執行緒執行體

  • 建立執行緒物件,呼叫start()方法啟動執行緒

  • 小結對比

    繼承Thread類 實現Runnable介面
    1 子類具有多執行緒能力 實現類具有多執行緒能力
    2 啟動:子類物件.start() 啟動:new Thread(傳入目標物件).start
    3 不建議使用,避免OOP單執行緒侷限性 推薦使用,方便同一個物件被多個執行緒使用
    • 案例練習:購票
    //多執行緒同時操作同一個都物件
    //買票
    public class RailwayTicket implements Runnable{
        int ticketNum = 10;//票數
        @Override
        public void run() {
            while(true){
                if(ticketNum <= 0){
                    break;
                }
                System.out.println(Thread.currentThread().getName()+"拿到了第:"+ticketNum--+"張票");
            }
        }
        public static void main(String[] args) {
            RailwayTicket rt = new RailwayTicket();
    
            new Thread(rt,"小明").start();
            new Thread(rt,"老師").start();
            new Thread(rt,"黃牛黨").start();
        }
    }
    /*
    執行結果:
    	黃牛黨拿到了第:10張票
        黃牛黨拿到了第:8張票
        小明拿到了第:9張票
        小明拿到了第:6張票
        小明拿到了第:5張票
        小明拿到了第:4張票
        小明拿到了第:3張票
        小明拿到了第:2張票
        老師拿到了第:10張票
        小明拿到了第:1張票
        黃牛黨拿到了第:7張票
    發現問題:(初始併發問題)
    	多個執行緒操作同一個資源的情況下,執行緒不安全,資料紊亂。
    */
    
    • 案例練習:龜兔賽跑
    public class Race implements Runnable{
        private static String winner;//勝利者
        @Override
        public void run() {
            for (int i = 0; i <= 100; i++) {
                //模擬兔子睡覺
                if(Thread.currentThread().getName().equals("兔子") && i%10 == 0){
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //判斷比賽是否結束
                boolean flag = gameOver(i);
    
                if(flag){
                    break;
                }
                System.out.println(Thread.currentThread().getName()+"跑了"+i+"步");
            }
        }
        //判斷是否完成比賽
        private boolean gameOver(int steps){
            //判斷是否由勝利者
            if(winner != null){//已經存在勝利者了
                return true;
            }{
                if(steps >= 100){
                    winner = Thread.currentThread().getName();
                    System.out.println("winner id "+ winner);
                }
            }
            return false;
        }
        public static void main(String[] args) {
            Race r = new Race();
            new Thread(r,"兔子").start();
            new Thread(r,"烏龜").start();
        }
    }
    

Callable 介面

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

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

  • 建立目標物件

  • 建立執行服務:ExecutorService es=Executor.newFixedThreadPool(1);

  • 提交執行:Futureresult1=ser.submit(t1);

  • 獲取結果:boolean r1=result.get()

  • 關閉服務:ser.shutdownNow();

    • 案例下載圖片
    public class CallableDownload implements Callable<Boolean> {
        private String url; //網路圖片地址
        private String name;//儲存的檔名
        //構造器
        public CallableDownload(String url,String name){
            this.url = url;
            this.name = name;
        }
        //下載圖片執行緒的執行體
        @Override
        public Boolean call() throws Exception {
            WebDownload wd = new WebDownload();
            wd.downloader(url,name);
            System.out.println("下載了檔名為:"+name);
            return true;
        }
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            //建立執行緒
            CallableDownload td1 = new CallableDownload("https://images.cnblogs.com/cnblogs_com/liuzd-2020/1802878/o_200709094656preparatory002.jpg","1.馮諾依·曼.jpg");
            CallableDownload td2 = new CallableDownload("https://images.cnblogs.com/cnblogs_com/liuzd-2020/1802878/o_200713134132introduction006.jpg","2.jvm.jpg");
            CallableDownload td3 = new CallableDownload("https://images.cnblogs.com/cnblogs_com/liuzd-2020/1802878/o_200716093402basicGrammar002.jpg","3.關鍵字.jpg");
            //建立執行服務
            ExecutorService service = Executors.newFixedThreadPool(3);
            //提交執行
            Future<Boolean> submit1 = service.submit(td1);
            Future<Boolean> submit2 = service.submit(td2);
            Future<Boolean> submit3 = service.submit(td3);
            //獲取結果
            Boolean b1 = submit1.get();
            Boolean b2 = submit2.get();
            Boolean b3 = submit3.get();
            //關閉服務
            service.shutdownNow();
        }
    }
    //下載器
    class WebDownload{
        //下載方法
        public void downloader(String url,String name){
            try {
                //commons-io-2.6.jar中的工具類
                FileUtils.copyURLToFile(new URL(url),new File(name));
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("IO異常,downloader()出現問題");
            }
        }
    }
    
    /*拓展部份*/
    FutureTask<Integer> futureTask = new FutureTask<Integer>(new Thread);
    new Thread(futureTask).start();
    Integer integer = futureTask.get();//需要捕獲異常
    

靜態代理模式

  • 真實物件和代理物件都要實現同一個介面

  • 代理物件搖代理真實角色

  • 好處:

    • 代理物件可以做很多真實物件做不了的事情
    • 真實物件專注做自己的事情
    • 案例:結婚
    public class StaticProxy {
        public static void main(String[] args) {
            //代理物件去做
            //WeddingCompany wc = new WeddingCompany(new You());
            //wc.HappyMarry();
            new WeddingCompany(new You()).HappyMarry();
        }
    }
    interface Marry{
         /*
        人間四大喜事
            1.久旱逢甘霖
            2.他鄉遇故知
            3.洞房花燭夜
            4.金榜題名時
         */
        void HappyMarry();
    }
    //真實角色
    class You implements Marry{
        @Override
        public void HappyMarry() {
            System.out.println("要結婚了,超開心");
        }
    }
    //婚慶公司(代理角色),幫你結婚
    class WeddingCompany implements Marry{
        private Marry target;
    
        public WeddingCompany(Marry target){
            this.target = target;
        }
        @Override
        public void HappyMarry() {
            before();
            this.target.HappyMarry();
            after();
        }
        private void before() {
            System.out.println("結婚之前,佈置現場");
        }
        private void after() {
            System.out.println("結婚之後,收尾款");
        }
    }
    

Lambda表示式

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

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

  • 其實質屬於函式時程式設計的概念

    (params) -> expression[表示式]
    (params) -> statement[語句]
    (params) -> {statements}
    //舉例
    new Thread(()->System.out.println("多執行緒學習......")).start();
    

為什麼使用lambda表示式

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

函式式介面

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

  • 函式式介面定義

    • 任何介面,如果只包含唯一一個抽象方法,那麼它就是一個函式式介面
    • 對於函式式介面,我們可以通過 Lambda 表示式來建立該介面的物件
  • 推導 Lambda表示式

    • 無參
    public class LambdaDemo {
        //3.靜態內部類
        static class LikeTwo implements ILike{
            @Override
            public void lambda() {
                System.out.println("I Like Lambda & 靜態內部類");
            }
        }
        public static void main(String[] args) {
            ILike like = new LikeOne();
            like.lambda();
            like = new LikeTwo();
            like.lambda();
            //4.區域性內部類
            class LikeThree implements ILike{
                @Override
                public void lambda() {
                    System.out.println("I Like Lambda & 區域性內部類");
                }
            }
            like = new LikeThree();
            like.lambda();
            //5.匿名內部類,沒有類的名稱,必須藉助介面或者父類
            like = new ILike(){
                @Override
                public void lambda() {
                    System.out.println("I Like Lambda & 匿名內部類");
                }
            };
            like.lambda();
            //6.lambda簡化
            like = () -> {
                System.out.println("I Like Lambda & lambda表達");
            };
            like.lambda();
        }
    }
    //1.定義一個函式式介面
    interface ILike{
        void lambda();
    }
    //2.實現類
    class LikeOne implements ILike{
        @Override
        public void lambda() {
            System.out.println("I Like Lambda & 普通實現類");
        }
    }
    
    • 帶參
    public class LambdaDemo {
        public static void main(String[] args) {
            ILove love = (int a)->{
                System.out.println("I love you ---> " + a);
            };
            //簡化1.去掉返回值型別
            love = (a)->{
                System.out.println("I love you ---> " + a);
            };
            //簡化2.去掉引數括號
            love = a->{
                System.out.println("I love you ---> " + a);
            };
            //簡化3.去掉花括號
            love = a-> System.out.println("I love you ---> " + a);
            love.love(520);
            /*
            總結:
                1.前提是介面為必須是函式式介面
                2.lambda表示式只能有一行程式碼的情況下才能簡化成為一行,如果有多行,那麼就用程式碼塊包裹
                3.多個引數也可以去掉引數型別,搖去掉就都去掉,必須加上括號
             */
            MyLove myLove = (a,b)-> System.out.println("我愛你:"+a+",你也愛我:"+b);
            myLove.love(520,125);
        }
    }
    interface ILove{
        void love(int a);
    }
    interface MyLove{
        void love(int a,int b);
    }
    
    • 多執行緒Runnable介面就是函式式介面

執行緒的狀態

  • 執行緒方法

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

執行緒停止

  • 不推薦使用 JDK 提供的stop(),destory(),【已廢棄】

  • 推薦執行緒自己停下來 ➡ 利用次數,不建議死迴圈

  • 建議使用標誌位 ➡ 設定標誌位(flag)

    • 案例:執行緒停止
    public class ThreadStop implements Runnable {
        //1.設定標誌位
        private boolean flag = true;
        @Override
        public void run() {
            int i = 0;
            while(flag){
                System.out.println("run....Thread " + i++);
            }
        }
        //2.設定一個公開的方法停止執行緒,轉換標誌位
        public void stop(){
            this.flag = false;
        }
        public static void main(String[] args) {
            ThreadStop ts = new ThreadStop();
            new Thread(ts).start();
    
            for (int i = 0; i < 1000; i++) {
                System.out.println("mian " + i);
                if(i == 900){
                    //呼叫stop切換標誌位,讓執行緒停止
                    ts.stop();
                    System.out.println("執行緒停止了....");
                }
            }
            /*
            主執行緒 到 900 ———> 停止了ts執行緒
             */
        }
    }
    

執行緒休眠_sleep

  • sleep(時間)指定當前執行緒阻塞的毫秒數

  • sleep存在異常InterruptedException(中斷異常)

  • sleep時間達到後,執行緒進入就緒狀態

  • sleep可以模擬網路延時,放大問題的發生性

  • 每一個物件都有一個鎖,sleep不會釋放鎖

    • 案例:計時
    public class ThreadSleep{
        public static void tenDown() throws InterruptedException {
            int num = 10;
            while (true){
                Thread.sleep(1000);
                System.out.println(num--);
                if(num <= 0){
                    break;
                }
            }
        }
        public static void main(String[] args) {
            try {
                tenDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    • 案例:一秒獲取一次系統時間
    public static void main(String[] args){
        //列印當前系統時間
        Date startTime = new Date(System.currentTimeMillis());//獲取系統當前時間
        while(true){
            try {
                Thread.sleep(1000);
                System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
                startTime = new Date(System.currentTimeMillis()); //更新當前時間
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

執行緒禮讓_yield

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

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

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

    public class ThreadYield {
        public static void main(String[] args) {
            MyYield my = new MyYield();
            new Thread(my,"A").start();
            new Thread(my,"B").start();
        }
    }
    class MyYield implements Runnable{
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"執行緒開始執行...");
            Thread.yield();//執行緒禮讓
            System.out.println(Thread.currentThread().getName()+"執行緒停止執行...");
        }
    }
    

執行緒強制執行_join

  • Join合併執行緒,待次執行緒完成後,再執行其他執行緒,其他執行緒阻塞,可以想象成為插隊

    public class ThreadJoin implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                System.out.println("執行緒VIP來了 "+i);
            }
        }
        public static void main(String[] args) throws InterruptedException {
            //啟動我們的執行緒
            ThreadJoin tj = new ThreadJoin();
            Thread thread = new Thread(tj);
            thread.start();
            //主執行緒
            for (int i = 0; i < 500; i++) {
                if(i == 200){
                    thread.join();//插隊
                }
                System.out.println("main"+i);
            }
        }
    }
    

執行緒狀態觀測

  • Thread.State,執行緒狀態,檢視API文件列舉型別。

    • NEW,尚未啟動的執行緒處於此狀態
    • RUNNABLE,再Java虛擬機器中執行的執行緒處於此狀態
    • BLOCKED,被阻塞等待監視器鎖定的執行緒處於此狀態
    • WAITING,正在等待另一個執行緒執行特定動作的執行緒處於此狀態
    • TIMED_WAITING,正在等待另一個執行緒執行動作達到指定等待時間的執行緒處於此狀態
    • TERMINATED,已退出的執行緒處於此狀態
  • 一個執行緒可以再給定時間點處於一個狀態,這些狀態時不反應任何作業系統執行緒狀態的虛擬機器狀態

    public class ThreadState {
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(()->{
                for (int i = 0; i < 5; i++) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(".......");
                }
            });
            //觀察狀態
            Thread.State state = thread.getState();
            System.out.println(state);//NEW
            //觀察啟動後
            thread.start();//啟動執行緒
            state = thread.getState();
            System.out.println(state);//Run
            while (state != Thread.State.TERMINATED){//只要執行緒不終止,就一直輸出狀態
                Thread.sleep(100);
                state = thread.getState();//更新執行緒狀態
                System.out.println(state);
            }
        }
    }
    

執行緒的優先順序

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

  • 執行緒的優先順序用數字表示,範圍從1~10

    • Thread.MIN_PRIORITY = 1
    • Thread.MAX_PRIORITY = 10
    • Thread.NORM_PRIORITY = 5
  • 使用以下方式改變或獲取優先順序(get/set)

    • getPriority()/setPriority(int xxx)
  • 優先順序低只是意味著獲得排程的概率低,並不是優先順序低就不會被呼叫了,這都是看CPU的排程

    public class ThreadPriority{
        public static void main(String[] args) {
            System.out.println(Thread.currentThread().getName() + " ---> " + Thread.currentThread().getPriority());//5
            MyPriority mp = new MyPriority();
            Thread t0 = new Thread(mp);
            Thread t1 = new Thread(mp);
            Thread t2 = new Thread(mp);
            Thread t3 = new Thread(mp);
            Thread t4 = new Thread(mp);
            Thread t5 = new Thread(mp);
            //先設定優先順序,再啟動
            t0.start(); //5
            t1.setPriority(Thread.MIN_PRIORITY);  //1
            t1.start();
            t2.setPriority(4);
            t2.start();
            t3.setPriority(Thread.MAX_PRIORITY); //10
            t3.start();
            t4.setPriority(8);
            t4.start();
            t5.setPriority(7);
            t5.start();
        }
    }
    class MyPriority implements Runnable{
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " ---> " + Thread.currentThread().getPriority());
        }
    }
    

守護(daemon)執行緒

  • 執行緒分為使用者執行緒和守護執行緒

  • 虛擬機器確保使用者執行緒執行完畢

  • 虛擬機器不用等待守護執行緒執行完畢

  • 如:後臺記錄操作日誌,監控記憶體,垃圾回收等待....

    public class ThreadDaemon {
        public static void main(String[] args) {
            God god = new God();
            You you = new You();
            Thread godThread = new Thread(god);//上帝 守護執行緒
            godThread.setDaemon(true);//預設時false表示使用者執行緒,正常的執行緒都是使用者執行緒
            godThread.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("你一生都開心的活著...");
            }
            System.out.println("=======goodBye! world=======");
        }
    }
    

執行緒同步

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

安全

  • 佇列+鎖

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

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

三大不安全案例

  • 案例:購票,購到了負數的票

    public class UnsafeBuyTicket {
        public static void main(String[] args) {
            BuyTicket station = new BuyTicket();
            new Thread(station,"苦逼的我").start();
            new Thread(station,"牛逼的你們").start();
            new Thread(station,"萬惡的黃牛黨").start();
        }
    }
    class BuyTicket implements Runnable{
        //票
        private int ticketNums = 10;
        boolean flag = true;//外部停止方式
        @Override
        public void run() {
            //買票
            while (flag){
                try {
                    buy();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        private void buy() throws InterruptedException {
            //判斷是否有票
            if(ticketNums<=0){
                flag =false;
                return;
            }
            //模擬延時
            Thread.sleep(100);
            //買票
            System.out.println(Thread.currentThread().getName()+"買到"+ticketNums--);
        }
    }
    
  • 案例,銀行取錢,餘額符數

    public class UnsafeBank {
        public static void main(String[] args) {
            //賬戶
            Account account = new Account(100,"結婚基金");
            Drawing you = new Drawing(account,50,"你");
            Drawing girlFriend = new Drawing(account,100,"女朋友");
            you.start();
            girlFriend.start();
        }
    }
    //賬戶
    class Account{
        int money;//餘額
        String name;//卡名
        public Account(int money, String name) {
            this.money = money;
            this.name = name;
        }
    }
    //銀行:模擬取款
    class Drawing extends Thread{
        Account account; //賬戶
        int drawingMoney;//取了多少錢
        int nowMoney;//手機還有多少錢
        public Drawing (Account account,int drawingMoney,String name){
            super(name);
            this.account = account;
            this.drawingMoney = drawingMoney;
        }
        @Override
        public void run() {
            //判斷有沒有錢
            if(account.money - drawingMoney < 0){
                //this.getName() = Thread.currentThread().getName()
                System.out.println(this.getName()+"錢不夠,取不了");
                return;
            }
            //sleep,放大問題的發生性
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //卡內餘額 = 餘額 - 取得錢
            account.money = account.money - drawingMoney;
            //你手裡得錢
            nowMoney = nowMoney + drawingMoney;
            System.out.println(account.name+"餘額為:"+account.money);
            System.out.println(this.getName()+"手裡的錢:"+nowMoney);
        }
    }
    
  • 案例,執行緒不安全的集合

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());//總會出現小於10000的集合數
    }
    

同步 synchronized

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

    • 同步方法
    public synchronized void method(int args){}
    //案例
    public class UnsafeBuyTicket {
        public static void main(String[] args) {
            BuyTicket station = new BuyTicket();
            new Thread(station,"苦逼的我").start();
            new Thread(station,"牛逼的你們").start();
            new Thread(station,"萬惡的黃牛黨").start();
        }
    }
    class BuyTicket implements Runnable{
    
        //票
        private int ticketNums = 10;
        boolean flag = true;//外部停止方式
    
        @Override
        public void run() {
            //買票
            while (flag){
                try {
                    buy();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        // synchronized 同步方法,鎖的是this
        private synchronized void buy() throws InterruptedException {
            //判斷是否有票
            if(ticketNums<=0){
                flag =false;
                return;
            }
            //模擬延時
            Thread.sleep(1000);
            //買票
            System.out.println(Thread.currentThread().getName()+"買到"+ticketNums--);
        }
    }
    
    
    
  • synchronized 方法控制對"物件"的訪問,每個物件對用一把鎖每個 synchronized 方法都必須獲得呼叫該方法的物件的鎖才能執行,否則執行緒會阻塞,方法一旦執行,就獨佔該鎖,直到該方法返回才釋放鎖,後面被阻塞的執行緒才能獲得這個鎖繼續執行

    • 缺陷,若將一個大的方法申明為 synchronized 會影響效率
  • 同步塊

    synchronized (Obj){}
    
    • Obj 稱之為 同步監視器
    • Obj可以是任何物件,但是推薦使用共享資源為同步監視器
    • 同步方法中無需指定同步監視器,因為同步方法的同步監視器就是this,就是這個物件本身,或者是class(反射)
  • 同步監視器的執行過程

    • 第一個執行緒訪問,鎖定同步監視器,執行其中程式碼
    • 第二個執行緒訪問,發現同步監視器被鎖定,無法訪問
    • 第一個執行緒訪問結束,解鎖同步監視器
    • 第二個執行緒訪問,發現同步監視器沒有鎖,然後鎖定並訪問
    public class UnsafeBank {
        public static void main(String[] args) {
            //賬戶
            Account account = new Account(1000,"結婚基金");
            Drawing you = new Drawing(account,50,"你");
            Drawing girlFriend = new Drawing(account,100,"女朋友");
            you.start();
            girlFriend.start();
        }
    }
    //賬戶
    class Account{
        int money;//餘額
        String name;//卡名
        public Account(int money, String name) {
            this.money = money;
            this.name = name;
        }
    }
    //銀行:模擬取款
    class Drawing extends Thread{
        Account account; //賬戶
        int drawingMoney;//取了多少錢
        int nowMoney;//手機還有多少錢
    
        public Drawing (Account account,int drawingMoney,String name){
            super(name);
            this.account = account;
            this.drawingMoney = drawingMoney;
        }
        //synchronized 預設鎖this,this是銀行所以不管用,要鎖變數的量 ➡ 賬戶
        @Override
        public void run() {
            synchronized(account) {
                //判斷有沒有錢
                if (account.money - drawingMoney < 0) {
                    //this.getName() = Thread.currentThread().getName()
                    System.out.println(this.getName() + "錢不夠,取不了");
                    return;
                }
                //sleep放大問題的發生性
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //卡內餘額 = 餘額 - 取得錢
                account.money = account.money - drawingMoney;
                //你手裡得錢
                nowMoney = nowMoney + drawingMoney;
                System.out.println(account.name + "餘額為:" + account.money);
                System.out.println(this.getName() + "手裡的錢:" + nowMoney);
            }
        }
    }
    
  • JUC安全型別的集合

    • JUC就是 java.util .concurrent 工具包的簡稱。這是一個處理執行緒的工具包,JDK 1.5開始出現的。
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList();
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
    

死鎖

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

    //死鎖:多個執行緒互相抱著對方想要的資源,然後形成僵持,導致程式卡死
    public class DeadLock {
        public static void main(String[] args) {
            Makeup gOne = new Makeup(0,"灰姑娘");
            Makeup gTwo = new Makeup(1,"白雪公主");
            gOne.start();
            gTwo.start();
        }
    }
    //口紅
    class Lipstick{ }
    //鏡子
    class Mirror{ }
    //化妝
    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+" 獲得口紅的鎖");
                    sleep(1000);
                    synchronized (mirror){//一秒鐘後獲得鏡子的鎖
                        System.out.println(this.girlName+" 獲得鏡子的鎖");
                    }
                }
            }else{
                synchronized (mirror){//獲得口紅的鎖
                    System.out.println(this.girlName+" 獲得口紅的鎖");
                    sleep(1000);
                    synchronized (lipstick){//一秒鐘後獲得鏡子的鎖
                        System.out.println(this.girlName+" 獲得鏡子的鎖");
                    }
                }
            }
        }
    }
    /*
    兩把鎖獨立,不去包對方的鎖,程式正常執行
    	private void makeup() throws InterruptedException {
            if(choice == 0){
                synchronized (lipstick){//獲得口紅的鎖
                    System.out.println(this.girlName+" 獲得口紅的鎖");
                    sleep(1000);
                }
                synchronized (mirror){//一秒鐘後獲得鏡子的鎖
                    System.out.println(this.girlName+" 獲得鏡子的鎖");
                }
            }else{
                synchronized (mirror){//獲得口紅的鎖
                    System.out.println(this.girlName+" 獲得口紅的鎖");
                    sleep(1000);
                }
                synchronized (lipstick){//一秒鐘後獲得鏡子的鎖
                    System.out.println(this.girlName+" 獲得鏡子的鎖");
                }
            }
        }
    */
    

Lock鎖

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

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

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

    public class RunLock {
        public static void main(String[] args) {
            ThreadLock tl = new ThreadLock();
            new Thread(tl).start();
            new Thread(tl).start();
            new Thread(tl).start();
        }
    }
    class ThreadLock implements Runnable{
        int ticketNums = 10;
        //定義Lock鎖
        private final ReentrantLock lock = new ReentrantLock();
        @Override
        public void run() {
            while(true){
                lock.lock();//加鎖
                try{
                    if(ticketNums>0){
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(ticketNums--);
                    }else{
                        break;
                    }
                }finally {
                    lock.unlock();//解鎖
                }
    
            }
        }
    }
    

synchronized & Lock

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

執行緒協作通訊

生產者&消費者問題

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

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

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

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

    • 對於生產者,沒有生產產品之前,要通知消費者等待,而生產了產品之後,有需要馬上通知消費者消費
    • 對於消費者,在消費之後要通知生產者已結束消費,需要生產新得產品以供消費
    • 生產者消費問題中,僅有synchronized 是不夠的。synchronized 可組織併發更新同一個資源實現了同步,但不能用來實現不同執行緒之間的訊息傳遞(通訊)
  • Java提供了幾個方法解決執行緒之間的通訊問題

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

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

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

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

    /*
    生產者消費者模型➡利用緩衝區解決:管程法
    生產者 消費者 產品  緩衝區
     */
    public class RunMethod {
        public static void main(String[] args) {
            Buffer buffer = new Buffer();
    
            new Producer(buffer).start();
            new Consumer(buffer).start();
    
        }
    }
    
    //生產者
    class Producer extends Thread{
        Buffer buffer;
        public Producer(Buffer buffer){
            this.buffer = buffer;
        }
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                buffer.push(new Product(i));
                System.out.println("生產了"+i+"個產品");
            }
        }
    }
    //消費者
    class Consumer extends Thread{
        Buffer buffer;
        public Consumer(Buffer buffer){
            this.buffer = buffer;
        }
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("消費了"+buffer.pop().id+"個產品");
            }
        }
    }
    //產品
    class Product{
        int id;//產品編號
    
        public Product(int id) {
            this.id = id;
        }
    }
    //緩衝區
    class Buffer{
        //需要一個容器大小
        Product[] products = new Product[10];
        //容器計數器
        int count = 0;
        //生產者放入產品
        public synchronized void push(Product product){
            //如果容器滿了,就需要等待消費者消費
            if(count == products.length){
                //通知消費者消費,生產者等待
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //如果沒有滿,我們就需要丟入產品
            products[count] = product;
            count++;
            //通知消費者可以消費了
            this.notifyAll();
        }
        //消費者消費產品
        public synchronized Product pop(){
            //判斷能否消費
            if(count == 0){
                //等待生產者生產,消費者等待
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //如果可以消費
            count--;
            Product product = products[count];
            //通知生產者生產
            this.notifyAll();
            return product;
        }
    }
    
  • 併發協作模型"生產者/消費者模式"➡訊號燈法

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

執行緒池

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

  • 思路 ➡ 提前建立好多個執行緒,放入執行緒池中,使用時直接獲取,使用完放回池中。可以避免頻繁建立銷燬、實現重複利用。類似生活中的公共交通工具

  • 好處

    • 提高響應速度(減少了建立新執行緒的時間)
    • 降低資源消耗(重複利用執行緒池中執行緒,不需要每次都建立)
    • 便於執行緒管理(....)
  • 常用管理命令:

    • corePoolSize,核心池的大小
    • maxmumPoolSize,最大執行緒數
    • keepAliveTime,執行緒沒有任務時最多保持多長時間後會終止
  • JDK 5.0 提供了執行緒池相關的額API,ExecetorSevice 和 Executors

  • ExecetorSevice,真正的執行緒池介面,常見子類 ThreadPoolExecutor

    • void extcute(Runnable command),執行任務/命令,沒有返回值,一般用來執行Runnable
    • Futuresubmit(callable task),執行任務,有返回值,一般又來執行Callable
    • void shutdown(),關閉連線池
  • Executors,工具類,執行緒池的工廠類,用於建立並返回不同型別的執行緒池

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