java - 多執行緒基礎

不回頭~發表於2020-12-17

1.執行緒介紹

1.1 多工

在這裡插入圖片描述

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

1.2 多執行緒

在這裡插入圖片描述

原來是一條路,慢慢因為車太多了,道路堵塞,效率極低。為了提高使用效率,能夠充分利用道路,於是加了多個車道。

1.3 普通方法呼叫和多執行緒

在這裡插入圖片描述

1.4 程式.程式.執行緒

在這裡插入圖片描述
程式是程式的一次動態執行過程,它需要經歷從程式碼載入,程式碼執行到執行完畢的一個完整的過程,這個過程也是程式本身從產生,發展到最終消亡的過程。多程式作業系統能同時達執行多個程式(程式),由於 CPU 具備分時機制,所以每個程式都能迴圈獲得自己的CPU 時間片。由於 CPU 執行速度非常快,使得所有程式好像是在同時執行一樣。

多執行緒是實現併發機制的一種有效手段。程式和執行緒一樣,都是實現併發的一個基本單位。執行緒是比程式更小的執行單位,執行緒是程式的基礎之上進行進一步的劃分。所謂多執行緒是指一個程式在執行過程中可以產生多個更小的程式單元,這些更小的單元稱為執行緒,這些執行緒可以同時存在,同時執行,一個程式可能包含多個同時執行的執行緒。

1.5 Process 與 Thread

  • 說起程式,就不得不說下程式。程式是指令和資料的有序集合,其本身並沒有任何的執行的含義,是一個靜態的概念
  • 而程式則是可執行程式的一次執行過程(可以理解為:執行中的程式),他是一個動態的概念。是系統資源分配的單位
  • 通常在一個程式中可以包含若干個執行緒,當然一個程式中至少有一個執行緒,不然沒有存在的意義。執行緒是CPU 排程和執行的單位。

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

1.6 總結

在這裡插入圖片描述

2.執行緒的建立

2.1 三種建立方式

在這裡插入圖片描述

2.2 繼承 Thread 類

  • 自定義執行緒類繼承 Thread
  • 重寫run()方法,編寫執行緒執行體
  • 建立執行緒物件,呼叫start()方法啟動執行緒
//建立一個物件,繼承Thread類
public class ThreadTest1 extends Thread{

//    實現 run 方法
    @Override
    public void run() {
//        其他執行緒列印
        for (int i = 0; i < 2000; i++) {
            System.out.println("Thread -- 我在學多執行緒");
        }
    }

    public static void main(String[] args) {
//        建立執行緒物件
        ThreadTest1 threadTest1 = new ThreadTest1();
//        開啟執行緒
        threadTest1.start();
//        主執行緒列印
        for (int i = 0; i < 2000; i++) {
            System.out.println("main -- 我真的在學多執行緒");
        }
    }
}

總結:

  • 執行緒開啟不一定執行,由 CPU 排程執行

  • 不是呼叫 run()方法,要呼叫start()方法

2.2.1 案例:下載圖片

2.2.2 下載 commons-io.jar 並匯入

在這裡插入圖片描述在 src 目錄下建立 一個 lib 目錄,把下載的jar拷貝到該目錄下。

在這裡插入圖片描述
在這裡插入圖片描述

在這裡插入圖片描述

2.2.3 程式碼

//練習Thread,實現多執行緒同步下載圖片
public class ThreadTest2 extends Thread {

    private String url;
    private String name;

    public ThreadTest2() {
    }

    public ThreadTest2(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) {
        ThreadTest2 threadTest1 = new ThreadTest2("https://dss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=1819216937,2118754409&fm=26&gp=0.jpg", "1.jpg");
        ThreadTest2 threadTest2 = new ThreadTest2("https://dss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=1819216937,2118754409&fm=26&gp=0.jpg", "2.jpg");
        ThreadTest2 threadTest3 = new ThreadTest2("https://dss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=1819216937,2118754409&fm=26&gp=0.jpg", "3.jpg");
        threadTest1.start();
        threadTest2.start();
        threadTest3.start();
    }
}

//下載器
class WebDownloader {

    //    下載方法
    public void downloader(String url, String name) {
        try {
            FileUtils.copyURLToFile(new URL(url), new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO異常!");
        }
    }

}

2.3 實現 Runnable 介面

