多執行緒詳解

1. 多執行緒快速入門

1.1 進程與執行緒

  • 什麼是進程?

    CPU從硬碟中讀取一段程式到記憶體中,該執行程式的實例就叫做進程。

    一個程式如果被CPU多次讀取到記憶體中,則變成多個獨立的進程。

  • 什麼是執行緒?

    執行緒是程式執行的最小單位,在一個進程中可以有多個不同的執行緒同時執行。

  • 為什麼在進程中還需要執行緒呢?

    例如,一個文本編輯器進程,在編輯器中,需要同時做很多事情:監聽用戶按下的鍵盤事件、將文本渲染到螢幕上,將文本內容持久化到硬碟,這三件事就是三個執行緒。執行緒是最小的並行單位。

  • 為什麼需要使用多執行緒?

    採用多執行緒的形式執行程式碼,目的就是為了提高程式的效率

    比如:一個項目只有一個程式設計師開發,需要開發的模組需求有會員模組、支付模組、訂單模組等,該程式設計師要按順序依次將各個模組完成。而當有三個程式設計師同時完成不同的模組,那麼就可以大大提高開發效率了。

  • 串列與並行的區別

    串列也就是單執行緒執行,程式碼執行效率非常低,程式碼從上到下執行。

    並行就是多個執行緒一起執行,效率比較高。

  • 多執行緒的應用場景有哪些?

    • 客戶端(/移動App)開發
    • 非同步發送簡訊/郵件
    • 將執行比較耗時的程式碼改用多執行緒非同步執行
    • 非同步寫入日誌 日誌框架底層
    • 多執行緒下載
  • 同步與非同步的區別

    同步:程式碼從頭到尾執行

    非同步:單獨分支執行,相互之間沒有任何影響

1.2 繼承Thread類創建執行緒

public class ThreadTest01 extends Thread {
    /**
     * 執行緒執行的程式碼在run方法
     */
    @Override
    public void run() {
        //獲取當前執行緒名稱
        System.out.print(Thread.currentThread().getName());
        System.out.println("子執行緒執行...");
    }

    public static void main(String[] args) {
        //獲取當前執行緒名稱
        System.out.println(Thread.currentThread().getName());
        //啟動執行緒 調用start方法而不是run方法
        //調用start()執行緒不是立即被CPU調度執行。
        new ThreadTest01().start();
        new ThreadTest01().start();
    }
}

1.3 實現Runnable介面創建執行緒

public class ThreadTest02 implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "子執行緒執行...");
    }

    public static void main(String[] args) {
        //啟動執行緒
        new Thread(new ThreadTest02()).start();
        //使用匿名內部類的形式創建執行緒
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "子執行緒執行...");
            }
        }).start();
        //使用Lambda創建多執行緒
        new Thread(() -> System.out.println(Thread.currentThread().getName() + "子執行緒執行...")).start();
    }
}

1.4 使用Callable和Future創建執行緒

Callable和Future執行緒可以獲取到返回結果,拋出異常,底層基於LockSupport

從Java1.5開始,Java提供了Callable介面,該介面是Runnable介面的增強版,Callable提供了一個call()方法,可以看作是執行緒的執行體,但call()方法比run()方法更強大。

假設有三個連續的程式碼塊(程式碼塊1,2,3),本屬於單執行緒(執行緒1)執行是從頭到尾依次執行,此時要求程式碼2使用Callable模式(執行緒2),也就是使用非同步執行且帶返回結果。執行緒2就會是一個單獨的執行緒執行:執行緒1在執行完程式碼1執行到程式碼2的時候,會單獨創建一個執行緒,執行程式碼2,執行緒1需要拿到程式碼2整個執行的返回結果,在拿到以後執行緒1繼續執行。

  • call()方法可以有返回值

  • all()方法可以聲明拋出異常

    public class ThreadTest03 implements Callable<Integer> {
        /**
         * 當前執行緒需要執行的程式碼 返回結果
         *
         * @return
         * @throws Exception
         */
        @Override
        public Integer call() throws Exception {
            System.out.println(Thread.currentThread().getName()+"子執行緒開始執行...");
            try {
                Thread.sleep(3000);
            }catch (Exception e){
    
            }
            System.out.println(Thread.currentThread().getName()+"返回1");
            return 1;
        }
    }
    
    public class ThreadTest04 {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            ThreadTest03 threadCallable = new ThreadTest03();
            FutureTask<Integer> futureTask = new FutureTask<>(threadCallable);
            new Thread(futureTask).start();
          	//調用get方法時 主執行緒阻塞 子執行緒執行完畢 再喚醒主執行緒
            Integer result = futureTask.get();
            System.out.println(Thread.currentThread().getName()+" "+result);
        }
    }
    

