多線程詳解

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底層也是通過數組+鏈表實現的)