JAVA多線程詳解(超詳細)

小心程序猿qaq 發佈 2023-05-28T01:54:28.433126+00:00

一、線程簡介1、進程、線程程序:開發寫的代碼稱之為程序。程序就是一堆代碼,一組數據和指令集,是一個靜態的概念。進程(Process) :將程序運行起來,我們稱之為進程。進程是執行程序的一次執行過程,它是動態的概念。進程存在生命周期,也就是說程序隨著程序的終止而銷毀。

一、線程簡介

1、進程、線程

  • 程序:開發寫的代碼稱之為程序。程序就是一堆代碼,一組數據和指令集,是一個靜態的概念。
  • 進程(Process) :將程序運行起來,我們稱之為進程。進程是執行程序的一次執行過程,它是動態的概念。進程存在生命周期,也就是說程序隨著程序的終止而銷毀。進程之間是通過TCP/IP埠實現交互的。
  • 線程(Thread) :線程是進程中的實際運作的單位,是進程的一條流水線,是程序的實際執行者,是最小的執行單位。通常在一個進程中可以包含若干個線程,當然一個進程中至少有一個線程。線程是CPU調度和執行的最小單位。
    注意:很多多線程都是模擬出來的,真正的多線程是指有多個CPU,即多核,如伺服器,如果是模擬出來的多線程,即一個CPU的情況下,在同一個時間點,CPU只能執行一個代碼,因為切換的很快,所以就有同時執行的錯覺。

2、並發、並行、串行

  • 並發:同一個對象被多個線程同時操作。(這是一種假並行。即一個CPU的情況下,在同一個時間點,CPU只能執行一個代碼,因為切換的很快,所以就有同時執行的錯覺)。
  • 並行:多個任務同時進行。並行必須有多核才能實現,否則只能是並發。
  • 串行:一個程序處理完當前進程,按照順序接著處理下一個進程,一個接著一個進行。

3、進程的三態

進程在運行的過程中不斷的改變其運行狀態。通常一個運行的進程必須有三個狀態,就緒態、運行態、阻塞態。

  • 就緒態:當進程獲取出CPU外所有的資源後,只要再獲得CPU就能執行程序,這時的狀態叫做就緒態。在一個系統中處於就緒態的進程會有多個,通常把這些排成一個隊列,這個就叫就緒隊列。
  • 運行態:當進程已獲得CPU操作權限,正在運行,這個時間就是運行態。在單核系統中,同一個時間只能有一個運行態,多核系統中,會有多個運行態。
  • 阻塞態:正在執行的進程,等待某個事件而無法繼續運行時,便被系統剝奪了CPU的操作權限,這時就是阻塞態。引起阻塞的原因有很多,比如:等待I/O操作、被更高的優先級的進程剝奪了CPU權限等。

二、線程實現

1、繼承Thread類

scss複製代碼 步驟:
 - 自定義線程類繼承Thread類
 - 重寫run()方法,編寫線程執行體
 - 創建線程對象,調用start()方法啟動線程(啟動後不一定立即執行,搶到CPU資源才能執行)

1234

代碼如下(示例):

Java複製代碼// 自定義線程對象,繼承Thread,重寫run()方法
public class MyThread extends Thread {

    public MyThread(String name){
        super(name);
    }

    @Override
    public void run() {
        // 線程執行體
        for (int i = 0; i < 10; i++) {
            System.out.println("我是自定義" + Thread.currentThread().getName() + "--" + i);
        }
    }

    public static void main(String[] args) {
        // main線程,主線程

        // 創建線程實現類對象
        MyThread thread = new MyThread("線程1");
        MyThread thread2 = new MyThread("線程2");
        // 調用start()方法啟動線程
        thread.start();
        thread2.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("我是主線程--" + i);
        }
    }
}

123456789101112131415161718192021222324252627282930
複製代碼執行結果:

1
lua複製代碼我是自定義線程2--0
我是自定義線程2--1
我是主線程--0
我是自定義線程1--0
我是主線程--1
我是主線程--2
我是自定義線程2--2
我是主線程--3
我是自定義線程1--1
我是主線程--4
我是主線程--5
我是主線程--6
我是主線程--7
我是主線程--8
我是主線程--9
我是自定義線程2--3
我是自定義線程1--2
我是自定義線程2--4
我是自定義線程1--3
我是自定義線程1--4
我是自定義線程1--5
我是自定義線程1--6
我是自定義線程1--7
我是自定義線程1--8
我是自定義線程1--9
我是自定義線程2--5
我是自定義線程2--6
我是自定義線程2--7
我是自定義線程2--8
我是自定義線程2--9

123456789101112131415161718192021222324252627282930

2、實現Runnable接口

scss複製代碼 步驟:
 - 自定義線程類實現Runnable接口
 - 實現run()方法,編寫線程體
 - 創建線程對象,調用start()方法啟動線程(啟動後不一定立即執行,搶到CPU資源才能執行)