  • 定義 MyRunnable 類實現 Runnable 介面
  • 實現 run()方法,編寫執行緒執行體
  • 建立執行緒物件,呼叫 start()方法啟動執行緒
//建立一個物件,實現Runnable介面
public class ThreadTest3 implements Runnable {

    //    實現 run 方法
    @Override
    public void run() {
//        其他執行緒列印
        for (int i = 0; i < 2000; i++) {
            System.out.println("Thread -- 我在學多執行緒");
        }
    }

    public static void main(String[] args) {
        //建立 Runnable 實現類物件
        ThreadTest3 threadTest3 = new ThreadTest3();
//        建立執行緒物件,通過執行緒物件來開啟我們的執行緒
        Thread thread = new Thread(threadTest3);
        thread.start();
//        主執行緒列印
        for (int i = 0; i < 2000; i++) {
            System.out.println("main -- 我真的在學多執行緒");
        }
    }
}

2.3.1 初現併發問題

// 多個執行緒同時操作一個物件
//    買火車票例子
//    發現問題:多個執行緒操作同意資源的情況下,執行緒不安全了,資料混亂
public class ThreadTest4 implements Runnable {


    private int ticketNums = 10;

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (ticketNums <= 0) {
                break;
            }
            System.out.println(Thread.currentThread() + "拿到了第" + ticketNums-- + "張票");
        }
    }

    public static void main(String[] args) {
        ThreadTest4 test = new ThreadTest4();
        new Thread(test,"小明").start();
        new Thread(test,"老師").start();
        new Thread(test,"黃牛黨").start();

    }
}

Thread[黃牛黨,5,main]拿到了第10張票
Thread[小明,5,main]拿到了第10張票
Thread[老師,5,main]拿到了第9張票
Thread[小明,5,main]拿到了第8張票
Thread[黃牛黨,5,main]拿到了第8張票
Thread[老師,5,main]拿到了第8張票
Thread[黃牛黨,5,main]拿到了第7張票
Thread[老師,5,main]拿到了第7張票
Thread[小明,5,main]拿到了第6張票
Thread[小明,5,main]拿到了第5張票
Thread[老師,5,main]拿到了第5張票
Thread[黃牛黨,5,main]拿到了第4張票
Thread[小明,5,main]拿到了第3張票
Thread[黃牛黨,5,main]拿到了第1張票
Thread[老師,5,main]拿到了第2張票

第十張表即被黃牛黨獲取,又被小明獲取…導致執行緒不安全,產生了併發問題

2.3.2 案例:龜兔掃跑

//模擬龜兔賽跑
public class Race implements Runnable {

    private static String winner;

    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {

            if("兔子".equals(Thread.currentThread().getName())){
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            boolean flag = gameOver(i);
            if(flag){
                break;
            }
            System.out.println(Thread.currentThread().getName() + "--> 跑了" + i + "步");
        }
    }

    //    判斷是否完成比賽
    private boolean gameOver(int steps) {
        if (winner != null) {
            return true;
        } else {
            if (steps >= 100) {
                winner = Thread.currentThread().getName();
                System.out.println("winner is " + winner);
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Race race = new Race();
        new Thread(race,"兔子").start();
        new Thread(race,"烏龜").start();
    }
}

2.4 實現 Callable 介面

  1. 實現 Callable介面,需要返回值型別
  2. 重寫call方法,需要丟擲異常
  3. 建立目標物件
  4. 建立執行服務:ExecutorService ser = Executors.newFixedThreadPool(1);
  5. 提交執行:Future<Boolean> trsult = ser.submit(t1);
  6. 獲取結果:boolean r1 = result.get();
  7. 關閉服務:ser.shutdownNow();
public class TestCallable implements Callable<Boolean> {
    private String url;
    private String name;

    public TestCallable() {
    }

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


    public static void main(String[] args) throws ExecutionException, InterruptedException {
        TestCallable threadTest1 = new TestCallable("https://dss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=1819216937,2118754409&fm=26&gp=0.jpg", "1.jpg");
        TestCallable threadTest2 = new TestCallable("https://dss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=1819216937,2118754409&fm=26&gp=0.jpg", "2.jpg");
        TestCallable threadTest3 = new TestCallable("https://dss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=1819216937,2118754409&fm=26&gp=0.jpg", "3.jpg");
//      建立執行服務
        ExecutorService ser = Executors.newFixedThreadPool(3);
//        提交執行
        Future<Boolean> submit = ser.submit(threadTest1);
        Future<Boolean> submit1 = ser.submit(threadTest2);
        Future<Boolean> submit2 = ser.submit(threadTest3);
//        獲取結果
        Boolean aBoolean = submit.get();
        Boolean aBoolean1 = submit1.get();
        Boolean aBoolean2 = submit2.get();



//        關閉服務
        ser.shutdownNow();
    }


    @Override
    public Boolean call() throws Exception {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url, name);
        System.out.println("下載了:" + name);
        return true;
    }
}

//下載器
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異常!");
        }
    }

}

2.5 靜態代理

//靜態代理模式:
//    1.真實物件 和 代理 物件都要實現同一個介面
//    2.代理物件要代理真實角色
// 好處:
//    1.代理物件可以做很多真實物件做不了的事情
//    2.真實物件專注做自己的事情
public class StaticProxy {

    public static void main(String[] args) {

//        Thread 是一個代理物件,代理的真實物件時 Runnable 介面
        new Thread(()-> System.out.println("我愛你")).start();
        new WeddingCompany(new You()).HappyMarry();

        WeddingCompany weddingCompany = new WeddingCompany(new You());
//        通過代理去"結婚"
        weddingCompany.HappyMarry();
    }

}

interface Marry {
    void HappyMarry();
}

//真實角色,你去結婚
class You implements Marry {

    @Override
    public void HappyMarry() {
        System.out.println("我結婚啦啦啦!");
    }
}

//代理角色,幫助你結婚
class WeddingCompany implements Marry {
    //代理誰?? -- 真實物件
    private Marry target;

    public WeddingCompany(Marry target) {
        this.target = target;
    }

    @Override
    public void HappyMarry() {
        before();
        this.target.HappyMarry();//真實角色
        after();
    }

    private void after() {
        System.out.println("結婚之後收尾款!");
    }

    private void before() {
        System.out.println("結婚之前,不知執行緒!");
    }
}

PS:可以去詳細瞭解代理模式:靜態代理,動態代理。因為 Thread 涉及到代理模式,所以在這裡提了一嘴

3.Lamdba表示式

new Thread( () -> System.out.println("多執行緒程式設計!") )

為什麼要使用Lamdba表示式?

  • 避免匿名內部類定義過多
  • 可以讓你的程式碼看起來很簡潔
  • 去掉了一堆沒有意義的程式碼,只留下核心的邏輯

函式式介面

  • 任何介面,如果只包含一個抽象方法,那麼它就是一個函式式介面
  • 對於函式式介面,我們可以通過Lamda表示式來建立該介面的物件
public class TestLambda {
    //3.第二階段:建立靜態內部類
    static class Like2 implements ILike {

        @Override
        public void lambda() {
            System.out.println("I like lambda2!");
        }
    }

    public static void main(String[] args) {
        ILike like = new Like();
        like.lambda();

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

//      第三階段:區域性內部類
        class Like3 implements ILike {

            @Override
            public void lambda() {
                System.out.println("I like lambda3!");
            }
        }

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

//        第四階段:匿名內部類
        like = new ILike() {
            @Override
            public void lambda() {
                System.out.println("I like lambda4!");
            }
        };

//        第五階段:Lambda表示式
        like = () -> {
            System.out.println("I like lambda5!");
        };
    }
}

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

}

//2.第一階段:建立實現類
class Like implements ILike {

    @Override
    public void lambda() {
        System.out.println("I like lambda!");
    }
}

一步一步的演變過來

  1. 第一階段:建立介面實現類,重寫方法,實現功能
  2. 第二階段:拋棄實現類,直接使用靜態內部類實現介面,重寫方法
  3. 第三階段:建立區域性內部類,實現介面,重寫方法
  4. 第四階段:使用匿名內部類方式,重寫方法
  5. 第五階段:Lambda 表示式
  • 無引數無返回值
like = () -> {
	System.out.println("I like lambda5!");
};
  • 有引數無返回值
like = (int a,int b) -> {
	System.out.println(b+"I like lambda5!" + a);
};1.簡化引數型別(要簡化就所有引數型別都簡化)
like = (a,b) -> {
	System.out.println("I like lambda5!" + a);
};2.簡化圓括號(只有一個引數才能簡化圓括號)
like = a -> {
	System.out.println("I like lambda5!" + a);
};3.簡化花括號(程式碼只有一行才能簡化)
like = a -> System.out.println("I like lambda5!" + a);

PS:建議自己去搜尋更加詳細的資料閱讀。

4. 執行緒狀態

在這裡插入圖片描述

在這裡插入圖片描述

4.1 執行緒方法 - Thread

在這裡插入圖片描述

4.2 停止執行緒

  • 不推薦使用JDK提供的stop(),destory()方法
  • 推薦執行緒自己停止下來
  • 建議使用一個標誌位進行終止變數,當flag = false,則終止執行緒執行
//使用一個標誌位進行終止變數,當flag = false,則終止執行緒執行
public class TestStop implements Runnable {
    public static void main(String[] args) {
        TestStop testStop = new TestStop();
        new Thread(testStop).start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("main" + i);
            if (i == 900) {
                testStop.stop();
                System.out.println("執行緒停止了");
            }
        }
    }

    //    設定標誌位進行終止變數
    private boolean flag = true;

    @Override
    public void run() {
        int i = 0;
        while (flag) {
            System.out.println("run...Thread" + i);
        }
    }

    public void stop() {
        this.flag = false;
    }
}

4.3 執行緒休眠

  • sleep(時間)指定當前執行緒阻塞的毫秒數
  • sleep存在異常InterruptedException
  • sleep時間達到後,執行緒進入就緒狀態
  • sleep可以模擬網路延時,倒數計時等(Thread.sleep(時間)
  • 每一個物件都有一個鎖,sleep不會釋放鎖

4.4 執行緒禮讓

  • 禮讓執行緒,讓當前正在執行的執行緒暫停,但不阻塞
  • 將執行緒從執行狀態轉為就緒狀態
  • 讓 cpu 重新排程,禮讓不一定成功!看 cpu 心情
public class TestYield {

    public static void main(String[] args) {
        MyYield myYield = new MyYield();
        new Thread(myYield,"a").start();
        new Thread(myYield,"b").start();

    }
}

class MyYield implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "執行緒開始執行");
        Thread.yield();
        System.out.println(Thread.currentThread().getName() + "執行緒停止執行");
    }
}

a執行緒開始執行
b執行緒開始執行
a執行緒停止執行
b執行緒停止執行

4.5 合併執行緒 Join

  • Join 合併執行緒,待此執行緒執行完成後,再執行其他執行緒,其他執行緒阻塞
  • 可以想象為 插隊
public class TestJoin implements Runnable {
    public static void main(String[] args) throws InterruptedException {
        TestJoin testJoin = new TestJoin();
        Thread thread = new Thread(testJoin);
        thread.start();

        for (int i = 0; i < 1000; i++) {
            if(i == 200){
                thread.join(); //插隊
            }
            System.out.println("main "+i);
        }
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("VIP 來了!" + i);
        }
    }
}

4.6 執行緒狀態觀測

Thread.State

執行緒狀態。執行緒可以處於以下狀態之一:

  • NEW

尚未啟動的執行緒處於此狀態

  • RUNNABLE

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

  • BLOCKED

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

  • WAITING

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

  • TIME_WAITING

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

  • TERMINATED

已退出的執行緒處於此狀態

//觀察執行緒狀態
public class TestState {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("。。。。。。。。");
            }
        });
		//獲取執行緒狀態
        Thread.State state = thread.getState();
        System.out.println(state);

        thread.start();
        state = thread.getState();
        System.out.println(state);

        while (state != Thread.State.TERMINATED) {
            Thread.sleep(100);
            state = thread.getState();
            System.out.println(state);
        }
        //        thread.start(); 執行緒執行結束後不能再重新啟動,只能啟動一次,否則報錯
    }
}

4.7 執行緒優先順序

  • Java 提供一個執行緒排程器來監控程式中啟動後進入就緒狀態的所有執行緒,執行緒排程器按照優先順序決定應該排程哪個執行緒來執行
  • 執行緒的優先順序用數字表示,範圍 1~10
    • Thread.MIN_PRIOPITY = 1;
    • Thread.MAX_PRIOPITY = 10;
    • Thread.NORM_PRIOPITY = 5;
  • 使用以下方式改變或獲取優先順序
    • getPriority()
    • setPriority(int xxx)
public class TestPriority {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()+"--->"+ Thread.currentThread().getPriority());

        MyPriority myPriority = new MyPriority();
        Thread thread = new Thread(myPriority);
        Thread thread1 = new Thread(myPriority);
        Thread thread2 = new Thread(myPriority);
        Thread thread3 = new Thread(myPriority);
        Thread thread4 = new Thread(myPriority);

        thread.start();
		//先設定優先順序,再啟動
        thread1.setPriority(4);
        thread1.start();

        thread2.setPriority(Thread.MAX_PRIORITY);
        thread2.start();

        thread3.setPriority(-1); //報錯
        thread3.start();

        thread4.setPriority(11); //報錯
        thread4.start();
    }
}

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

注意:優先順序低只是意味著獲取排程的概率低,並不是優先順序低就不會被呼叫了,這都是看cpu的排程

4.8 守護執行緒 daemon

  • 執行緒分為使用者執行緒 和 守護執行緒
  • JVM虛擬機器必須確保使用者執行緒執行完畢
  • JVM虛擬機器不用等待守護執行緒執行完畢
  • 如,後臺記錄操作日誌,監控記憶體,垃圾回收等等都是守護執行緒
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();

        Thread thread1 = new Thread(you);
        thread1.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!=========");
    }
}

守護執行緒用來服務於使用者執行緒,不需要上層邏輯介入。其實守護執行緒和使用者執行緒區別不大,可以理解為特殊的使用者執行緒。特殊就特殊在如果程式中所有的使用者執行緒都退出了,那麼所有的守護執行緒就都會被殺死,很好理解,沒有被守護的物件了,也不需要守護執行緒了。

5.執行緒同步

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

併發:

  • 同一個物件被多個執行緒同時操作
  • 例如搶票,兩個銀行同時取錢

執行緒同步:

  • 現實生活中,我們會遇到“同一個資源,多個人都想使用”的問題,比如食堂排隊打飯,每個人都想吃飯,最天然的解決辦法就是,排隊,一個一個來
  • 處理多執行緒問題時,多個執行緒訪問同一個物件,並且這個執行緒還想修改這個物件,這時候我們就需要執行緒同步。執行緒同步其實就是一種等待機制,多個需要同時訪問此物件的執行緒進入這個物件的等待池形成佇列,等待前面的執行緒使用完畢,下一個執行緒再使用
  • 由於同一個程式的多個執行緒共享同一塊儲存空間,在帶來方便的同時,也帶來了訪問衝突問題,為了保證資料在方法中被訪問的正確性,在訪問時加入鎖機制 synchronized ,當一個執行緒獲的物件的排它鎖,獨佔資源,其他執行緒必須等待,使用後釋放鎖即可。存在以下問題:
    • 一個執行緒持有鎖會導致其他所有需要此鎖的執行緒掛起
    • 在多執行緒競爭下,加鎖,釋放鎖會導致比較多的上下文切換 和 排程延時,引起效能問題
    • 如果一個優先順序高的執行緒等待一個優先順序低的執行緒釋放鎖 會 導致優先順序倒置,引起效能問題

佇列+鎖解決多執行緒安全問題。

5.1 執行緒不安全案例

執行緒不安全的買票

//不安全的買票
public class UnsafeBuyTicket {
    public static void main(String[] args) {
        BuyTicket station = new BuyTicket();
        new Thread(station,"苦逼的我").start();
        new Thread(station,"牛逼的你men").start();
        new Thread(station,"可惡的黃牛黨").start();
    }
}

class BuyTicket implements Runnable {

    private int ticketNums = 10;
    boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            buy();
        }
    }

    public void stop() {
        this.flag = false;
    }

    private void buy() {
        if (ticketNums <= 0) {
            return;
        }
        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,"girlfriend");
        you.start();
        girlfriend.start();
    }
}

//賬戶
class Account {
    int money;
    String name;

    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }

}

//銀行:模擬取款
class Drawing extends Thread {

    Account account;

    int drawingMoney;

    int  nowMoney;

    public Drawing(Account account, int drawingMoney, String name) {
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    //    取錢
    @Override
    public void run() {
        if (account.money - drawingMoney < 0) {
            System.out.println(Thread.currentThread().getName() + "錢不夠了,取不了");
            return;
        }
        try {
            Thread.sleep(1000);
        } 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 class UnsafeList {
   public static void main(String[] args) throws InterruptedException {
       List<String> list = new ArrayList<>();
       for (int i = 0; i < 10000; i++) {
           new Thread(() -> {
               list.add(Thread.currentThread().getName());
           }).start();
       }
       Thread.sleep(3000);
       System.out.println(list.size()); //9998
   }
}

5.2 解決執行緒不安全問題

同步方法

  • 由於我們可以通過 private 關鍵字來保證資料物件只能被訪問,借鑑這個思路,我們只需要針對方法提出一套機制,這套機制就是synchronized關鍵字,它包括兩種用法:
    • synchronized 方法 和 synchronized塊
    • 同步方法:public synchronized void method(int args){}
  • synchronized方法控制對“物件”的訪問,每個物件對應一把鎖,每個synchronized方法都必須獲取呼叫該方法的物件的鎖才能執行,否則執行緒會阻塞,方法一旦執行,就獨佔該鎖,直到該方法返回才釋放鎖,後面被阻塞的執行緒才能獲得這個鎖,繼續執行
    • 缺陷:若將一個大的方法申明為synchronized 將會影響效率

同步塊

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

執行緒安全的買票

public class safeBuyTicket {
    public static void main(String[] args) {
        BuyTicket station = new BuyTicket();
        new Thread(station,"苦逼的我").start();
        new Thread(station,"牛逼的你men").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();
            }
        }
    }

    public void stop() {
        this.flag = false;
    }

    //================================同步方法  synchronized ===============================
    private synchronized void buy() throws InterruptedException {
        if (ticketNums <= 0) {
            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,"girlfriend");
        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
// ===================synchronized 預設鎖定是 this,但在這裡我們應該鎖Account==============
    public /*synchronized*/ void run() {
        //鎖 變化的量
       synchronized (account){
           if (account.money - drawingMoney < 0) {
               System.out.println(Thread.currentThread().getName() + "錢不夠了,取不了");
               return;
           }
           try {
               Thread.sleep(1000);
           } 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 class UnsafeList {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(() -> {
//                鎖 list
               synchronized (list){
                   list.add(Thread.currentThread().getName());
               }
            }).start();
        }
        Thread.sleep(3000);
        System.out.println(list.size()); //9998
    }
}

5.3 死鎖

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

死鎖情況模擬

public class DeaLock {

    public static void main(String[] args) {
        Makeup g1 = new Makeup(0, "灰姑涼");
        Makeup g2 = new Makeup(1, "白雪公主");

        g1.start();
        g2.start();
    }
}

class Lipstick {

}

class Mirror {

}

class Makeup extends Thread {

    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 + "獲得口紅的鎖");
                Thread.sleep(1000);

                synchronized (mirror) {
                    System.out.println(this.girlName + "獲得鏡子的鎖");
                }

            }
        } else {
            synchronized (mirror) {
                System.out.println(this.girlName + "獲得鏡子的鎖");
                Thread.sleep(1000);

                synchronized (lipstick) {
                    System.out.println(this.girlName + "獲得鏡口號的鎖");
                }

            }
        }
    }
}

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

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

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

5.4 Lock(鎖)

  • 從 JDK5.0 開始,Java提供了更加強大的執行緒同步機制 —— 通過顯示定義同步鎖物件來實現同步。同步鎖使用Lock物件充當
  • java.util.concurrent.locks.Lock介面時控制多個執行緒對共享資源進行訪問的工具。鎖提供了對共享資源的獨佔訪問,每次只能有一個執行緒對Lock物件加鎖,執行緒開始訪問共享資源之前應該先獲得Lock物件
  • ReentrantLock(可重用鎖)類實現了Lock,它擁有與synchronized相同的併發性和記憶體語義,在實現執行緒安全的控制中,比較常用的是ReentrantLock,可以顯式加鎖,釋放鎖
public class TestLock {
    public static void main(String[] args) {
        TestLock2 lock2 = new TestLock2();

        new  Thread(lock2).start();
        new  Thread(lock2).start();
        new  Thread(lock2).start();
    }
}

class TestLock2 implements Runnable {
    int ticketNums = 10;

//    定義 lock 鎖
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true){
           try {
//               加鎖
               lock.lock();
               if(ticketNums > 0){
                   try {
                       Thread.sleep(1000);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   System.out.println(ticketNums--);
               }else{
                   break;
               }
           }finally {
//               解鎖。如果同步程式碼塊有異常,要將 unlock() 寫入finally 語句塊
                lock.unlock();
           }
        }
    }
}

synchronized 與 Lock 的對比

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

6. 執行緒通訊

生產者消費者模式

  • 應用場景:生產著和消費者問題
    • 假設倉庫中只能存放一件產品,生產者將生產出來的產品放入倉庫,消費者將倉庫中的產品取走消費
    • 如果倉庫中沒有產品,則生產者將產品放入倉庫,否則停止生產並等待,直到倉庫中的產品被消費者取走為止
    • 如果倉庫中放有產品,則消費者可以將產品取走消費,否則停止消費並等待,直到倉庫中再次放入產品為止

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

  • 對於生產者,沒有生產產品之前,要通知消費者等待,而生產了產品之後,又需要馬上通知消費者消費
  • 對於消費者,在消費之後,要通知生產者已經結束消費,需要生產新的產品以供消費
  • 在生產者消費問題中,僅有 synchronized 是不夠的
    • synchronized 是 可阻止併發更新同一個共享資源,實現了同步
    • synchronized 不能用來實現不同執行緒之前的訊息傳遞(通訊)

在這裡插入圖片描述

6.1 解決方法一:

在這裡插入圖片描述

public class TestPc {
    public static void main(String[] args) {
        SynContainer synContainer = new SynContainer();
        new Productor(synContainer).start();
        new Consumer(synContainer).start();
    }

}

//生產者
class Productor extends Thread{

    SynContainer container;

    public Productor(SynContainer synContainer){
        this.container = synContainer;
    }

    @Override
    public void run() {

        for (int i = 0; i < 100; i++) {
            container.push(new Chicken(i));
            System.out.println("生產了"+i+"只雞");
        }
    }
}
//消費者
class Consumer extends Thread{
    SynContainer container;

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

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

    }
}

class Chicken{
//    產品編號
    int id;

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

class SynContainer{
//    需要一個容器大小
    Chicken[] chickens = new Chicken[10];

    int count = 0;

//    生產者放入產品
    public synchronized void push(Chicken chicken){
//        如果容器滿了,就需要等待消費著消費
        if(count == chickens.length){
//            通知消費者消費,生產等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
//        如果沒有滿,我們就需要丟入產品
        chickens[count++] = chicken;
//         可以通知消費者消費了
        this.notifyAll();
    }

//    消費者消費產品
    public synchronized Chicken pop(){
//        判斷是否能消費
        if(count == 0){
//            不能消費,等待生產者生產產品
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
//        如果可以消費,就消費
        count--;
        Chicken chicken = chickens[count];
//        吃完了,通知生產者生產
        this.notifyAll();
        return chicken;
    }
}

6.2 解決方法二:

在這裡插入圖片描述

public class TestPc2 {
    public static void main(String[] args) {
        TV tv = new TV();
        new Player(tv).start();
        new Watcher(tv).start();
    }

}

//生產者 --》 演員
class Player extends Thread {
    TV tv;

    public Player(TV tc) {
        this.tv = tc;
    }

    @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++) {
            this.tv.watch();
        }

    }
}

//產品 --》 節目
class TV {
    //    演員表演,觀眾等待 T
//    觀眾觀看,演員等待  F
    String voice; //表演的節目
    boolean flag = true;

    //    表演
    public synchronized void play(String voice) {
        if (!flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("演員表演了:" + voice);
//        通知觀眾觀看
        this.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;
        }
    }
}

6.3 執行緒池

使用執行緒池

  • 背景:經常建立和銷燬,使用量特別大的資源,比如併發情況下的執行緒,對效能影響很大
  • 思路:提前建立好多個執行緒,放入執行緒池中,使用時直接獲取,使用完放回池中。可以避免頻繁建立銷燬,實現重複利用。類似生活中的公共交通工具
  • 好處
    • 提高響應速度(減少了建立新執行緒的時間)
    • 降低資源消耗(重複利用執行緒池中執行緒,不需要每次都建立)
    • 便於執行緒管理
      • corePoolSize:核心池的大小
      • maximumPoolSize:最大執行緒數
      • keepAliveTime:執行緒沒有任務時最多保持多長時間後會終止

使用執行緒池

  • JDK5.0 起提供了執行緒池相關API:ExecutorService 和 Executors
  • ExecutorService:真正的執行緒池介面。常見子類ThreadPoolExecutor
    • void execute(Runnable command):執行任務/命令,沒有返回值,一般用來執行Runnable
    • <T>Future<T> submit(Callable<T> task):執行任務,有返回值,一般用來執行Callable
    • void shutdown():關閉連線池
  • Executors:工具類,執行緒池的工廠類,用於建立並返回不同型別的執行緒池
public class TestPool {
    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());
//      3.關閉連線
        service.shutdownNow();

    }
}

class MyThread implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

相關文章