1.5 使用執行緒池創建執行緒

public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"開始執行子執行緒...");
        }
    });
}

JUC並發中會詳細說明

1.6 @Async非同步註解創建執行緒

項目中會使用Spring的@Async註解和執行緒池來實現多執行緒

在方法上添加@Async註解,當調用此方法時,就會創建新的執行緒來非同步執行此方法。若沒有添加非同步註解,順序執行程式,調用到該方法時,如果該方法有sleep,會一直等到該方法執行完畢才會繼續執行。

因此,一般將比較耗時的程式碼添加@Async註解。

1.7 執行緒同步/執行緒安全性問題

執行緒如何實現同步?(如何保證執行緒安全性問題)

核心思想:上鎖。當多個執行緒共享同一個全局變數時,將可能會發生執行緒安全的程式碼上鎖,最終只能有一個執行緒能夠獲取到鎖,保證只有拿到鎖的執行緒才可以執行該程式碼,沒有拿到鎖的執行緒不可以執行,需要經歷鎖的升級過程,如果一直沒有獲取到鎖,則會一直阻塞等待

如果執行緒A獲取鎖,但是執行緒A一直不釋放鎖,執行緒B就一直獲取不到鎖,會一直阻塞等待。

  • 使用synchronized鎖
  • 使用Lock鎖(屬於JUC並發包)。底層基於aqs+cas實現
  • 使用Threadlocal
  • 原子類CAS非阻塞式

2. synchronized鎖

2.1 概述

什麼是執行緒安全問題?

多個執行緒共享同一個全局變數,做的操作時,可能會受到其他執行緒的干擾,就會發生執行緒安全問題。

public class ThreadCount implements Runnable {
    private int count = 100;

    @Override
    public void run() {
        while (true){
            if (count > 1) {
                try {
                    //運行狀態->休眠狀態——CPU的執行權讓給其他執行緒
                    Thread.sleep(30);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                count--;
                System.out.println(Thread.currentThread().getName() + ":" + count);
            }else{
                break;
            }
        }
    }

    public static void main(String[] args) {
        ThreadCount threadCount = new ThreadCount();
        //開啟執行緒
        new Thread(threadCount).start();
        new Thread(threadCount).start();
    }
}

在這個程式中,兩個執行緒很大概率會同時對count進行操作。

上synchronized鎖:那麼程式碼的哪一塊需要上鎖?——可能發生執行緒安全性問題的程式碼需要上鎖

如果將synchronized鎖加在run方法上,那麼就會變成單執行緒,因為兩個執行緒有非公平鎖的特性,即誰拿到鎖/搶到鎖,誰就可以執行run方法,誰搶不到,誰就會一直阻塞等待。又因為run方法有死循環,不會釋放鎖,另一個執行緒就會一直阻塞等待

public class ThreadCount implements Runnable {
    private int count = 100;

    @Override
    public synchronized void run() {
        ...
    }

    public static void main(String[] args) {
        ThreadCount threadCount = new ThreadCount();
        //開啟執行緒
        new Thread(threadCount).start();
        new Thread(threadCount).start();
    }
}

因此在加鎖的時候並不是一次將整塊程式碼都上鎖,可能會使執行緒變為單執行緒,而且加鎖後,可能會影響程式的執行效率,因為執行該程式碼前要競爭鎖的資源。

正確加鎖

public class ThreadCount implements Runnable {
    private int count = 100;

    @Override
    public void run() {
        while (true){
            if (count > 1) {
                ...
                synchronized (this) {
                    count--;
                    System.out.println(Thread.currentThread().getName() + ":" + count);
                }
            }else{
                break;
            }
        }
    }

    public static void main(String[] args) {
        ThreadCount threadCount = new ThreadCount();
        //開啟執行緒
        new Thread(threadCount).start();	//執行緒0
        new Thread(threadCount).start();	//執行緒0
    }
}

執行緒0、執行緒1同時獲取this鎖,假設執行緒0獲取到this鎖,意味著執行緒1沒有獲取到鎖,則會阻塞等待。等執行緒0執行完count–,釋放鎖之後,就會喚醒執行緒1重新競爭鎖資源。

synchronized獲取鎖和釋放鎖底層已經由虛擬機實現,會自動獲取鎖、釋放鎖並喚醒其他阻塞執行緒競爭鎖資源。

2.2 synchronized鎖的基本用法

  1. 修飾程式碼塊,指定加鎖對象,對給定對象加鎖,進入同步程式碼塊前要獲得給定對象的鎖

    synchronized(對象鎖){ 需要保證執行緒安全的程式碼 }

    對象鎖需要保證是同一個對象

    比如:

    ThreadCount threadCount1 = new ThreadCount();
    ThreadCount threadCount2 = new ThreadCount();
    //開啟執行緒
    new Thread(threadCount1).start();
    new Thread(threadCount2).start();
    

    兩個執行緒並不是同一個對象鎖,這時也會出現執行緒安全問題

    @Override
    public void run() {
        while (true){
            cal();
        }
    }
    
    public void cal(){
        if (count > 1) {
            try {
                //運行狀態->休眠狀態——CPU的執行權讓給其他執行緒
                Thread.sleep(30);
            } catch (Exception e) {
                e.printStackTrace();
            }
            synchronized (this) {
                count--;
                System.out.println(Thread.currentThread().getName() + ":" + count);
            }
        }
    }
    
    public static void main(String[] args) {
        ThreadCount threadCount = new ThreadCount();
        //開啟執行緒
        new Thread(threadCount).start();
        new Thread(threadCount).start();
    }
    
  2. 修飾實例方法,作用與當前實例加鎖,進入同步程式碼前要獲得當前實例的鎖

    @Override
    public void run() {
        while (true) {
            if (count > 1) {
                try {
                    //運行狀態->休眠狀態——CPU的執行權讓給其他執行緒
                    Thread.sleep(30);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                cal();
            } else {
                break;
            }
        }
    }
    
    public synchronized void cal() {
        count--;
        System.out.println(Thread.currentThread().getName() + ":" + count);
    }
    

    將synchronized加在實例方法上,則默認使用的是this鎖

  3. 修飾靜態方法,作用於當前類對象(當前類.class)加鎖,進入同步程式碼前要獲得當前類對象的鎖

2.3 synchronized死鎖問題

我們如果在使用synchronized 需要注意 synchronized鎖嵌套的問題,避免死鎖的問題發生。

案例:

public class DeadlockThread implements Runnable {
    private int count = 1;
    private String lock = "lock";

    @Override
    public void run() {
        while (true) {
            count++;
            if (count % 2 == 0) {
                // 執行緒1需要獲取lock鎖 再獲取a方法this鎖
                // 執行緒2需要獲取this鎖 再獲取b方法lock鎖
                synchronized (lock) {
                    a();
                }
            } else {
                synchronized (this) {
                    b();
                }
            }
        }
    }

    public synchronized void a() {
        System.out.println(Thread.currentThread().getName() + ",a方法...");
    }

    public void b() {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + ",b方法...");
        }
    }

    public static void main(String[] args) {
        DeadlockThread deadlockThread = new DeadlockThread();
        Thread thread1 = new Thread(deadlockThread);
        Thread thread2 = new Thread(deadlockThread);
        thread1.start();
        thread2.start();
    }
}

執行緒1先獲取自定義對象的lock鎖,進入a方法需要獲取this鎖

執行緒2先獲取this鎖,進入b方法需要獲取自定義對象的lock鎖

當兩個執行緒同時執行,開始執行緒1和執行緒2分別拿到了lock鎖和this鎖,之後兩個執行緒都需要對方已經持有的鎖,最終出現死鎖問題。

如何排查synchronized死鎖問題

使用synchronized 死鎖診斷工具:JDK安裝目錄\jdk\jdk8\bin\jconsole.exe

image

image

3. 執行緒之間通訊

等待/通知機制

等待/通知的相關方法是任意Java對象都具備的,因為這些方法被定義在所有對象的超類java.lang.Object上,方法如下:

  • notify() :通知一個在對象上等待的執行緒,使其從main()方法返回,而返回的前提是該執行緒獲取到了對象的鎖
  • notifyAll():通知所有等待在該對象的執行緒
  • wait():調用該方法的執行緒進入WAITING狀態,只有等待其他執行緒的通知或者被中斷,才會返回。需要注意調用wait()方法後,會釋放對象的鎖 。

注意:wait,notify和notifyAll要與synchronized一起使用

wait/notify的簡單用法

public class Thread03 extends Thread {
    @Override
    public void run() {
        try {
            synchronized (this) {
                System.out.println(Thread.currentThread().getName() + ">>當前執行緒阻塞,同時釋放鎖!<<");
                this.wait();
            }
            System.out.println(">>run()<<");
        } catch (InterruptedException e) {

        }
    }

    public static void main(String[] args) {
        Thread03 thread = new Thread03();
        thread.start();
        try {
            Thread.sleep(3000);
            //3s後喚醒子執行緒
        } catch (Exception e) {

        }
        synchronized (thread) {
            // 喚醒正在阻塞的執行緒
            thread.notify();
        }
    }
}

多執行緒通訊實現生產者與消費者

看以下案例:

package com.mark.sunchronized;

/**
 * @author Mark
 * @version 1.0
 * @className Thread
 * @date 2022/11/6 18:41
 */
public class Thread04 {
    /**
     * 共享對象Res
     */
    class Res {
        /**
         * 姓名
         */
        private String userName;
        /**
         * 性別
         */
        private char sex;
    }

    /**
     * 輸入執行緒
     */
    class InputThread extends Thread {
        private Res res;

        public InputThread(Res res) {
            this.res = res;
        }

        @Override
        public void run() {
            int count = 0;
            while (true) {
                if (count == 0) {
                    res.userName = "張三";
                    res.sex = '男';
                } else {
                    res.userName = "李四";
                    res.sex = '女';
                }
                count = (count + 1) % 2;
            }
        }
    }

    /**
     * 輸出執行緒
     */
    class OutPutThread extends Thread {
        private Res res;

        public OutPutThread(Res res) {
            this.res = res;
        }

        @Override
        public void run() {
            while (true) {
                System.out.println(res.userName + "," + res.sex);
            }
        }
    }

    public static void main(String[] args) {
        new Thread04().print();
    }

    private void print() {
        //全局對象
        Res res = new Res();
        //輸入執行緒
        InputThread inputThread = new InputThread(res);
        //輸出執行緒
        OutPutThread outPutThread = new OutPutThread(res);
        inputThread.start();
        outPutThread.start();
    }
}

可以發現,輸入輸出執行緒公用Res對象,該程式存在執行緒安全問題。

修改:加synchronized鎖

/**
 * 輸入執行緒
 */
class InputThread extends Thread {
    private Res res;

    public InputThread(Res res) {
        this.res = res;
    }

    @Override
    public void run() {
        int count = 0;
        while (true) {
            synchronized (res) {
                if (count == 0) {
                    res.userName = "張三";
                    res.sex = '男';
                } else {
                    res.userName = "李四";
                    res.sex = '女';
                }
            }
            count = (count + 1) % 2;
        }
    }
}

/**
 * 輸出執行緒
 */
class OutPutThread extends Thread {
    private Res res;

    public OutPutThread(Res res) {
        this.res = res;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (res) {
                System.out.println(res.userName + "," + res.sex);
            }
        }
    }
}

那麼如何實現交替進行輸出,而不是一直在一段時間裡輸出相同的姓名性別?

在Res中添加一個flag標記,輸入執行緒為false,輸出執行緒為true

/**
     * 輸入執行緒
     */
    class InputThread extends Thread {
        private Res res;

        public InputThread(Res res) {
            this.res = res;
        }

        @Override
        public void run() {
            int count = 0;
            while (true) {
                synchronized (res) {
                    if (res.flag) {
                        try {
                            res.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    if (count == 0) {
                        res.userName = "張三";
                        res.sex = '男';
                    } else {
                        res.userName = "李四";
                        res.sex = '女';
                    }
                    res.flag = true;
                    //喚醒輸出執行緒
                    res.notify();
                }
                count = (count + 1) % 2;

            }
        }
    }

    /**
     * 輸出執行緒
     */
    class OutPutThread extends Thread {
        private Res res;

        public OutPutThread(Res res) {
            this.res = res;
        }

        @Override
        public void run() {
            while (true) {
                synchronized (res) {
                    //如果 res.flag = false 則輸出的執行緒主動釋放鎖 也就是讓輸出執行緒進入WAITING狀態,阻塞輸出執行緒
                    if (!res.flag) {
                        try {
                            res.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println(res.userName + "," + res.sex);
                    //輸出完畢,改變狀態
                    res.flag = false;
                    res.notify();
                }
            }
        }
    }
}

4. 多執行緒核心API

4.1 Join的底層原理

public static void main(String[] args){
        Thread t1 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",執行緒執行"), "t1");
        Thread t2 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",執行緒執行"), "t2");
        Thread t3 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",執行緒執行"), "t3");
        t1.start();
        t2.start();
        t3.start();
    }

執行上述程式碼發現,三個進程並不是按start的先後順序啟動。那麼如何實現三個執行緒按期望的順序去執行呢?

public static void main(String[] args) {
    Thread t1 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",執行緒執行"), "t1");
    Thread t2 = new Thread(() -> {
        try {
            //t1執行完才執行t2
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ",執行緒執行");
    }, "t2");
    Thread t3 = new Thread(() -> {
        try {
            //t2執行完才執行t3
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ",執行緒執行");
    }, "t3");
    t1.start();
    t2.start();
    t3.start();
}

Join底層原理是基於wait封裝的,喚醒的程式碼在jvm Hotspot 源碼中。jvm在關閉執行緒之前會檢測線阻塞在t1執行緒對象上的執行緒,然後執行notfyAll(),這樣t2就被喚醒了。

4.2 多執行緒的七種執行狀態

  • 初始化狀態
  • 就緒狀態
  • 運行狀態
  • 死亡狀態
  • 阻塞狀態
  • 等待狀態
  • 超時等待

image

  • start():調用start()方法會使得該執行緒開始執行,正確啟動執行緒的方式。、
  • wait():調用wait()方法,進入等待狀態,釋放資源,讓出CPU。需要在同步快中調用。
  • sleep():調用sleep()方法,進入超時等待,不釋放資源,讓出CPU
  • stop():調用sleep()方法,執行緒停止,執行緒不安全,不釋放鎖導致死鎖,過時。
  • join():調用sleep()方法,執行緒是同步,它可以使得執行緒之間的並行執行變為串列執行。
  • yield():暫停當前正在執行的執行緒對象,並執行其他執行緒,讓出CPU資源可能立刻獲得資源執行。yield()的目的是讓相同優先順序的執行緒之間能適當的輪轉執行
  • notify():在鎖池隨機喚醒一個執行緒。需要在同步快中調用。
  • notifyAll():喚醒鎖池裡所有的執行緒。需要在同步快中調用。

使用sleep方法避免cpu空轉 防止cpu佔用100%

sleep(long millis) 執行緒睡眠 millis 毫秒

sleep(long millis, int nanos) 執行緒睡眠 millis 毫秒 + nanos 納秒

public static void main(String[] args) {
    new Thread(() -> {
        while (true) {
            try {
              	//執行緒每隔30ms休眠一次
                Thread.sleep(30);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();
}

wait/join和sleep之間的區別

sleep(long)方法在睡眠時不釋放對象鎖

Wait(long)方法在等待的過程中釋放對象鎖

join(long)方法先執行另外的一個執行緒,在等待的過程中釋放對象鎖底層是基於wait封裝的

4.3 守護執行緒與用戶執行緒

java中執行緒分為兩種類型:用戶執行緒守護執行緒。通過Thread.setDaemon(false)設置為用戶執行緒;通過Thread.setDaemon(true)設置為守護執行緒。如果不設置屬性,默認為用戶執行緒。

  1. 守護執行緒依賴於用戶執行緒,用戶執行緒退出了,守護執行緒就會退出,典型的守護執行緒如垃圾回收執行緒。
  2. 用戶執行緒是獨立存在的,不會因為其他用戶執行緒退出而退出。

4.4 安全停止執行緒

  • 調用stop方法(不推薦)

    stop:中止執行緒,並且清除監控器鎖的資訊,但是可能導致執行緒安全問題,JDK不建議用。

    destroy: JDK未實現該方法。

  • Interrupt

    Interrupt 打斷正在運行或者正在阻塞的執行緒。

    1. 如果目標執行緒在調用Object class的wait()、wait(long)或wait(long, int)、join()、join(long, int)或sleep(long, int)方法時被阻塞,那麼Interrupt會生效,該執行緒的中斷狀態將被清除,拋出InterruptedException異常。

      public class Thread02 extends Thread {
          @Override
          public void run() {
              while (true) {
                  try {
                      System.out.println("1");
                      Thread.sleep(1000000);
                      System.out.println("2");
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
          }
      
          public static void main(String[] args) {
              Thread02 thread02 = new Thread02();
              thread02.start();
              try {
                  Thread.sleep(3000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println("中斷...");
              thread02.interrupt();
          }
      }
      
    2. 如果目標執行緒是被I/O或者NIO中的Channel所阻塞,同樣,I/O操作會被中斷或者返回特殊異常值。達到終止執行緒的目的。

    如果以上條件都不滿足,則會設置此執行緒的中斷狀態。

  • 標誌位

    在程式碼邏輯中,增加一個判斷,用來控制執行緒執行的中止。

    private volatile boolean isFlag = true;
    
        @Override
        public void run() {
            while (isFlag) {
    
            }
        }
    
        public static void main(String[] args) {
            Thread07 thread07 = new Thread07();
            thread07.start();
    //        thread07.isFlag = false;
        }
    

4.5 多執行緒優先順序

  1. 在java語言中,每個執行緒都有一個優先順序,當執行緒調控器有機會選擇新的執行緒時,執行緒的優先順序越高越有可能先被選擇執行,執行緒的優先順序可以設置1-10,數字越大代表優先順序越高

    注意:Oracle為Linux提供的java虛擬機中,執行緒的優先順序將被忽略,即所有執行緒具有相同的優先順序。

    所以,不要過度依賴優先順序。

  2. 執行緒的優先順序用數字來表示,默認範圍是1到10,即Thread.MIN_PRIORITY到Thread.MAX_PRIORTY.一個執行緒的默認優先順序是5,即Thread.NORM_PRIORTY

  3. 如果cpu非常繁忙時,優先順序越高的執行緒獲得更多的時間片,但是cpu空閑時,設置優先順序幾乎沒有任何作用。

public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        int count = 0;
        for (; ; ) {
            System.out.println(Thread.currentThread().getName() + "," + count++);
        }
    }, "t1執行緒:");
    Thread t2 = new Thread(() -> {
        int count = 0;
        for (; ; ) {
            System.out.println(Thread.currentThread().getName() + "," + count++);
        }
    }, "t2執行緒:");
    t1.setPriority(Thread.MIN_PRIORITY);
    t1.setPriority(Thread.MAX_PRIORITY);
    t1.start();
    t2.start();
}

5. Lock鎖的使用

在jdk1.5後新增的ReentrantLock類同樣可達到鎖的效果,且在使用上比synchronized更加靈活。

相關API:

  • 使用ReentrantLock實現同步
  • lock()方法:上鎖
  • unlock()方法:釋放鎖
  • 使用Condition實現等待/通知,類似於 wait()和notify()及notifyAll()
  • Lock鎖底層基於AQS實現,需要自己封裝實現自旋鎖。

Synchronized屬於JDK關鍵字,底層通過C++JVM虛擬機底層實現

Lock鎖底層基於AQS實現,變為重量級鎖

Synchronized底層原理:鎖的升級過程。推薦使用Synchronized鎖

使用Lock鎖過程中要注意獲取鎖、釋放鎖

5.1 ReentrantLock用法

使用synchronized獲取鎖和釋放鎖全部由虛擬機來完成

而使用Lock鎖需要手動獲取鎖和釋放鎖,需要開發者自己定義

public class Thread04 {
    /**
     * 定義鎖
     */
    private Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread04 thread04 = new Thread04();
        thread04.print1();
        try {
            Thread.sleep(500);
            System.out.println("開始執行執行緒2搶鎖");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread04.print2();

    }

    private void print1() {
        new Thread((() -> {
            //獲取鎖
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "獲取鎖成功");
        }), "t1").start();
    }

    public void print2() {
        new Thread((() -> {
          	System.out.println("1");
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "獲取鎖成功");
        }), "t2").start();
    }
}

/*
t1獲取鎖成功
開始執行執行緒2搶鎖
1
*/

上述程式中,t1未釋放鎖,則t2無法獲取鎖,阻塞。

因此在獲取鎖後要釋放鎖。

private void print1() {
    new Thread((() -> {
        try {
            //獲取鎖
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "獲取鎖成功");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }), "t1").start();
}

5.2 Condition用法

Condition介面提供了與Object阻塞(wait())與喚醒(notify()或notifyAll())相似的功能,只不過Condition介面提供了更為豐富的功能,如:限定等待時長等

public class Thread05 {
    private Lock lock = new ReentrantLock();
    /**
     * 定義
     */
    private Condition condition = lock.newCondition();

    public static void main(String[] args) {
        Thread05 thread05 = new Thread05();
        thread05.cal();
        try {
            Thread.sleep(3000);
        } catch (Exception e) {
        }
      	//釋放鎖
        thread05.signal();

    }

    public void signal() {
        try {
          	//獲取鎖
            lock.lock();
          	//喚醒執行緒
            condition.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void cal() {
        //喚醒執行緒
        new Thread(() -> {
            try {
                lock.lock();
                System.out.println("1");
                //釋放鎖,變為阻塞狀態
                condition.await();
                System.out.println("2");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
              	//釋放鎖
                lock.unlock();
            }
        }).start();
    }
}

6.多執行緒綜合案例實戰

6.1 執行緒安全性問題分析

分析執行緒安全性問題需要站在下面幾個維度考慮:

  1. 位元組碼角度

    JVM已經把底層封裝得很好,很難了解底層,因此需要從位元組碼彙編指令分析執行緒安全性問題

  2. 上下文切換

    單核CPU上的多執行緒,並不是真正意義上的多執行緒,而是執行緒切換實現多執行緒

  3. JMM java記憶體模型

public class Run extends Thread{
    private static int sum = 0;

    @Override
    public void run() {
        sum();
    }

    public void sum(){
        for (int i = 0 ; i <10000; i++){
            sum ++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Run run1 = new Run();
        Run run2 = new Run();
        run1.start();
        run2.start();
        run1.join();
        run2.join();
        System.out.println(sum);
    }
}

不考慮執行緒安全問題,上述程式碼應當輸出20000,然而,輸出的卻比20000小。

通過反編譯來查看過程:

  • target中找到Run.class文件
  • 打開Terminal,將Run.class所在目錄拖到Terminal
  • 輸入命令:javap -p -v Run.class

分析:

共享變數值 sum=0

假設現CPU執行到t1執行緒,t1執行緒執行完++但是還沒有保存sum,就切換到t2執行緒執行,t2執行緒將靜態變數sum=0改成sum=1,CPU又切換到t1執行緒,使用之前的sum++ 得到的sum=1賦值給共享變數sum,導致最終結果為sum1,然而現在sum++實際上已經執行了兩次,最終結果卻為1。

6.2 Callable和FutureTask原理分析

public interface MarkCallable<V> {
    /**
     * 當前執行緒執行完畢返回的結果
     * @return
     * @throws Exception
     */
    V call();
}
public class MarkFutureTask<V> implements Runnable {
    private MarkCallable<V> markCallable;
    private Object lock = new Object();
    private V result;

    public MarkFutureTask(MarkCallable<V> markCallable) {
        this.markCallable = markCallable;
    }

    @Override
    public void run() {
        //執行緒需要執行程式碼
        result = markCallable.call();
        //如果子執行緒執行完畢,喚醒主執行緒,可以拿到返回結果
        synchronized (lock) {
            lock.notify();
        }
    }

    public V get() {
        //獲取子執行緒非同步執行完畢後的返回結果
        //主執行緒阻塞
        synchronized (lock) {
            try {
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return result;
    }
}
public class MarkCallableImpl implements MarkCallable<Integer>{
    @Override
    public Integer call(){
        try {
            System.out.println(Thread.currentThread().getName()+",子執行緒執行");
            Thread.sleep(3000);
        }catch (Exception e){

        }
        //耗時程式碼執行完畢,返回1
        return 1;
    }
}
public static void main(String[] args) {
    MarkCallableImpl markCallable = new MarkCallableImpl();
    MarkFutureTask<Integer> markFutureTask = new MarkFutureTask<Integer>(markCallable);
    new Thread(markFutureTask).start();
    Integer result = markFutureTask.get();
    System.out.println(result);
}

使用LockSupport實現:

LockSupport:不需要實現synchronized即可實現wait和notify相似的操作

public class MarkFutureTask<V> implements Runnable {
    private MarkCallable<V> markCallable;
    private Object lock = new Object();
    private V result;
    private Thread currentThread;

    public MarkFutureTask(MarkCallable<V> markCallable) {
        this.markCallable = markCallable;
    }

    @Override
    public void run() {
        //執行緒需要執行程式碼
        result = markCallable.call();
        if (currentThread != null) {
            LockSupport.unpark(currentThread);
        }

    }

00    public V get() {
        //獲取子執行緒非同步執行完畢後的返回結果
        //主執行緒阻塞
        currentThread = Thread.currentThread();
        LockSupport.park();
        return result;
    }
}

7. ConcurrentHashMap

7.1 HashTable與HashMap的區別

  • 在多執行緒情況下,同時對一個共享HashMap使用put方法做寫操作,底層會共享一個table數組,發生執行緒安全問題,在多執行緒操作中,需要使用synchronized關鍵字。而HashTable執行緒是安全的,在每個公共方法上都使用了synchronized。
  • HashMap是允許key和value為null的,key為null的hash值為0,存在index=0的位置,而HashTable不允許key和value為空
  • HashMap需要重新計算hash值作為hashCode,而HashTable直接使用對象的hashCode
  • HashMap繼承了AbstractMap類,而HashTable繼承了Didtionary類

7.2 Hashtable集合的缺陷

  • 使用傳統的Hashtable保證執行緒問題,是採用synchronized鎖將整個Hashtable中的數組鎖住,在多執行緒中只允許一個執行緒訪問put或get,效率非常低,但是能夠保證執行緒安全問題。當多個執行緒對Hashtable在get或put時,會發生this鎖的競爭,多個執行緒競爭鎖,最終只會有一個執行緒獲取到this鎖,獲取不到的阻塞等待,最終只能單執行緒get/put。所以在多執行緒並不推薦使用Hashtable,因為其效率非常低。

7.3 ConcurrentHashMap1.7實現原理

數據結構實現:數組+Segments分段鎖+HashEntry鏈表實現

鎖的實現:Lock鎖+CAS樂觀鎖+UNSAFE類

擴容實現:支援多個Segment同時擴容

原理就是將大的Hashtable拆分成n多個小的Hashtable集合,默認16個。——分段鎖

分段鎖的核心思想是減少多個執行緒對鎖的競爭:不會再訪問到同一個Hashtable(每個小的HashTable都有一個獨立鎖,多個執行緒訪問大的Hashtable,會先根據key計算存放具體小的Hashtable的位置,然後進行操作)

ConcurrentHashMap get()方法沒有鎖的競爭,而Hashtable get()方法有鎖的競爭

而在JDK1.8取消了分段鎖。

在多執行緒情況下訪問ConcurrentHashMap1.7版本進行操作,如果多個執行緒操作的key最終計算落地到不同的小的Hashtable集合中,就可以實現多執行緒同時操作Hashtable而不會發生鎖的競爭。但是如果多個執行緒操作的key最終計算落地到同一個小的Hashtable集合中就會發生鎖的競爭。

(實際在ConcurrentHashMap中,並不是叫HashTable,而是叫Segments和Segment)

7.4 ConcurrentHashMap的使用

使用方法與HashMap一樣

7.5 手寫ConcurrentHashMap

  1. 提前創建固定數組容量大小的小的Hashtable集合
  2. 通過構造函數初始化Hashtable數組
public class MarkConcuurentHashMap<K, V> {
    /**
     * 創建一個存放小的HashTable集合
     */
    private Hashtable<K, V>[] hashTables;

    public MarkConcuurentHashMap() {
        //默認情況下 初始化16個小的HashTable
        hashTables = new Hashtable[16];

        for (int i = 0; i < hashTables.length; i++) {
            hashTables[i] = new Hashtable<>();
        }
    }

    public void put(K k, V v) {
        //先計算key存放到哪個具體小的HashTable集合中
        int hashTableIndex = k.hashCode() % hashTables.length;
        //將key存入到具體小的HashTable集合中
        hashTables[hashTableIndex].put(k, v);
    }

    public void get(K k) {
        //先計算key存放到了哪個具體小的HashTable集合中
        int hashTableIndex = k.hashCode() % hashTables.length;
        //根據key從具體小的HashTable集合中get
        hashTables[hashTableIndex].get(k);
    }
}

7.6 分段鎖設計概念

ConcurrentHashMap底層採用分段鎖設計,將一個大的HashTable執行緒安全的集合拆封成n多個小的HashTable集合,默認初始化16個小的HashTable集合。如果多個執行緒最終根據key計算出的index值落地到不同的小的HashTable集合,不會發生鎖的競爭,同時支援多個執行緒訪問ConcurrentHashMap進行寫的操作,效率非常高。

ConcurrentHashMap會計算兩次index值

  • 第一次計算index的值,計算key具體存放到哪個小的HashTable
  • 第二次計算index的值,計算key存放到具體小的HashTable對應具體數組index的哪個位置(HashTable底層也是通過數組+鏈表實現的)