1. 程式人生 > 實用技巧 >這份java多執行緒筆記,你真得好好看看,我還沒見過總結的這麼全面的

這份java多執行緒筆記,你真得好好看看,我還沒見過總結的這麼全面的

1.執行緒,程序和多執行緒

1.程式:指指令和資料的有序集合,其本身沒有任何意義,是一個靜態的概念
2.程序:指執行程式的一次執行過程,是一個動態的概念。是系統資源分配的單位(注意:很多多執行緒是模擬出來的,真正的多執行緒是指有多個cpu,即多核,如伺服器。即在一個cpu的情況下,在同一時間點,cpu只能執行一個程式碼,因為切換的很快,所以就有同時執行的錯覺)
3.執行緒:通常一個程序中可以包含若干個執行緒,一個程序中至少有一個執行緒。執行緒是cpu排程和執行的單位
4.並行:指在同一時刻,有多條指令在多個處理器上同時執行。所以無論從微觀還是從巨集觀來看,二者都是一起執行的。
5.併發:指在同一時刻只能有一條指令執行,但多個程序指令被快速的輪換執行,使得在巨集觀上具有多個程序同時執行的效果,但在微觀上並不是同時執行的,只是把時間分成若干段,使多個程序快速交替的執行。

核心概念

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

2.執行緒建立

三種建立方式:

  • Thread class====>繼承Thread類(重點)
  • Runnable介面====>實現Runnable介面(重點)
  • Callable介面====>實現Callable介面(瞭解)

Thread

  • 自定義執行緒類繼承Thread類
  • 重寫run()方法,編寫執行緒執行體
  • 建立執行緒物件,呼叫start()方法啟動執行緒
    不建議使用:避免OPP單繼承侷限性
//建立方式一:繼承Thread類,重寫run方法,呼叫start開啟執行緒

public class TestThread1 extends Thread{
    @Override
    public void run() {
        //run方法執行緒體
        for (int i = 0; i < 200; i++) {
            System.out.println("我在打遊戲-------"+i);
        }
    }

    public static void main(String[] args) {
        //main執行緒,主執行緒

        //建立一個執行緒物件
        TestThread1 testThread1 = new TestThread1();

        //呼叫start方法,開啟執行緒
        testThread1.start();

        for (int i = 0; i < 200; i++) {
            System.out.println("我在打飛機遊戲-------"+i);
        }
    }
}

最後會發現“我在打遊戲”和“我在打飛機遊戲”會交替執行。
注意:執行緒不一定立即執行,由cpu安排排程;

實現Runnable

推薦使用Runnable,避免單繼承的侷限性,方便同一個物件被多個執行緒使用

  • 定義MyRunnable類事項Runnable介面
  • 實現run()方法,編寫執行緒執行體
  • 建立執行緒物件,呼叫start()方法啟動執行緒
    買票案例
//票數
    //多個執行緒同時操作同一個物件
//買火車票

//發現問題,多個執行緒操作同一個資源的情況下,執行緒不安全,資料紊亂
public class TestThread04 implements Runnable{

    //票數
    private int ticketNums = 10;

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

    public static void main(String[] args) {
        TestThread04 ticket = new TestThread04();

        new Thread(ticket,"小明").start();
        new Thread(ticket,"老師").start();
        new Thread(ticket,"黃牛").start();
    }
}

老師拿到了第10票
小明拿到了第9票
黃牛拿到了第9票
老師拿到了第8票
小明拿到了第6票
黃牛拿到了第7票
黃牛拿到了第5票
老師拿到了第4票
小明拿到了第4票
黃牛拿到了第2票
小明拿到了第1票
老師拿到了第3票

可以發現,多個執行緒操作同一資源可能存在併發性問題

龜兔賽跑案例

//模擬龜兔賽跑
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 is "+winner);
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Race race = new Race();

        new Thread(race,"兔子").start();
        new Thread(race,"烏龜").start();
    }
}

實現Callable介面(瞭解即可)

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

下載案例

//執行緒建立方式三:實現callable介面
public class TestCallable implements Callable<Boolean> {

    private String url;//網路圖片地址
    private String name;//儲存檔名

    public TestCallable(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 {
        TestCallable t1 = new TestCallable("https://i.guancha.cn/bbs/2020/07/10/20200710142345269.jpg?imageView2/2/w/500/format/jpg","1.jpg");
        TestCallable t2 = new TestCallable("https://i.guancha.cn/bbs/2020/07/10/20200710142345269.jpg?imageView2/2/w/500/format/jpg","2.jpg");
        TestCallable t3 = new TestCallable("https://i.guancha.cn/bbs/2020/07/10/20200710142345269.jpg?imageView2/2/w/500/format/jpg","3.jpg");

        //建立執行服務
        ExecutorService ser= Executors.newFixedThreadPool(3);
        //提交執行
        Future<Boolean> r1 = ser.submit(t1);
        Future<Boolean> r2 = ser.submit(t2);
        Future<Boolean> r3 = ser.submit(t3);
        //獲取結果
        boolean rs1 = r1.get();
        boolean rs2 = r2.get();
        boolean rs3 = r3.get();

        System.out.println(rs1);
        System.out.println(rs2);
        System.out.println(rs3);

        //關閉服務
        ser.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方法出現問題");
            }
        }
    }
}

3.Lamda表示式

  • λ希臘字母表中排序第十一為的字母,英語名稱為Lambda
  • 避免匿名內部類定義過多
  • 其實質屬於函數語言程式設計的概念
  • jdk1.8後新增的一種技術
(params)->expression[表示式]
  (params)->statement[語句]
    (params)->{statements}

a->System.out.println("i like lambda-->"+a);

new Thread(()->System.out.println("多執行緒學習。。。")).start();

為什麼要使用lambda表示式

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

函式式介面的定義

  • 任何介面,如果只包含唯一一個抽象方法,那麼他就是一個函式式介面。

    	public interface Runnable{
    			public abstract void run();
    	}
    
    
  • 對於函式式介面,我們可以通過lambda表示式來建立該介面的物件。

推到lambda表示式

/*
推到lambda表示式
 */

public class TestLambda1 {

    //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();

        //4.區域性內部類
        class Like3 implements ILike{
            @Override
            public void lambda() {
                System.out.println("i like lambda3");
            }
        }

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

        //5.匿名內部類,沒有類的名稱,必須藉助介面或者父類
        like = new ILike() {
            @Override
            public void lambda() {
                System.out.println("i like lambda4");
            }
        };
        like.lambda();

        //6.用lambda簡化
        ILike like1 = ()->{
            System.out.println("i like lambda6");
        };
        like1.lambda();
    }
}

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

//2.實現類
class Like implements ILike{

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

案列二

public class TestLambda2 {
    static class Love implements ILove{

        @Override
        public void love(int a) {
            System.out.println("i love u"+a);
        }
    }

    public static void main(String[] args) {
        //1.lambda表示簡化
        ILove love = (int a) ->{
                System.out.println("i love u"+a);
        };

        //簡化1,去掉引數型別
        love = (a)->{
            System.out.println("i love u"+a);
        };

        //簡化2,簡化括號
        love = a->{
            System.out.println("i love u"+a);
        };

        //簡化3,去掉花括號
        love = a -> System.out.println("i love u"+a);

        //總結:
            //lambda表示式只能有一行程式碼的情況下才能簡化成為一行,如果有多行,那麼就用程式碼塊包裹
            //前提介面為函式式介面
            //多個引數也可以去掉引數型別,要去掉就都去掉,必須加上括號

        love.love(521);

    }
}

interface ILove{
    void love(int a);
}

4.靜態代理

  • 你:真實角色
  • 婚慶公司:代理你,幫你處理結婚的事
  • 結婚:實現都實現結婚介面即可

案列

public class StaticProxy {

    public static void main(String[] args) {
        You you =new You();//你要結婚

        new Thread( ()-> System.out.println("我愛你")).start();

        new WeddingCompany(new You()).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("結婚之前,佈置現場");
    }
}

5.執行緒狀態

五大狀態

執行緒停止

setPriority(int newPriority): 更改執行緒的優先順序

  • static void sleep(long millis): 在指定的毫秒數內讓當前正在執行的執行緒休眠
  • void join():等待該執行緒終止
  • static void yield():暫停當前正在執行的執行緒物件,並執行其他執行緒
  • void interrupt():中斷執行緒,別用這個方式
  • boolean isAlive():測試執行緒是否處於活動狀態

1.不推薦使用JDK提供的stop(),destroy()方法。【已廢棄】

2.推薦執行緒自己停止下來

3.建議使用一個標誌位進行終止變數當flag=false,則終止執行緒執行。

//測試stop
//1.建議執行緒正常停止-->利用次數,不建議死迴圈
//2.建議使用標誌位-->設定一個標誌位
//3.不要使用stop或者destroy等過時或者JDK不建議使用的方法
public class TestStop 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) {
        TestStop testStop = new TestStop();

        new Thread(testStop).start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("main.."+i);
            if (i == 900){
                //呼叫stop方法,切換標誌位,讓執行緒停止
                testStop.stop();
                System.out.println("執行緒該停止了");
            }
        }
    }
}

執行緒休眠

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

用sleep方法模擬倒計時和列印當前時間

//模擬倒計時
public class TestSleep2 {

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

    //模擬倒計時
    public static void tenDown() throws InterruptedException {
        int num = 10;

        while (true){
            Thread.sleep(1000);
            System.out.println(num--);
            if (num<=0){
                break;
            }
        }
    }

}

執行緒禮讓

  • 禮讓執行緒,讓當前正在執行的執行緒暫停,但不阻塞
  • 將執行緒從執行狀態轉為就緒狀態
  • 讓cpu重新排程,禮讓不一定成功!看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()+"執行緒停止執行");
    }
}

Join

  • Join合併執行緒,待此執行緒執行完成後,再執行其他執行緒,其他執行緒阻塞
  • 可以想象成插隊
//測試join方法//想象為插隊
public class TestJoin  implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < 100; i++) {
            System.out.println("執行緒vip來了"+i);
        }
    }
    public static void main(String[] args) throws InterruptedException {

        //啟動我們的執行緒
        TestJoin testJoin = new TestJoin();
        Thread thread = new Thread(testJoin);
        thread.start();

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

執行緒狀態觀測

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

  • NEW:尚未啟動的執行緒處於此狀態
  • RUNNABLE:在Java虛擬機器中執行的執行緒處於此狀態。
  • BLOCKED:被阻塞等待監視器鎖定的執行緒處於此狀態
  • WAITING:正在等待另一個執行緒執行特定動作的執行緒處於此狀態
  • TIMED_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);//NEW

        //觀察啟動後
        thread.start();
        state = thread.getState();
        System.out.println(state);//Runnable

        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;
  • 使用以下方式改變或獲取優先順序
    • getPriority() setPriority(int xxx)

優先順序的設定建議再start()排程前
優先順序低知識意味著獲得排程的概率低,並不是優先順序低就不會被呼叫了,這都是看cpu的排程。

6.守護(daemon)執行緒

  • 執行緒分為使用者執行緒守護執行緒
  • 虛擬機器必須確保使用者執行緒執行完畢
  • 虛擬機器不用等待守護執行緒執行完畢
  • 如,後臺記錄操作日誌,監控記憶體,垃圾回收等待
//測試守護執行緒
//上帝守護你
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("你一生都開心的活著");
        }

        System.out.println("======goodbye!world============");
    }
}

7.執行緒安全

執行緒同步

多個執行緒操作同一個資源
併發:同一個物件多個執行緒同時操作

執行緒同步:

  • 現實生活中,我們會遇到“同一個資源,多個人都想使用”的問題,比如,食堂排隊打飯,每個人都想吃飯,最天然的解決辦法就是,排隊,一個個來。
  • 處理多執行緒問題是,多個執行緒訪問同一個物件,並且某些執行緒還想修改這個物件,著時候我們就需要執行緒同步,執行緒同步其實就是一種等待機制,多個需要同時訪問此物件的執行緒進入這個物件的等待池形成佇列,等待前面執行緒使用完畢,下一個執行緒再使用

形成條件:佇列+鎖

鎖:

  • 由於同一程序的多個執行緒共享同一塊儲存空間,在帶來方便的同時,也帶來了訪問衝突問題,為了保證資料在方法中被訪問時的正確性,在訪問時加入鎖機制synchronized,當一個執行緒獲得物件的排它鎖,獨佔資源,其他執行緒必須等待,使用後釋放鎖即可,存在以下問題:
    • 一個執行緒持有鎖會導致其他所有需要此鎖的執行緒掛起;
    • 在多執行緒競爭下,加鎖,釋放鎖會導致比較多的上下文切換和排程延時,引起效能問題;
    • 如果一個優先順序高的執行緒等待一個優先順序低的執行緒釋放鎖,會導致優先順序倒置,引起效能問題。

三大執行緒不安全案例

買票不安全

//不安全的買票
//執行緒不安全,有負數
public class UnsafeByyTicket {

    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;
    private 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,"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;
        }

        //sleep可以放大問題的發生性
        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()+Thread.currentThread().getName());
        System.out.println(this.getName()+"手裡的錢"+nowMoney);
    }
}

不安全的集合

//執行緒不安全的集合
public class UnsafeList {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        Thread.sleep(5000);
        System.out.println(list.size());
    }
}

同步方法(synchronized)

  • 由於我們可以通過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 UnsafeBank {

    public static void main(String[] args) {
        Account account = new Account(1000,"結婚基金");

        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;

    }

    //取錢
    //synchronized預設鎖的是this
    @Override
    public void run() {
        synchronized (account){

            //判斷有沒有錢
            if (account.money-drawingMoney <0){
                System.out.println(Thread.currentThread().getName()+"錢不夠,取不了");
                return;
            }

            //sleep可以放大問題的發生性
            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()+Thread.currentThread().getName());
            System.out.println(this.getName()+"手裡的錢"+nowMoney);
        }

    }
}

//不安全的買票
//執行緒不安全,有負數
public class UnsafeByyTicket {

    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;
    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 (ticketNums <= 0){
            flag = false;
            return;
        }

        //買票
        System.out.println(Thread.currentThread().getName()+"拿到"+ticketNums--);
    }
}

//執行緒不安全的集合
public class UnsafeList {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                synchronized (list){
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
        Thread.sleep(3000);
        System.out.println(list.size());
    }
}

JUC安全型別的集合

//測試JUC安全型別的集合
public class TestJUC {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
        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());
    }
}

死鎖

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

死鎖避免方法

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

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

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

8.Lock(鎖)

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

synchronized與Lock的對比

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

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

class TestLock2 implements Runnable{

    int ticketNums = 1000;

    //定義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();
            }

        }
    }
}

9.執行緒協作

生產者消費者模式

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

執行緒通訊-分析

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

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

執行緒通訊

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

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

解決方式1

併發協作模型“生產者/消費者模式”—>管程法

  • 生產者:負責生產資料的模組(可能是方法,物件,執行緒,程序)
  • 消費者:負責處理資料的模組(可能是方法,物件,執行緒,程序)
  • 緩衝區:消費者不能直接使用生產者的資料,他們之間有個“緩衝區”
    生產者將生產好的資料放入緩衝區,消費者從緩衝區拿出資料
//測試:生產者消費者模型--》利用緩衝區解決:管程法

//生產者,消費者,產品,緩衝區
public class TestPC {
    public static void main(String[] args) {
        SynContainer container = new SynContainer();

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

//生產者
class Productor extends Thread{
    SynContainer container;

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

    //生產
    @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 container){
        this.container = container;
    }

    //消費
    @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;
        count++;

        //可以通知消費者消費了
        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;
    }
}

解決方式2

  • 併發協作模型“生產者/消費者模式”–>訊號燈法
package com.kuang.gaoji;

//測試生產者消費者問題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 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;

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

10.執行緒池

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

使用執行緒池

  • JDK5.0起提供了執行緒池相關API:ExecutorService和Executors
  • ExecutorService:真正的執行緒池介面。常見子類ThreadPoolExecutor
    • void execute(Runnable command):執行任務/命令,沒有返回值,一般用來執行Runnable
    • Futuresubmit(Callabletask):執行任務,有返回值,一般用來執行Callable
    • void shutdown():關閉執行緒池
  • Executors:工具類、執行緒池的工廠類,用於建立並返回不同型別的執行緒池。
//測試執行緒池
public class TestPool {

    public static void main(String[] args) {
        //1.建立執行緒池
        //newFixedThreadPool 引數為:執行緒池大小
        ExecutorService service = Executors.newFixedThreadPool(10);

        //執行
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());

        //2.關閉連結
        service.shutdown();
    }
}

class MyThread implements Runnable{

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

11.總結

//回顧總結執行緒的建立
public class ThreadNew {
    public static void main(String[] args) {
        new MyThread1().start();

        new Thread(new MyThread2()).start();

        FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyThread3());
        new Thread(futureTask).start();

        try {
            Integer integer = futureTask.get();
            System.out.println(integer);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}

//1.繼承Thread類
class MyThread1 extends Thread{
    @Override
    public void run(){
        System.out.println("MyThread1");
    }
}

//2.實現Runnable介面
class MyThread2 implements Runnable{

    @Override
    public void run() {
        System.out.println("MyThread2");
    }
}

//3.實現Callable介面
class MyThread3 implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {

        System.out.println("MyThread3");
        return 100;
    }
}

最後

感謝你看到這裡,看完有什麼的不懂的可以在評論區問我,覺得文章對你有幫助的話記得給我點個贊,每天都會分享java相關技術文章或行業資訊,歡迎大家關注和轉發文章!