1234

代碼如下(示例):

java複製代碼
// 自定義線程對象,實現Runnable接口,重寫run()方法
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 線程執行體
        for (int i = 0; i < 10; i++) {
            System.out.println("我是自定義" + Thread.currentThread().getName() + "--" + i);
        }
    }

    public static void main(String[] args) {
        // main線程,主線程

        // 創建實現類對象
        MyRunnable myRunnable = new MyRunnable();
        // 創建代理類對象
        Thread thread = new Thread(myRunnable,"線程1");
        Thread thread2 = new Thread(myRunnable,"線程2");
        // 調用start()方法啟動線程
        thread.start();
        thread2.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("我是主線程--" + i);
        }
    }
}

12345678910111213141516171819202122232425262728

執行結果:

lua複製代碼我是主線程--0
我是自定義線程1--0
我是自定義線程2--0
我是自定義線程1--1
我是主線程--1
我是自定義線程1--2
我是自定義線程2--1
我是自定義線程1--3
我是主線程--2
我是主線程--3
我是自定義線程1--4
我是自定義線程2--2
我是自定義線程2--3
我是自定義線程2--4
我是自定義線程1--5
我是自定義線程1--6
我是主線程--4
我是自定義線程1--7
我是自定義線程1--8
我是自定義線程1--9
我是自定義線程2--5
我是自定義線程2--6
我是自定義線程2--7
我是自定義線程2--8
我是主線程--5
我是自定義線程2--9
我是主線程--6
我是主線程--7
我是主線程--8
我是主線程--9

123456789101112131415161718192021222324252627282930

3、實現Callable接口(不常用)

ini複製代碼 步驟:
 - 實現Callable接口,先要返回值類型
 - 重寫call()方法,需要拋出異常
 - 創建目標對象
 - 創建執行服務:ExecutorService ser = Executor.newFixedThreadPool(1);
 - 提交執行:Future<boolean> res = ser.submit(t1);
 - 獲取結果:boolean r1 = res.get();
 - 關閉服務:ser.shutdownNow();

12345678

代碼如下(示例):

java複製代碼import java.util.concurrent.*;

// 自定義線程對象,實現Callable接口,重寫call()方法
public class MyThread implements Callable<Boolean> {

    @Override
    public Boolean call() throws Exception {
        // 線程執行體
        for (int i = 0; i < 10; i++) {
            System.out.println("我是自定義" + Thread.currentThread().getName() + "--" + i);
        }

        return true;
    }

    public static void main(String[] args) throws ExecutionException,
        InterruptedException {
        // main線程,主線程

        // 創建線程實現類對象
        MyThread thread = new MyThread();
        MyThread thread2 = new MyThread();

        // 創建執行服務,參數是線程池線程數量
        ExecutorService ser = Executors.newFixedThreadPool(2);
        // 提交執行
        Future<Boolean> res = ser.submit(thread);
        Future<Boolean> res2 = ser.submit(thread2);
        // 獲取結果
        boolean r1 = res.get();
        boolean r2 = res2.get();
        // 關閉服務
        ser.shutdownNow();
    }
}

1234567891011121314151617181920212223242526272829303132333435
複製代碼執行結果:

1
lua複製代碼我是自定義pool-1-thread-1--0
我是自定義pool-1-thread-2--0
我是自定義pool-1-thread-1--1
我是自定義pool-1-thread-1--2
我是自定義pool-1-thread-1--3
我是自定義pool-1-thread-1--4
我是自定義pool-1-thread-1--5
我是自定義pool-1-thread-2--1
我是自定義pool-1-thread-1--6
我是自定義pool-1-thread-2--2
我是自定義pool-1-thread-2--3
我是自定義pool-1-thread-2--4
我是自定義pool-1-thread-2--5
我是自定義pool-1-thread-2--6
我是自定義pool-1-thread-2--7
我是自定義pool-1-thread-2--8
我是自定義pool-1-thread-2--9
我是自定義pool-1-thread-1--7
我是自定義pool-1-thread-1--8
我是自定義pool-1-thread-1--9

1234567891011121314151617181920

三、線程常用方法

1、線程的狀態

  • 新建狀態(NEW) :線程已創建,尚未調用start()方法啟動之前。
  • 運行狀態(RUNNABLE) :線程對象被創建後,調用該對象的start()方法,並獲取CPU權限進行執行。
  • 阻塞狀態(BLOCKED) :線程在獲取synchronized同步鎖失敗(因為鎖被其它線程所占用),它會進入同步阻塞狀態。
  • 等待狀態(WAITING ) :等待狀態。正在等待另一個線程執行特定動作來喚醒該線程的狀態。
  • 超時等待狀態(TIME_WAITING) :有明確結束時間的等待狀態。
  • 終止狀態(TERMINATED ) :當線程結束完成之後就會變成此狀態。

2、線程常用方法

markdown複製代碼1、 sleep(Long time)方法:
 - 讓線程阻塞的指定的毫秒數。
 - 指定的時間到了後,線程進入就緒狀態。
 - sleep可研模擬網絡延時,倒計時等。
 - 每一個對象都有一個鎖,sleep不會釋放鎖。

12345

代碼如下(示例):

java複製代碼public class MyThread implements Runnable {

    @Override
    public void run() {
        // 模擬倒計時
        for (int i = 10; i >= 0; i--) {
            try {
                System.out.println(i);
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread thread = new Thread(myThread);
        thread.start();
    }

}

12345678910111213141516171819202122
複製代碼執行結果:

1
複製代碼10
9
8
7
6
5
4
3
2
1
0

1234567891011
scss複製代碼2、yield()方法:
 - 提出申請釋放CPU資源,至於能否成功釋放取決於JVM決定。
 - 調用yield()方法後,線程仍然處於RUNNABLE狀態,線程不會進入阻塞狀態。
 - 調用yield()方法後,線程處於RUNNABLE狀態,就保留了隨時被調用的權利。

1234

代碼如下(示例):

java複製代碼public class MyThread implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "線程開始執行");
        Thread.yield();
        System.out.println(Thread.currentThread().getName() + "線程結束執行");
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        Thread thread = new Thread(myThread,"a");
        Thread thread2 = new Thread(myThread,"b");
        thread.start();
        thread2.start();
    }

}

123456789101112131415161718
css複製代碼執行結果:從結果1看,a釋放CPU成功後,b就搶到了CPU執行權,接著b也釋放CPU成功,a搶到了CPU執行權;從結果2看,a並沒有成功釋放CPU。

1
css複製代碼結果1:
a線程開始執行
b線程開始執行
a線程結束執行
b線程結束執行
結果2:
a線程開始執行
a線程結束執行
b線程開始執行
b線程結束執行

12345678910
scss複製代碼3、join()方法:
 - 將當前的線程掛起,當前線程阻塞,待其他的線程執行完畢,當前線程才能執行。
 - 可以把join()方法理解為插隊,誰插到前面,誰先執行。

123

代碼如下(示例):

java複製代碼public class MyThread implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "join()線程執行:" + i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        Thread thread = new Thread(myThread,"a");
        thread.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("主線程執行:" + i);
            if (i == 2) {
                thread.join(); //主線程阻塞,等待thread一口氣執行完,主線程才能繼續執行
            }
        }
    }

}

1234567891011121314151617181920212223
複製代碼執行結果:

1
scss複製代碼主線程執行:0
a線程join()執行:0
主線程執行:1
主線程執行:2
a線程join()執行:1
a線程join()執行:2
a線程join()執行:3
a線程join()執行:4
a線程join()執行:5
a線程join()執行:6
a線程join()執行:7
a線程join()執行:8
a線程join()執行:9
主線程執行:3
主線程執行:4
主線程執行:5
主線程執行:6
主線程執行:7
主線程執行:8
主線程執行:9

1234567891011121314151617181920
diff複製代碼4、setPriority (int newPriority)、getPriority()
- 改變、獲取線程的優先級。
- Java提供一個線程調度器來監控程序中啟動後進入就緒狀態的所有線程,線程調度器按照優先級決定應該調度哪個線程來執行。
- 線程的優先級用數據表示,範圍1~10。
- 線程的優先級高只是表示他的權重大,獲取CPU執行權的機率大。
- 先設置線程的優先級,在執行start()方法。

123456

代碼如下(示例):

ini複製代碼public class MyThread implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "線程優先級:"
            + Thread.currentThread().getPriority());
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        Thread thread = new Thread(myThread,"a");
        Thread thread2 = new Thread(myThread,"b");
        Thread thread3= new Thread(myThread,"c");
        Thread thread4= new Thread(myThread,"d");
        thread3.setPriority(Thread.MAX_PRIORITY);
        thread.setPriority(Thread.MIN_PRIORITY);
        thread2.setPriority(Thread.NORM_PRIORITY);
        thread4.setPriority(8);
        thread.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

123456789101112131415161718192021222324

執行結果:優先級高的線程不一定先執行

css複製代碼c線程優先級:10
b線程優先級:5
a線程優先級:1
d線程優先級:8

1234
scss複製代碼5、stop()、destroy()。【已廢棄】
- JDK提供的上述兩種方法已廢棄,不推薦使用。
- 推薦線程自動停止下來,建議使用一個標識位變量進行終止,當flag=false時,則終止線程運行。

123

代碼如下(示例):

java複製代碼public class MyThread implements Runnable {
    /**
     * 標識位,為false時,線程結束
     */
    private boolean flag = true;

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

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

    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        Thread thread = new Thread(myThread,"a");
        thread.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("主線程:" + i);
            if (i == 5) {
                // 當主線程 i== 5時,標識位變為false,控制子線程停止
                myThread.setFlag(false); 
            }
        }
    }
}

123456789101112131415161718192021222324252627282930313233343536

執行結果:主線程 i== 5 之後,子線程就停止運行了

css複製代碼主線程:0
主線程:1
a線程:0
a線程:1
a線程:2
a線程:3
a線程:4
a線程:5
a線程:6
主線程:2
a線程:7
主線程:3
a線程:8
主線程:4
a線程:9
主線程:5
a線程:10
主線程:6
主線程:7
主線程:8
主線程:9

123456789101112131415161718192021

四、多線程

線程同步就是線程排隊,就是操作共享資源要有先後順序,一個線程操作完之後,另一個線程才能操作或者讀取。

  • 防止線程同步訪問共享資源造成衝突。
  • 變量需要同步,常量不需要同步(常量存放於方法區)。
  • 多個線程訪問共享資源的代碼(即線程執行體)有可能是同一份代碼,也有可能是不同的代碼;無論是否執行同一份代碼,只要這些線程的代碼訪問同一份可變的共享資源,這些線程之間就需要同步。

1、守護(Deamon)線程

  • 線程分為用戶線程守護線程
  • 虛擬機必須確保用戶線程執行完畢。
  • 虛擬機不用等待守護線程執行完畢。(如:後天記錄操作日誌、監控內存、垃圾回收等線程)。
  • Thread.setDeamon(booean on)方法,true:守護線程;fasle:用戶進程。默認是false。

代碼如下(示例):

java複製代碼public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        DeamonThread deamon = new DeamonThread();
        UserThread user = new UserThread();

        Thread deamonThread = new Thread(deamon);
        deamonThread.setDaemon(true); // 設置為守護進程
        deamonThread.start();

        Thread userThread = new Thread(user);
        userThread.start();
    }
}

// 模擬守護線程
class DeamonThread implements Runnable{

    @Override
    public void run() {
        // 驗證虛擬機不用等待守護線程執行完畢,只要用戶線程執行完畢,程序就結束。
        // 如果成功,怎下面的列印不會一直輸出;如果成功,則下面的列印會一直輸出
        while (true) {
            System.out.println("我是守護線程");
        }
    }
}

// 用戶線程
class UserThread implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("我是用戶線程 :" + i);
        }
    }
}

12345678910111213141516171819202122232425262728293031323334353637

執行結果:守護進程不會一直列印

erlang複製代碼我是守護線程
我是用戶線程 :0
我是守護線程
...
我是用戶線程 :1
我是守護線程
...
我是用戶線程 :2
我是用戶線程 :3
我是用戶線程 :4
我是用戶線程 :5
我是用戶線程 :6
我是用戶線程 :7
我是用戶線程 :8
我是用戶線程 :9
我是守護線程
...
我是守護線程

123456789101112131415161718

2、多線程並發與同步

(1)、多線程並發
在多線程場景下,如果多個線程修改同一個資源,或者一個線程修改共享資源,另一個線程讀取共享資源,可能會導致結果不對的問題,這就導致線程不安全,即並發。導致線程並發的原因:

  • 原子性:一個或多個操作在CPU執行過程中被中斷。即一個操作或者多個操作要麼全部執行並且執行的過程中不會被任何因素打斷,要麼就不執行。原子性就像資料庫裡面的事務一樣,他們是一個整體,同存亡。
  • 可見性:一個線程對共享變量的修改,另一個線程不能立馬看到。
  • 有序性:程序執行的順序沒有按照代碼的先後順序執行。

下面以兩個例子演示線程不安全問題。
示例1:買票問題

代碼如下(示例):

java複製代碼// 模擬線程不安全問示例1:買票
public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        BuyTicker ticker = new BuyTicker();

        Thread person1Thread = new Thread(ticker, "person1");
        Thread person2Thread = new Thread(ticker, "person2");
        Thread person3Thread = new Thread(ticker, "person3");
        person1Thread.start();
        person2Thread.start();
        person3Thread.start();
    }
}

class BuyTicker implements Runnable{
    // 車票
    private int tickerNum = 10;
    // 停止線程標識
    boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            try {
                buyTicker();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void buyTicker() throws InterruptedException {
        // 判斷是否還有票
        if (tickerNum <= 0) {
            flag = false;
            return;
        }
        // 模擬延時
        Thread.sleep(100);
        // 買票
        System.out.println(Thread.currentThread().getName() + "買到第" + tickerNum -- + "張票");
    }
}

12345678910111213141516171819202122232425262728293031323334353637383940414243

執行結果:可以看到第4、3張票賣了兩次,還有人買到了第0張票

複製代碼person3買到第8張票
person2買到第10張票
person1買到第9張票
person1買到第7張票
person3買到第5張票
person2買到第6張票
person1買到第4張票
person2買到第4張票
person3買到第3張票
person1買到第2張票
person2買到第2張票
person3買到第1張票
person1買到第0張票

12345678910111213

示例2:銀行取錢

代碼如下(示例):

java複製代碼// 模擬線程不安全示例2:銀行取錢
public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        Account account = new Account(1000, "旅遊基金");

        new Bank(account, 500, "你").start();
        new Bank(account, 600, "女朋友").start();
    }
}

// 帳戶
class Account {
    // 帳戶總餘額
    int money;
    // 帳戶名
    String name;

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

// 銀行
class Bank extends Thread{
    // 客戶帳戶
    Account account;
    // 取得錢數
    int drawingMoney;

    public Bank(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(account.name+ "錢不夠了,取不了了");
            return;
        }

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 卡內餘額 = 餘額 - 取得錢
        account.money = account.money - drawingMoney;

        System.out.println(Thread.currentThread().getName()  + "取了" + drawingMoney);
        System.out.println(account.name + "餘額為:" + account.money);

    }
}

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556

執行結果:當你取500時,線程執行到account.money = account.money - drawingMoney之前,另一個線程搶到了CPU執行權,也執行到account.money = account.money - drawingMoney之前,現在餘額還是1000,繼續執行1000-500-900=-400.

yaml複製代碼你取了500
女朋友取了900
旅遊基金餘額為:-400
旅遊基金餘額為:-400

1234

(2)、同步(解決並發問題)

解決線程並發問題的方法是線程同步線程同步就是讓線程排隊,就是操作共享資源要有先後順序,一個線程操作完之後,另一個線程才能操作或者讀取。

  • 防止線程同步訪問共享資源造成衝突。
  • 變量需要同步,常量不需要同步(常量存放於方法區)。
  • 多個線程訪問共享資源的代碼(即線程執行體)有可能是同一份代碼,也有可能是不同的代碼;無論是否執行同一份代碼,只要這些線程的代碼訪問同一份可變的共享資源,這些線程之間就需要同步。

解決並發問題的兩種方法:
同步方法:public synchronized void method(int args){執行體…}

  • synchronized 方法控制對(synchronized修飾的方法所在的對象,就是this)「對象」的訪問,每個對象對應一把鎖,每個synchronized方法都必須獲得調用該方法的鎖才能執行,否則線程會阻塞,synchronized所在方法一旦執行,就獨占該鎖,直到方法執行完
    才釋放鎖,後面被阻塞的線程才能獲得這個鎖,繼續執行。
  • 缺陷:若將一個大的方法聲明為synchronized 將會影響效率。需要修改的內容才需要鎖,鎖的太多,浪費資源。

同步代碼塊:synchronized (Obj){執行體…}

  • Obj稱之為同步監視器,可以是任何對象,但是推薦使用共享資源作為同步監視器。
  • 不同方法中無需指定同步監視器,因為同步方法中的同步監視器就是this,就是這個對象本身,或者是class。

示例1:買票問題(使用同步方法改造成線程安全)

代碼如下(示例):

java複製代碼// 模擬線程不安全問示例1:買票
public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        BuyTicker ticker = new BuyTicker();

        Thread person1Thread = new Thread(ticker, "person1");
        Thread person2Thread = new Thread(ticker, "person2");
        Thread person3Thread = new Thread(ticker, "person3");
        person1Thread.start();
        person2Thread.start();
        person3Thread.start();
    }
}

class BuyTicker implements Runnable{
    // 車票
    private int tickerNum = 10;
    // 停止線程標識
    boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            try {
                buyTicker();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private synchronized void buyTicker() throws InterruptedException {
        // 判斷是否還有票
        if (tickerNum <= 0) {
            flag = false;
            return;
        }
        // 模擬延時
        Thread.sleep(100);
        // 買票
        System.out.println(Thread.currentThread().getName() + "買到第" + tickerNum -- + "張票");
    }
}

12345678910111213141516171819202122232425262728293031323334353637383940414243

執行結果:

複製代碼person1買到第10張票
person1買到第9張票
person1買到第8張票
person1買到第7張票
person1買到第6張票
person1買到第5張票
person3買到第4張票
person3買到第3張票
person3買到第2張票
person2買到第1張票

12345678910

示例2:銀行取錢(使用同步代碼塊改造成線程安全)

代碼如下(示例):

java複製代碼// 模擬線程不安全示例2:銀行取錢
public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        Account account = new Account(1000, "旅遊基金");

        new Bank(account, 500, "你").start();
        new Bank(account, 600, "女朋友").start();
    }
}

// 帳戶
class Account {
    // 帳戶總餘額
    int money;
    // 帳戶名
    String name;

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

// 銀行
class Bank extends Thread{
    // 客戶帳戶
    Account account;
    // 取得錢數
    int drawingMoney;

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

    @Override
    public void run() {
        synchronized (account) {
            if (account.money- drawingMoney < 0) {
                System.out.println(account.name+ "錢不夠了,取不了了");
                return;
            }

            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 卡內餘額 = 餘額 - 取得錢
            account.money = account.money - drawingMoney;
            System.out.println(Thread.currentThread().getName()  + "取了" + drawingMoney);
            System.out.println(account.name + "餘額為:" + account.money);
        }
    }
}

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556

執行結果:你取了500後,你女朋友取600,就提示餘額不足,不會出現餘額為負數的情況了。這裡的同步監視器是account,account才是操作的共享資源,而不是bank。

複製代碼你取了500
旅遊基金餘額為:500
旅遊基金錢不夠了,取不了了

123

示例3:模擬集合ArrayList<>()是線程不安全的

代碼如下(示例):

java複製代碼import java.util.ArrayList;
import java.util.List;

// 模擬集合ArrayList<>()是線程不安全的
public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                list.add(Thread.currentThread().getName());
            }).start();
        }
                // sleep可研模擬網絡延遲
        Thread.sleep(5000);
        System.out.println(list.size());
    }
}

1234567891011121314151617

執行結果:list的大小應該是1000,結果是999

複製代碼999

1

使用同步代碼塊改造成線程安全的

代碼如下(示例):

java複製代碼import java.util.ArrayList;
import java.util.List;

// 模擬線程不安全示例2:銀行取錢
public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                // 加鎖
                synchronized (list) {
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }

        Thread.sleep(5000);
        System.out.println(list.size());
    }
}

1234567891011121314151617181920

執行結果:

yaml複製代碼1000

1

3、死鎖

(1)死鎖形成的原因:多個線程各自占有一個資源,並且相互等待其他線程占有的資源才能運行,從而導致另個或者多個線程都在等待對方釋放資源,都停止了執行。某一個同步代碼塊同時擁有「兩個以上對象的鎖」時,就可能會發生「死鎖」的問題。

代碼如下(示例):

java複製代碼// 死鎖例子:魚和熊不可兼得
public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        Person personA = new Person(0, "獵人A");
        Person personB = new Person(1, "獵人B");
        personA.start();
        personB.start();
    }
}

// 熊掌
class Bear {

}

// 魚
class Fish {

}

// 人
class Person extends Thread {
    // 保證資源只有一份
    public static Bear bear = new Bear();
    public static Fish fish = new Fish();

    int choose;
    String personName;

    public Person (int choose, String personName) {
        this.choose = choose;
        this.personName = personName;
    }

    @Override
    public void run() {
        // 捕獵
        try {
            this.hunting();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 捕獵方法
    private void hunting() throws InterruptedException {
        if (choose == 0) {
            synchronized (bear) {
                System.out.println(personName + "想捕捉熊");
                Thread.sleep(1000);
                synchronized (fish) {
                    System.out.println(personName + "想捕捉魚");
                }
            }
        } else {
            synchronized (fish) {
                System.out.println(personName + "想捕捉魚");
                Thread.sleep(1000);
                synchronized (bear) {
                    System.out.println(personName + "想捕捉熊");
                }
            }
        }
    }
}

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465

執行結果:兩個線程一直阻塞,都在等在對方釋放鎖,結果導致死鎖。

css複製代碼獵人A想捕捉熊
獵人B想捕捉魚

12

(2)解決死鎖的方法:同步代碼塊中不要相互嵌套,即,不要相互嵌套鎖。

代碼如下(示例):

java複製代碼// 死鎖例子:魚和熊不可兼得
public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        Person personA = new Person(0, "獵人A");
        Person personB = new Person(1, "獵人B");
        personA.start();
        personB.start();
    }
}

// 熊掌
class Bear {

}

// 魚
class Fish {

}

// 人
class Person extends Thread {
    // 保證資源只有一份
    public static Bear bear = new Bear();
    public static Fish fish = new Fish();

    int choose;
    String personName;

    public Person (int choose, String personName) {
        this.choose = choose;
        this.personName = personName;
    }

    @Override
    public void run() {
        // 捕獵
        try {
            this.hunting();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 捕獵方法
    private void hunting() throws InterruptedException {
        if (choose == 0) {
            synchronized (bear) {
                System.out.println(personName + "想捕捉熊");
                Thread.sleep(1000);
            }
            // 把嵌套的代碼塊拿到外面,兩個代碼塊並列
            synchronized (fish) {
                System.out.println(personName + "想捕捉魚");
            }
        } else {
            synchronized (fish) {
                System.out.println(personName + "想捕捉魚");
                Thread.sleep(1000);
            }
            // 把嵌套的代碼塊拿到外面,兩個代碼塊並列
            synchronized (bear) {
                System.out.println(personName + "想捕捉熊");
            }
        }
    }
}

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667

執行結果:兩個線程即捕到了熊,有捕到了魚,解決了死鎖問題。

css複製代碼獵人B想捕捉魚
獵人A想捕捉熊
獵人A想捕捉魚
獵人B想捕捉熊

1234

4、Lock(鎖)

Lock 鎖也稱同步鎖,java.util.concurrent.locks.Lock 機制提供了⽐ synchronized 代碼塊和 synchronized ⽅法更⼴泛的鎖定操作,同步代碼塊 / 同步⽅法具有的功能 Lock 都有,除此之外更強⼤,更體現⾯向對象。
創建對象 Lock lock = new ReentrantLock() ,加鎖與釋放鎖⽅法如下:
public void lock() :加同步鎖
public void unlock() :釋放同步鎖

synchronized和Lock的對比:

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

代碼如下(示例):

java複製代碼import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

// 測試 Lock鎖
public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        TestLock testLock = new TestLock();
        new Thread(testLock,"a").start();
        new Thread(testLock,"b").start();
        new Thread(testLock,"c").start();
    }
}

class TestLock implements Runnable {

    // 車票
    private static int tickerNum = 10;

    // 創建一個Lock鎖
    private final Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            lock.lock(); // 加鎖
            try {
                // 判斷是否還有票
                if (tickerNum > 0) {
                    // 模擬延時
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 買票
                    System.out.println(Thread.currentThread().getName() + "線程買到第" + tickerNum -- + "張票");
                } else {
                    break;
                }
            } finally {
                lock.unlock(); // 解鎖
            }
        }
    }
}

123456789101112131415161718192021222324252627282930313233343536373839404142434445

執行結果:

css複製代碼a線程買到第10張票
a線程買到第9張票
a線程買到第8張票
a線程買到第7張票
a線程買到第6張票
a線程買到第5張票
b線程買到第4張票
b線程買到第3張票
b線程買到第2張票
b線程買到第1張票

12345678910

5、線程協作

  • 線程之間需要進行通信,通信有數據共享(1、文件共享;2、網絡共享;3、變量共享)和線程協作兩種方式。
  • 線程協作指不同線程驅動的任務相互依賴,依賴一般就是對共享資源的依賴。(有共享就有競爭,有競爭就會有線程安全問題(即並發),解決並發問題就用線程同步)。

應用場景:生產者和消費者問題

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

場景分析:這是一個線程同步問題,生產者和消費者共享同一個資源,並且生產者和消費者之間相互依賴,互為條件。

  • 對於生產者,沒有生產產品之前,要通知消費者等待。而生產了產品之後,又需要馬上通知消費者消費。
  • 對於消費者,在消費之後,要通知生產者已經結束消費,需要生產新的產品以供消費。

在生產者消費者問題中,沒生產出產品之前,消費者是不能消費的,反之,消費者沒消費完之前,生產者是不能生產的。這就需要來實現線程之間的同步。僅有同步還不行,還要實現線程之間的消息傳遞,即通信

Java提供了解決線程之間通信問題的方法:

方法名

作用

wait ()

表示線程一直等待,直到其他線程通知,與 sleep 不同會釋放鎖

wait (long timeOut)

指定等待的毫秒數

notify ()

喚醒一個處於等待狀態的線程

notifyAll()

喚醒同一個對象所有的調用 wait()方法的線程,優先級高的優先調度

注意:均是Object的方法,均只能在同步方法或者同步代碼塊中使用,否則會拋出異常IIIegalMonitorStageException。

解決線程之間通信的方式管程法信號燈法

管程法:生產者把生產好的數據放入緩存區,消費者從緩存區中拿出數據。

代碼如下(示例):

java複製代碼// 線程通信:生產消費模式-管程法
public class MyThread{
    public static void main(String[] args) {
        SynContainer container = new SynContainer();
        new Productor(container).start();
        new Consumer(container).start();
    }
}

// 產品
class Chicken {
    int id;

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

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

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

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            synContainer.pushTo(new Chicken(i));
        }
    }
}

// 消費者
class Consumer extends Thread {
    SynContainer synContainer;

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

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            synContainer.popTo();
        }
    }
}

// 容器
class SynContainer {
    // 定義一個大小為10的容器
    Chicken[] chickens = new Chicken[10];
    // 容器計數器
    int count;

    // 生產者生產產品方法
    public synchronized void pushTo(Chicken chicken) {
        // 如果容器滿了,就停止生產
        if (chickens.length == count) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 如果容器沒滿,就往容器中放入產品
        chickens[count] = chicken;
        System.out.println("生產了" + chicken.id + "個雞腿");
        count ++;

        // 通知消費者消費
        this.notifyAll();
    }

    // 消費者消費產品方法
    public synchronized Chicken popTo() {
        // 如果容器中沒有產品了,就停止消費
        if (count == 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 如果容器有產品,就可以消費
        count --;
        Chicken chicken = chickens[count];
        System.out.println("消費了第" + chicken.id + "個雞退");

        //只要消費了,就通知生產者生產
        this.notifyAll();
        return chicken;
    }
}

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798

執行結果:

複製代碼生產了0個雞腿
生產了1個雞腿
生產了2個雞腿
生產了3個雞腿
生產了4個雞腿
生產了5個雞腿
生產了6個雞腿
生產了7個雞腿
生產了8個雞腿
生產了9個雞腿
消費了第9個雞退
生產了10個雞腿
消費了第10個雞退
生產了11個雞腿
消費了第11個雞退
生產了12個雞腿
消費了第12個雞退
生產了13個雞腿
消費了第13個雞退
生產了14個雞腿
消費了第14個雞退
生產了15個雞腿
消費了第15個雞退
生產了16個雞腿
消費了第16個雞退
生產了17個雞腿
消費了第17個雞退
消費了第8個雞退
消費了第7個雞退
消費了第6個雞退
消費了第5個雞退
消費了第4個雞退
消費了第3個雞退
消費了第2個雞退
消費了第1個雞退
消費了第0個雞退
生產了18個雞腿
生產了19個雞腿
消費了第19個雞退
消費了第18個雞退

12345678910111213141516171819202122232425262728293031323334353637383940

信號燈法:設置一個標識位,標識位來判斷線程是等待還是執行。

代碼如下(示例):

java複製代碼// 線程通信:生產消費模式-信號燈法
public class MyThread{
    public static void main(String[] args) {
        CCTV cctv = new CCTV();
        new Player(cctv).start();
        new Watcher(cctv).start();
    }
}

// 演員
class Player extends Thread {
    CCTV cctv;

    public Player(CCTV cctv) {
        this.cctv = cctv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (i%2 == 0) {
                cctv.play("快樂大本營");
            } else {
                cctv.play("天天向上");
            }
        }
    }
}

// 觀眾
class Watcher extends Thread {
    CCTV cctv;

    public Watcher(CCTV cctv) {
        this.cctv = cctv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            cctv.watch();
        }
    }
}

// 電視
class CCTV {
    // 表演的節目
    String voice;
    // 標識
    boolean flag = true;

    // 表演節目
    public synchronized void play(String voice) {
        if (!flag) {
            try {
                this.wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        this.voice = voice;
        System.out.println("演員表演了:" + voice);
        // 通知觀眾觀看
        this.notifyAll();
        this.flag = !flag;
    }

    // 觀看節目
    public synchronized void watch () {
        if (flag) {
            try {
                this.wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        // 如果容器有產品,就可以消費
        System.out.println("觀眾觀看了:" + voice);
        // 通知演員表演節目
        this.notifyAll();
        this.flag = !flag;
    }
}

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485

執行結果:

複製代碼演員表演了:快樂大本營
觀眾觀看了:快樂大本營
演員表演了:天天向上
觀眾觀看了:天天向上
演員表演了:快樂大本營
觀眾觀看了:快樂大本營
演員表演了:天天向上
觀眾觀看了:天天向上
演員表演了:快樂大本營
觀眾觀看了:快樂大本營
演員表演了:天天向上
觀眾觀看了:天天向上
演員表演了:快樂大本營
觀眾觀看了:快樂大本營
演員表演了:天天向上
觀眾觀看了:天天向上
演員表演了:快樂大本營
觀眾觀看了:快樂大本營
演員表演了:天天向上
觀眾觀看了:天天向上

1234567891011121314151617181920

6、線程池

背景:經常創建和銷毀線程,消耗特別大的資源,比如並發的情況下的線程,對性能影響很大。線程池就是問題為了解決這個問題,提前創建好多個線程,放在線程池中,使用時直接獲取,使用完放回線程池中,可以避免頻繁的創建、銷毀,實現重複利用。
優點

  • 提高相應速度(減少創建線程的時間)
  • 降低資源消耗(重複利用線程池中的線程,不需要每次都創建)
  • 便於線程管理:corePoolSize:核心池的大小。maximumPoolSize:最大線程數。
    keepAliveTime:線程沒有任務時最多保持多長時間後終止。

線程池相關的API:

  • ExecutorService:線程池接口。
  • 常見的實現類:ThreadPoolExecutor。
    void execute(Runnable command):執行任務命令,沒有返回值,一般用來執行Runnable.
    Future submit(Callable task):執行任務,有返回值,一般用來執行Callable
  • void shutdown():關閉連接池
  • Executors:工具類,線程池的工廠類,用來創建並返回不同類型的線程池

代碼如下(示例):

java複製代碼import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

// 線程池
public class ThreadPool {
    public static void main(String[] args) {
        // 1、創建服務,創建線程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        MyThread myThread = new MyThread();
        // 執行
        service.execute(myThread);
        service.execute(myThread);
        service.execute(myThread);
        service.execute(myThread);
        // 關閉連接
        service.shutdown();
    }
}

// 演員
class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}


123456789101112131415161718192021222324252627

執行結果:

arduino複製代碼pool-1-thread-2
pool-1-thread-4
pool-1-thread-3
pool-1-thread-1


作者:Java水解
連結:https://juejin.cn/post/7237308635927576631

關鍵字: