ch17_multithreading_basics

小兵学习笔记發表於2024-09-13
  • 第17章 多執行緒基礎
    • 執行緒相關概念
      • 程式(program)
      • 程序
      • 執行緒
      • 其他相關概念
    • 執行緒基本使用
      • 建立執行緒的兩種方式
      • 執行緒應用案例1-繼承Thread 類
      • 執行緒應用案例2-實現Runnable 介面
      • 執行緒使用應用案例-多執行緒執行
      • 執行緒如何理解
    • 繼承Thread vs 實現Runnable 的區別
    • 執行緒終止
      • 基本說明
      • 應用案例
    • 執行緒常用方法
      • 常用方法第一組
      • 注意事項和細節
      • 常用方法第二組
      • 課堂練習
      • 使用者執行緒和守護執行緒
      • 應用案例
    • 執行緒的生命週期
      • 執行緒的幾種狀態
      • 執行緒狀態轉換圖!!!!
    • Synchronized
      • 執行緒同步機制
      • 同步具體方法-Synchronized
        • 方法一同步程式碼塊
        • 方法二方法宣告
    • 分析同步原理
    • 互斥鎖
      • 基本介紹
      • 注意事項和細節
    • 執行緒的死鎖
      • 基本介紹
      • 應用案例
    • 釋放鎖
      • 下面操作會釋放鎖
      • 下面操作不會釋放鎖
    • 本章作業

第17章 多執行緒基礎

執行緒相關概念

程式(program)

是為完成特定任務、用某種語言編寫的一組指令的集合。簡單的說:就是我們寫的程式碼。

程序

  1. 程序是指執行中的程式,比如我們使用QQ,就啟動了一個程序,作業系統就會為該程序分配記憶體空間。當我們使用迅雷,又啟動了一個程序,作業系統將為迅雷分配新的記憶體空間。
  2. 程序是程式的一次執行過程,或是正在執行的一個程式。是動態過程:有它自身的產生、存在和消亡的過程

執行緒

  1. 執行緒由程序建立的,是程序的一個實體

  2. 一個程序可以擁有多個執行緒,如下圖

其他相關概念

  1. 單執行緒:同一個時刻,只允許執行一個執行緒。

  2. 多執行緒:同一個時刻,可以執行多個執行緒,比如:一個qq程序,可以同時打
    開多個聊天視窗,一個迅雷程序,可以同時下載多個檔案。

  3. 併發:同一個時刻,多個任務交替執行,造成一種“貌似同時”的錯覺,簡單的說單核cpu實現的多工就是併發。

  1. 並行:同一個時刻,多個任務同時執行。多核cpu可以實現並行。

獲取cpu的數量/核心數

package com.hspedu;


public class CpuNum {
    public static void main(String[] args) {

        Runtime runtime = Runtime.getRuntime();
        //獲取當前電腦的cpu數量/核心數
        int cpuNums = runtime.availableProcessors();
        System.out.println("當前有cpu 個數=" + cpuNums);
    }
}

執行緒基本使用

建立執行緒的兩種方式

在java中執行緒來使用有兩種方法。

1.繼承Thread類,重寫run方法

2.實現Runnable介面,重寫run方法

執行緒應用案例1-繼承Thread 類

執行程式時就相當啟動了一個程序,進入main時就開啟了一個main執行緒,

1)請編寫程式,開啟一個執行緒,該執行緒每隔1秒。在控制檯輸出“哺瞄。我是小貓咪

2)對上題改進:當輸出80次啪瞄,我是小貓咪,結束該執行緒

3)使用JConsole 監控執行緒執行情況,並畫出程式示意圖!

在程序執行時直接在控制檯輸入JConsole即可。

主執行緒掛了但是子執行緒還在繼續執行,這並不會導致應用程式的結束。說明: 當main執行緒啟動一個子執行緒 Thread-0, 主執行緒不會阻塞, 會繼續執行(不會等執行完畢後再往下執行),這時 主執行緒和子執行緒是交替執行。

package com.hspedu.threaduse;

/**
 * 演示透過繼承 Thread 類建立執行緒
 */
public class Thread01 {
    public static void main(String[] args) throws InterruptedException {

        //建立Cat物件,可以當做執行緒使用
        Cat cat = new Cat();

        // 讀原始碼
        /*
            (1)
            public synchronized void start() {
                start0();
            }
            (2)
            //start0() 是本地方法,是JVM呼叫, 底層是c/c++實現
            //真正實現多執行緒的效果, 是start0(), 而不是 run
            private native void start0();

         */

        cat.start();// 啟動執行緒-> 最終會執行cat的run方法



        //cat.run();//run方法就是一個普通的方法, 沒有真正的啟動一個執行緒,就會把run方法執行完畢,才向下執行,因此要真正實現多執行緒,還是應該使用start方法。
        //說明: 當main執行緒啟動一個子執行緒 Thread-0, 主執行緒不會阻塞, 會繼續執行(不會等執行完畢後再往下執行),這時 主執行緒和子執行緒是交替執行。
        System.out.println("主執行緒繼續執行" + Thread.currentThread().getName());//名字main
        for(int i = 0; i < 60; i++) {
            System.out.println("主執行緒 i=" + i);
            //讓主執行緒休眠
            Thread.sleep(1000);
        }
    }
}

// 說明
//1. 當一個類繼承了 Thread 類, 該類就可以當做執行緒使用
//2. 我們會重寫 run方法,寫上自己的業務程式碼
//3. run Thread 類 實現了 Runnable 介面的run方法,如下

/*
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
 */



class Cat extends Thread {

    int times = 0;
    @Override
    public void run() {//重寫run方法,寫上自己的業務邏輯

        while (true) {
            //該執行緒每隔1秒。在控制檯輸出 “喵喵, 我是小貓咪”
            System.out.println("喵喵, 我是小貓咪" + (++times) + " 執行緒名=" + Thread.currentThread().getName());
            //讓該執行緒休眠1秒 ctrl+alt+t
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(times == 80) {
                break;//當times 到80, 退出while, 這時執行緒也就退出..
            }
        }
    }
}

start()方法呼叫start0()方法後,該執行緒並不一定會立馬執行,只是將執行緒變成了可執行狀態。具體什麼時候執行,取決於CPU,由CPU統一排程。

執行緒應用案例2-實現Runnable 介面

  1. java是單繼承的,在某些情況下一個類可能已經繼承了某個父類,這時在用繼承Thread類方法來建立執行緒顯然不可能了。
  2. java設計者們提供了另外一個方式建立執行緒,就是透過實現Runnable介面來建立執行緒

應用案例

請編寫程式,該程式可以每隔1秒。在控制檯輸出“hi!”,當輸出10次後,自動退出。請使用實現Runnable介面的方式實現。

這裡底層使用了設計模式[代理模式]=>程式碼模擬實現Runnable介面開發執行緒的機制

package com.hspedu.threaduse;

/**
 * 透過實現介面Runnable 來開發執行緒
 */
public class Thread02 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        //dog.start(); 這裡不能呼叫start
        //建立了Thread物件,把 dog物件(實現Runnable),放入Thread
        Thread thread = new Thread(dog);
        thread.start();

//        Tiger tiger = new Tiger();//實現了 Runnable
          // 1.
//        ThreadProxy threadProxy = new ThreadProxy(tiger);
          // 2.
//        threadProxy.start();
    }
}

class Animal {
}

class Tiger extends Animal implements Runnable {
    // 6.
    @Override
    public void run() {
        System.out.println("老虎嗷嗷叫....");
    }
}

//執行緒代理類 , 模擬了一個極簡的Thread類
class ThreadProxy implements Runnable {//你可以把Proxy類當做 ThreadProxy

    private Runnable target = null;//屬性,型別是 Runnable
    // 5.
    @Override
    public void run() {
        if (target != null) {
            target.run();//動態繫結(執行型別Tiger)
        }
    }

    public ThreadProxy(Runnable target) {
        this.target = target;
    }
    // 3.
    public void start() {
        start0();//這個方法時真正實現多執行緒方法
    }
    // 4.
    public void start0() {
        run();
    }
}


class Dog implements Runnable { //透過實現Runnable介面,開發執行緒

    int count = 0;

    @Override
    public void run() { //普通方法
        while (true) {
            System.out.println("小狗汪汪叫..hi" + (++count) + Thread.currentThread().getName());

            //休眠1秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if (count == 10) {
                break;
            }
        }
    }
}

執行緒使用應用案例-多執行緒執行

請編寫一個程式,建立兩個執行緒,一個執行緒每隔1秒輸出“hello,world”,輸出10次,退出, 一個執行緒每隔1秒輸出“hi”,輸出5次退出。

package com.hspedu.threaduse;

/**
 * main執行緒啟動兩個子執行緒
 */
public class Thread03 {
    public static void main(String[] args) {

        T1 t1 = new T1();
        T2 t2 = new T2();
        Thread thread1 = new Thread(t1);
        Thread thread2 = new Thread(t2);
        thread1.start();//啟動第1個執行緒
        thread2.start();//啟動第2個執行緒
    }
}

class T1 implements Runnable {

    int count = 0;

    @Override
    public void run() {
        while (true) {
            //每隔1秒輸出 “hello,world”,輸出10次
            System.out.println("hello,world " + (++count));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(count == 60) {
                break;
            }
        }
    }
}

class T2 implements Runnable {

    int count = 0;

    @Override
    public void run() {
        //每隔1秒輸出 “hi”,輸出5次
        while (true) {
            System.out.println("hi " + (++count));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(count == 50) {
                break;
            }
        }
    }
}

執行緒如何理解

繼承Thread vs 實現Runnable 的區別

  1. 從java的設計來看,透過繼承Thread或者實現Runnable介面來建立執行緒本質上沒有區別,從jdk幫助文件我們可以看到Thread類本身就實現了
    Runnable介面。

  2. 實現Runnable介面方式更加適合多個執行緒共享一個資源的情況,並且避免了單繼承的限制,建議使用Runnable。

  3. [售票系統],程式設計模擬三個售票視窗售票100,分別使用繼承 Thread和實現 Runnable方式,並分析有什麼問題? 均會出現超賣的問題。

package com.hspedu.ticket;

/**
 * 使用多執行緒,模擬三個視窗同時售票100張
 */
public class SellTicket {
    public static void main(String[] args) {

        //測試
//        SellTicket01 sellTicket01 = new SellTicket01();
//        SellTicket01 sellTicket02 = new SellTicket01();
//        SellTicket01 sellTicket03 = new SellTicket01();
//
//        //這裡我們會出現超賣..
//        sellTicket01.start();//啟動售票執行緒
//        sellTicket02.start();//啟動售票執行緒
//        sellTicket03.start();//啟動售票執行緒


        System.out.println("===使用實現介面方式來售票=====");
        SellTicket02 sellTicket02 = new SellTicket02();

        new Thread(sellTicket02).start();//第1個執行緒-視窗
        new Thread(sellTicket02).start();//第2個執行緒-視窗
        new Thread(sellTicket02).start();//第3個執行緒-視窗


    }
}

//使用Thread方式

class SellTicket01 extends Thread {

    private static int ticketNum = 100;//讓多個執行緒共享 ticketNum

    @Override
    public void run() {
        while (true) {

            if (ticketNum <= 0) {
                System.out.println("售票結束...");
                break;
            }

            //休眠50毫秒, 模擬
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("視窗 " + Thread.currentThread().getName() + " 售出一張票"
                    + " 剩餘票數=" + (--ticketNum));

        }
    }
}



//實現介面方式
class SellTicket02 implements Runnable {
    private int ticketNum = 100;//讓多個執行緒共享 ticketNum

    @Override
    public void run() {
        while (true) {

            if (ticketNum <= 0) {
                System.out.println("售票結束...");
                break;
            }

            //休眠50毫秒, 模擬
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("視窗 " + Thread.currentThread().getName() + " 售出一張票"
                    + " 剩餘票數=" + (--ticketNum));//1 - 0 - -1  - -2

        }
    }
}

執行緒終止

基本說明

  1. 當執行緒完成任務後,會自動退出。
  2. 還可以透過使用變數來控制run方法退出的方式停止執行緒,即通知方式。

應用案例

需求:啟動一個執行緒t,要求在main執行緒中去停止執行緒t,請程式設計實現。

package com.hspedu.exit_;

public class ThreadExit_ {
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T();
        t1.start();

        // 如果希望 main 執行緒去控制 t1 執行緒的終止, 必須可以修改 loop
        // 讓 t1 退出 run 方法,從而終止 t1 執行緒 -> 通知方式
        // 讓主執行緒休眠 10 秒,再通知 t1 執行緒退出
        System.out.println("main執行緒休眠10s...");
        Thread.sleep(10 * 1000);
        t1.setLoop(false);
    }
}

class T extends Thread {
    private int count = 0;
    // 設定一個控制變數
    private boolean loop = true;
    @Override
    public void run() {
        while (loop) {

            try {
                Thread.sleep(50);// 讓當前執行緒休眠50ms
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("T 執行中...." + (++count));
        }
    }
    public void setLoop(boolean loop) {
        this.loop = loop;
    }
}

執行緒常用方法

常用方法第一組

  1. setName A //設定執行緒名稱,使之與引數name相同

  2. getName //返回該執行緒的名稱

  3. startM //使該執行緒開始執行;Java虛擬機器底層呼叫該執行緒的start0方

  4. run //呼叫執行緒物件 run方法;

  5. setPriority //更改執行緒的優先順序

  6. getPriority //獲取執行緒的優先順序

  7. sleep //在指定的毫秒數內讓當前正在執行的執行緒休眠(暫停執行)

  8. interrupt //中斷執行緒

注意事項和細節

  1. start底層會建立新的執行緒,呼叫run, run 就是一個簡單的方法呼叫,不會啟動新執行緒。

  2. 執行緒優先順序的範圍。

  3. interrupt,中斷執行緒,但並沒有真正的結束執行緒。所以一般用於中斷正在休眠執行緒。

  4. sleep:執行緒的靜態方法,使當前執行緒休眠。

package com.hspedu.method;

public class ThreadMethod01 {
    public static void main(String[] args) throws InterruptedException {
        //測試相關的方法
        T t = new T();
        t.setName("timerring");
        t.setPriority(Thread.MIN_PRIORITY);//1
        t.start();//啟動子執行緒


        //主執行緒列印5 hi ,然後我就中斷 子執行緒的休眠
        for(int i = 0; i < 5; i++) {
            Thread.sleep(1000);
            System.out.println("hi " + i);
        }

        System.out.println(t.getName() + " 執行緒的優先順序 =" + t.getPriority());//1
        t.interrupt();//當執行到這裡,就會中斷 t執行緒的休眠.
    }
}

class T extends Thread { // 自定義的執行緒類
    @Override
    public void run() {
        while (true) {
            for (int i = 0; i < 100; i++) {
                // Thread.currentThread().getName() 獲取當前執行緒的名稱
                System.out.println(Thread.currentThread().getName() + "  吃包子~~~~" + i);
            }
            try {
                System.out.println(Thread.currentThread().getName() + " 休眠中~~~");
                Thread.sleep(20000);//20秒
            } catch (InterruptedException e) {
                // 當該執行緒執行到一個interrupt 方法時,就會catch 一個 異常, 可以加入自己的業務程式碼
                // InterruptedException 是捕獲到一箇中斷異常.
                System.out.println(Thread.currentThread().getName() + "被 interrupt了");
            }
        }
    }
}

常用方法第二組

  1. yield:執行緒的禮讓。讓出cpu,讓其他執行緒執行,但禮讓的時間不確定,所以也不一定禮讓成功
  2. join:執行緒的插隊。插隊的執行緒一旦插隊成功,則肯定先執行完插入的執行緒所有的任務。

案例:main執行緒建立一個子執行緒,每隔1s輸出hello,輸出20次,主執行緒每隔1秒, 輸出hi,輸出20次。要求:兩個執行緒同時執行,當主執行緒輸出5次後,就讓子執行緒執行完畢,主執行緒再繼續。

package com.hspedu.method;

public class ThreadMethod02 {
    public static void main(String[] args) throws InterruptedException {

        T2 t2 = new T2();
        t2.start();

        for(int i = 1; i <= 20; i++) {
            Thread.sleep(1000);
            System.out.println("主執行緒(小弟) 吃了 " + i  + " 包子");
            if(i == 5) {
                System.out.println("主執行緒(小弟) 讓 子執行緒(老大) 先吃");
                //join, 執行緒插隊
                //t2.join();// 這裡相當於讓t2 執行緒先執行完畢
                Thread.yield();//禮讓,不一定成功.
                System.out.println("執行緒(老大) 吃完了 主執行緒(小弟) 接著吃..");
            }
        }
    }
}

class T2 extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 20; i++) {
            try {
                Thread.sleep(1000);//休眠1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("子執行緒(老大) 吃了 " + i +  " 包子");
        }
    }
}

課堂練習

  1. 主執行緒每隔1s,輸出hi,一共10次
  2. 當輸出到hi5時,啟動一個子執行緒(要求實現Runnable),每隔1s輸出hello,等該執行緒輸出10次 hello後,退出
  3. 主執行緒繼續輸出hi,直到主執行緒退出.
  4. 如圖,完成程式碼其實執行緒插隊
package com.hspedu.method;

public class ThreadMethodExercise {
    public static void main(String[] args) throws InterruptedException {
        Thread t3 = new Thread(new T3());//建立子執行緒
        for (int i = 1; i <= 10; i++) {
            System.out.println("hi " + i);
            if(i == 5) {//說明主執行緒輸出了5次 hi
                t3.start();//啟動子執行緒 輸出 hello...
                t3.join();//立即將t3子執行緒,插入到main執行緒,讓t3先執行
            }
            Thread.sleep(1000);//輸出一次 hi, 讓main執行緒也休眠1s
        }
    }
}

class T3 implements Runnable {
    private int count = 0;

    @Override
    public void run() {
        while (true) {
            System.out.println("hello " + (++count));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (count == 10) {
                break;
            }
        }
    }
}

使用者執行緒和守護執行緒

  1. 使用者執行緒: 也叫工作執行緒,當執行緒的任務執行完或通知方式結束。
  2. 守護執行緒: 一般是為工作執行緒服務的,當所有的使用者執行緒結束,守護執行緒自動結束。
  3. 常見的守護執行緒: 垃圾回收機制

應用案例

下面我們測試如何將一個執行緒設定成守護執行緒。

只需要將 myDaemonThread.setDaemon(true); 設定為 true 即可。

package com.hspedu.method;

public class ThreadMethod03 {
    public static void main(String[] args) throws InterruptedException {
        MyDaemonThread myDaemonThread = new MyDaemonThread();
        //如果我們希望當main執行緒結束後,子執行緒自動結束,只需將子執行緒設為守護執行緒即可
        myDaemonThread.setDaemon(true);
        myDaemonThread.start();

        for( int i = 1; i <= 10; i++) {//main執行緒
            System.out.println("工作...");
            Thread.sleep(1000);
        }
    }
}

class MyDaemonThread extends Thread {
    public void run() {
        for (; ; ) {//無限迴圈
            try {
                Thread.sleep(1000);//休眠1000毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("聊天");
        }
    }
}

執行緒的生命週期

執行緒的幾種狀態

JDK 中用Thread.State 列舉表示了執行緒的幾種狀態

執行緒狀態轉換圖!!!!

有些書中說一共有7個狀態,實際上就是將Runnable狀態中Ready和Running分開了。到底是否執行還是取決於核心態的排程情況。

package com.hspedu.state_;

public class ThreadState_ {
    public static void main(String[] args) throws InterruptedException {
        T t = new T();
        System.out.println(t.getName() + " 狀態 " + t.getState());
        t.start();

        while (Thread.State.TERMINATED != t.getState()) {
            System.out.println(t.getName() + " 狀態 " + t.getState());
            Thread.sleep(500);
        }

        System.out.println(t.getName() + " 狀態 " + t.getState());

    }
}

class T extends Thread {
    @Override
    public void run() {
        while (true) {
            for (int i = 0; i < 10; i++) {
                System.out.println("hi " + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            break;
        }
    }
}

Synchronized

執行緒同步機制

  1. 在多執行緒程式設計,一些敏感資料不允許被多個執行緒同時訪問,此時就使用同步訪問技術,保證資料在任何同一時刻,最多有一個執行緒訪問,以保證資料的完整性。
  2. 也可以這裡理解:執行緒同步,即當有一個執行緒在對記憶體進行操作時,其他執行緒都不可以對這個記憶體地址進行操作,直到該執行緒完成操作,其他執行緒才能對該記憶體地址進行操作。

同步具體方法-Synchronized

方法一同步程式碼塊

synchronized (物件){ // 得到物件的鎖,才能操作同步程式碼
	// 需要被同步程式碼;
}

方法二方法宣告

synchronized 還可以放在方法宣告中,表示整個方法-為同步方法

public synchronized void m (String name){
	//需要被同步的程式碼
}

分析同步原理

互斥鎖

基本介紹

  1. Java語言中,引入了物件互斥鎖的概念,來保證共享資料操作的完整性。
  2. 每個物件都對應於一個可稱為“互斥鎖”的標記,這個標記用來保證在任一時刻,只能有一個執行緒訪問該物件。
  3. 關鍵字synchronized來與物件的互斥鎖聯絡。當某個物件用synchronized修飾時,表明該物件在任一時刻只能由一個執行緒訪問。
  4. 同步的侷限性:導致程式的執行效率要降低。
  5. 同步方法(非靜態的)的鎖可以是this,也可以是其他物件(要求是同一個物件)。
  6. 同步方法(靜態的)的鎖為當前類本身。即類.class
package com.hspedu.syn;

/**
 * 使用多執行緒,模擬三個視窗同時售票100張
 */
public class SellTicket {
    public static void main(String[] args) {

        //測試
//        SellTicket01 sellTicket01 = new SellTicket01();
//        SellTicket01 sellTicket02 = new SellTicket01();
//        SellTicket01 sellTicket03 = new SellTicket01();
//
//        //這裡我們會出現超賣..
//        sellTicket01.start();//啟動售票執行緒
//        sellTicket02.start();//啟動售票執行緒
//        sellTicket03.start();//啟動售票執行緒


//        System.out.println("===使用實現介面方式來售票=====");
//        SellTicket02 sellTicket02 = new SellTicket02();
//
//        new Thread(sellTicket02).start();//第1個執行緒-視窗
//        new Thread(sellTicket02).start();//第2個執行緒-視窗
//        new Thread(sellTicket02).start();//第3個執行緒-視窗

        //測試一把
        SellTicket03 sellTicket03 = new SellTicket03();
        new Thread(sellTicket03).start();//第1個執行緒-視窗
        new Thread(sellTicket03).start();//第2個執行緒-視窗
        new Thread(sellTicket03).start();//第3個執行緒-視窗

    }
}


//實現介面方式, 使用synchronized實現執行緒同步
class SellTicket03 implements Runnable {
    private int ticketNum = 100;//讓多個執行緒共享 ticketNum
    private boolean loop = true;//控制run方法變數
    Object object = new Object();


    //同步方法(靜態的)的鎖為當前類本身
    //1. public synchronized static void m1() {} 鎖是加在 SellTicket03.class 上
    //2. 如果在靜態方法中,實現一個同步程式碼塊.
    /*
        synchronized (SellTicket03.class) {
            System.out.println("m2");
        }
     */
    public synchronized static void m1() {

    }
    public static  void m2() {
        synchronized (SellTicket03.class) {
            System.out.println("m2");
        }
    }

    //1. public synchronized void sell() {} 就是一個同步方法
    //2. 這時鎖在 this物件
    //3. 也可以在程式碼塊上寫 synchronize ,同步程式碼塊, 互斥鎖還是在this物件
    public /*synchronized*/ void sell() { //同步方法, 在同一時刻, 只能有一個執行緒來執行sell方法

        synchronized (/*this*/ object) {
            if (ticketNum <= 0) {
                System.out.println("售票結束...");
                loop = false;
                return;
            }

            //休眠50毫秒, 模擬
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("視窗 " + Thread.currentThread().getName() + " 售出一張票"
                    + " 剩餘票數=" + (--ticketNum));//1 - 0 - -1  - -2
        }
    }

    @Override
    public void run() {
        while (loop) {

            sell();//sell方法是一共同步方法
        }
    }
}

//使用Thread方式
// new SellTicket01().start()
// new SellTicket01().start(); 物件不是同一個,鎖不住m1()

class SellTicket01 extends Thread {

    private static int ticketNum = 100;//讓多個執行緒共享 ticketNum

    // 以下寫法沒用,因為每次物件都不是同一個,鎖不住
//    public void m1() {
//        synchronized (this) {
//            System.out.println("hello");
//        }
//    }

    @Override
    public void run() {


        while (true) {

            if (ticketNum <= 0) {
                System.out.println("售票結束...");
                break;
            }

            //休眠50毫秒, 模擬
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("視窗 " + Thread.currentThread().getName() + " 售出一張票"
                    + " 剩餘票數=" + (--ticketNum));

        }
    }
}


//實現介面方式
class SellTicket02 implements Runnable {
    private int ticketNum = 100;//讓多個執行緒共享 ticketNum

    @Override
    public void run() {
        while (true) {

            if (ticketNum <= 0) {
                System.out.println("售票結束...");
                break;
            }

            //休眠50毫秒, 模擬
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("視窗 " + Thread.currentThread().getName() + " 售出一張票"
                    + " 剩餘票數=" + (--ticketNum));//1 - 0 - -1  - -2

        }
    }
}

注意事項和細節

  1. 同步方法如果沒有使用static修飾:預設鎖物件為this
  2. 如果方法使用static修飾,預設鎖物件:當前類.class
  3. 實現的落地步驟:
    • 需要先分析上鎖的程式碼
    • 選擇同步程式碼塊或同步方法(同步的範圍越小,當然效率也就越高)
    • 要求多個執行緒的鎖物件為同一個即可!

執行緒的死鎖

基本介紹

多個執行緒都佔用了對方的鎖資源,但不肯相讓,導致了死鎖,在程式設計是一定要避免死鎖的發生。

應用案例

package com.hspedu.syn;

/**
 * 模擬執行緒死鎖
 */
public class DeadLock_ {
    public static void main(String[] args) {
        //模擬死鎖現象
        DeadLockDemo A = new DeadLockDemo(true);
        A.setName("A執行緒");
        DeadLockDemo B = new DeadLockDemo(false);
        B.setName("B執行緒");
        A.start();
        B.start();
    }
}


//執行緒
class DeadLockDemo extends Thread {
    static Object o1 = new Object();// 保證多執行緒,共享一個物件,這裡使用static
    static Object o2 = new Object();
    boolean flag;

    public DeadLockDemo(boolean flag) {//構造器
        this.flag = flag;
    }

    @Override
    public void run() {

        //下面業務邏輯的分析
        //1. 如果flag 為 T, 執行緒A 就會先得到/持有 o1 物件鎖, 然後嘗試去獲取 o2 物件鎖
        //2. 如果執行緒A 得不到 o2 物件鎖,就會Blocked
        //3. 如果flag 為 F, 執行緒B 就會先得到/持有 o2 物件鎖, 然後嘗試去獲取 o1 物件鎖
        //4. 如果執行緒B 得不到 o1 物件鎖,就會Blocked
        if (flag) {
            synchronized (o1) { //物件互斥鎖, 下面就是同步程式碼
                System.out.println(Thread.currentThread().getName() + " 進入1");
                synchronized (o2) { // 這裡獲得li物件的監視權
                    System.out.println(Thread.currentThread().getName() + " 進入2");
                }
                
            }
        } else {
            synchronized (o2) {
                System.out.println(Thread.currentThread().getName() + " 進入3");
                synchronized (o1) { // 這裡獲得li物件的監視權
                    System.out.println(Thread.currentThread().getName() + " 進入4");
                }
            }
        }
    }
}

釋放鎖

下面操作會釋放鎖

  1. 當前執行緒的同步方法、同步程式碼塊執行結束。
  2. 當前執行緒在同步程式碼塊、同步方法中遇到break、return。
  3. 當前執行緒在同步程式碼塊、同步方法中出現了未處理的Error或Exception,導致異常結束。
  4. 當前執行緒在同步程式碼塊、同步方法中執行了執行緒物件的wait()方法,當前執行緒暫停,並釋放鎖。

下面操作不會釋放鎖

  1. 執行緒執行同步程式碼塊或同步方法時,程式呼叫Thread.sleep()、Thread.yield()方法暫停當前執行緒的執行,不會釋放鎖
  2. 執行緒執行同步程式碼塊時,其他執行緒呼叫了該執行緒的suspend()方法將該執行緒掛起,該執行緒不會釋放鎖。提示:應儘量避免使用suspend()和resume()來控制執行緒,方法不再推薦使用

本章作業

  1. 程式設計題

    (1)在main方法中啟動兩個執行緒

    (2)第1個執行緒迴圈隨機列印100以內的整數

    (3)直到第2個執行緒從鍵盤讀取了“Q”命令。

    package com.hspedu.homework;
    
    import java.util.Scanner;
    
    
    public class Homework01 {
        public static void main(String[] args) {
            A a = new A();
            B b = new B(a);//一定要注意.
            a.start();
            b.start();
        }
    }
    
    //建立A執行緒類
    class A extends Thread {
        private boolean loop = true;
    
        @Override
        public void run() {
            //輸出1-100數字
            while (loop) {
                System.out.println((int)(Math.random() * 100 + 1));
                //休眠
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("a執行緒退出...");
    
        }
    
        public void setLoop(boolean loop) {//可以修改loop變數
            this.loop = loop;
        }
    }
    
    //直到第2個執行緒從鍵盤讀取了“Q”命令
    class B extends Thread {
        private A a;
        private Scanner scanner = new Scanner(System.in);
    
        public B(A a) {//構造器中,直接傳入A類物件
            this.a = a;
        }
    
        @Override
        public void run() {
            while (true) {
                //接收到使用者的輸入
                System.out.println("請輸入你指令(Q)表示退出:");
                char key = scanner.next().toUpperCase().charAt(0);
                if(key == 'Q') {
                    //以通知的方式結束a執行緒
                    a.setLoop(false);
                    System.out.println("b執行緒退出.");
                    break;
                }
            }
        }
    }
    
  2. 程式設計題

    (1)有2個使用者分別從同一個卡上取錢(總額:10000)

    (2)每次都取1000,當餘額不足時,就不能取款了

    (3)不能出現超取現象=》執行緒同步問題.

    package com.hspedu.homework;
    
    public class Homework02 {
        public static void main(String[] args) {
            T t = new T();
            Thread thread1 = new Thread(t);
            thread1.setName("t1");
            Thread thread2 = new Thread(t);
            thread2.setName("t2");
            thread1.start();
            thread2.start();
        }
    }
    
    //1.因為這裡涉及到多個執行緒共享資源,所以我們使用實現Runnable方式
    //2. 每次取出 1000
    class T implements  Runnable {
        private int money = 10000;
    
        @Override
        public void run() {
            while (true) {
    
                //解讀
                //1. 這裡使用 synchronized 實現了執行緒同步
                //2. 當多個執行緒執行到這裡時,就會去爭奪 this物件鎖
                //3. 哪個執行緒爭奪到(獲取)this物件鎖,就執行 synchronized 程式碼塊, 執行完後,會釋放this物件鎖
                //4. 爭奪不到this物件鎖,就blocked ,準備繼續爭奪
                //5. this物件鎖是非公平鎖.
    
                synchronized (this) {//
                    //判斷餘額是否夠
                    if (money < 1000) {
                        System.out.println("餘額不足");
                        break;
                    }
    
                    money -= 1000;
                    System.out.println(Thread.currentThread().getName() + " 取出了1000 當前餘額=" + money);
                }
    
                //休眠1s
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }