Java修煉之道--併發編程

原做地址:https://github.com/frank-lam/2019_campus_applyjavascript

前言

在本文將總結多線程併發編程中的常見面試題,主要核心線程生命週期、線程通訊、併發包部分。主要分紅 「併發編程」 和 「面試指南」 兩 部分,在面試指南中將討論併發相關面經。html

參考資料:java

  • 《Java併發編程實戰》

第一部分:併發編程

1. 線程狀態轉換

在這裏插入圖片描述

新建(New)

建立後還沒有啓動。linux

可運行(Runnable)

可能正在運行,也可能正在等待 CPU 時間片。git

包含了操做系統線程狀態中的 運行(Running ) 和 就緒(Ready)。程序員

阻塞(Blocking)

這個狀態下,是在多個線程有同步操做的場景,好比正在等待另外一個線程的 synchronized 塊的執行釋放,或者可重入的 synchronized 塊裏別人調用 wait() 方法,也就是線程在等待進入臨界區。github

阻塞能夠分爲:等待阻塞,同步阻塞,其餘阻塞面試

無限期等待(Waiting)

等待其它線程顯式地喚醒,不然不會被分配 CPU 時間片。算法

進入方法 退出方法
沒有設置 Timeout 參數的 Object.wait() 方法 Object.notify() / Object.notifyAll()
沒有設置 Timeout 參數的 Thread.join() 方法 被調用的線程執行完畢
LockSupport.park() 方法 -

限期等待(Timed Waiting)

無需等待其它線程顯式地喚醒,在必定時間以後會被系統自動喚醒。數據庫

調用 Thread.sleep() 方法使線程進入限期等待狀態時,經常用 「使一個線程睡眠」 進行描述。

調用 Object.wait() 方法使線程進入限期等待或者無限期等待時,經常用 「掛起一個線程」 進行描述。

睡眠和掛起是用來描述行爲,而阻塞和等待用來描述狀態

阻塞和等待的區別在於,阻塞是被動的,它是在等待獲取一個排它鎖。而等待是主動的,經過調用 Thread.sleep() 和 Object.wait() 等方法進入。

進入方法 退出方法
Thread.sleep() 方法 時間結束
設置了 Timeout 參數的 Object.wait() 方法 時間結束 / Object.notify() / Object.notifyAll()
設置了 Timeout 參數的 Thread.join() 方法 時間結束 / 被調用的線程執行完畢
LockSupport.parkNanos() 方法 -
LockSupport.parkUntil() 方法 -

死亡(Terminated)

  • 線程由於 run 方法正常退出而天然死亡
  • 由於一個沒有捕獲的異常終止了 run 方法而意外死亡

2. Java實現多線程的方式及三種方式的區別

有三種使用線程的方法:

  • 實現 Runnable 接口;
  • 實現 Callable 接口;
  • 繼承 Thread 類。

實現 Runnable 和 Callable 接口的類只能當作一個能夠在線程中運行的任務,不是真正意義上的線程,所以最後還須要經過 Thread 來調用。能夠說任務是經過線程驅動從而執行的。

實現 Runnable 接口

須要實現 run() 方法。

經過 Thread 調用 start() 方法來啓動線程。

public class MyRunnable implements Runnable {
    public void run() {
        // ...
    }
}
public static void main(String[] args) {
    MyRunnable instance = new MyRunnable();
    Thread thread = new Thread(instance);
    thread.start();
}

實現 Callable 接口

與 Runnable 相比,Callable 能夠有返回值,返回值經過 FutureTask 進行封裝。

public class MyCallable implements Callable<Integer> {
    public Integer call() {
        return 123;
    }
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
    MyCallable mc = new MyCallable();
    FutureTask<Integer> ft = new FutureTask<>(mc);
    Thread thread = new Thread(ft);
    thread.start();
    System.out.println(ft.get());
}

繼承 Thread 類

一樣也是須要實現 run() 方法,由於 Thread 類也實現了 Runable 接口。

public class MyThread extends Thread {
    public void run() {
        // ...
    }
}
public static void main(String[] args) {
    MyThread mt = new MyThread();
    mt.start();
}

實現接口 VS 繼承 Thread

實現接口會更好一些,由於:

  • Java 不支持多重繼承,所以繼承了 Thread 類就沒法繼承其它類,可是能夠實現多個接口;
  • 類可能只要求可執行就行,繼承整個 Thread 類開銷過大。

三種方式的區別

  • 實現 Runnable 接口能夠避免 Java 單繼承特性而帶來的侷限;加強程序的健壯性,代碼可以被多個線程共享,代碼與數據是獨立的;適合多個相同程序代碼的線程區處理同一資源的狀況。
  • 繼承 Thread 類和實現 Runnable 方法啓動線程都是使用 start() 方法,而後 JVM 虛擬機將此線程放到就緒隊列中,若是有處理機可用,則執行 run() 方法。
  • 實現 Callable 接口要實現 call() 方法,而且線程執行完畢後會有返回值。其餘的兩種都是重寫 run() 方法,沒有返回值。

3. 基礎線程機制

Executor

Executor 管理多個異步任務的執行,而無需程序員顯式地管理線程的生命週期。這裏的異步是指多個任務的執行互不干擾,不須要進行同步操做。

主要有三種 Executor:

  • CachedThreadPool:一個任務建立一個線程;
  • FixedThreadPool:全部任務只能使用固定大小的線程;
  • SingleThreadExecutor:至關於大小爲 1 的 FixedThreadPool。
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < 5; i++) {
        executorService.execute(new MyRunnable());
    }
    executorService.shutdown();
}

爲何引入Executor線程池框架?

new Thread() 的缺點

  • 每次 new Thread() 耗費性能
  • 調用 new Thread() 建立的線程缺少管理,被稱爲野線程,並且能夠無限制建立,之間相互競爭,會致使過多佔用系統資源致使系統癱瘓。
  • 不利於擴展,好比如定時執行、按期執行、線程中斷

採用線程池的優勢

  • 重用存在的線程,減小對象建立、消亡的開銷,性能佳
  • 可有效控制最大併發線程數,提升系統資源的使用率,同時避免過多資源競爭,避免堵塞
  • 提供定時執行、按期執行、單線程、併發數控制等功能

Daemon(守護線程)

Java 中有兩類線程:User Thread (用戶線程)、Daemon Thread (守護線程)

用戶線程即運行在前臺的線程,而守護線程是運行在後臺的線程。 守護線程做用是爲其餘前臺線程的運行提供便利服務,並且僅在普通、非守護線程仍然運行時才須要,好比垃圾回收線程就是一個守護線程。當 JVM 檢測僅剩一個守護線程,而用戶線程都已經退出運行時,JVM 就會退出,由於沒有若是沒有了被守護這,也就沒有繼續運行程序的必要了。若是有非守護線程仍然存活,JVM 就不會退出。

守護線程並不是只有虛擬機內部提供,用戶在編寫程序時也能夠本身設置守護線程。用戶能夠用 Thread 的 setDaemon(true) 方法設置當前線程爲守護線程。

雖然守護線程可能很是有用,但必須當心確保其餘全部非守護線程消亡時,不會因爲它的終止而產生任何危害。由於你不可能知道在全部的用戶線程退出運行前,守護線程是否已經完成了預期的服務任務。一旦全部的用戶線程退出了,虛擬機也就退出運行了。 所以,不要在守護線程中執行業務邏輯操做(好比對數據的讀寫等)。

另外有幾點須要注意:

  • setDaemon(true) 必須在調用線程的 start() 方法以前設置,不然會跑出 IllegalThreadStateException 異常。
  • 在守護線程中產生的新線程也是守護線程。
  • 不要認爲全部的應用均可以分配給守護線程來進行服務,好比讀寫操做或者計算邏輯。

守護線程是程序運行時在後臺提供服務的線程,不屬於程序中不可或缺的部分。

當全部非守護線程結束時,程序也就終止,同時會殺死全部守護線程。

main() 屬於非守護線程。

使用 setDaemon() 方法將一個線程設置爲守護線程。

public static void main(String[] args) {
    Thread thread = new Thread(new MyRunnable());
    thread.setDaemon(true);
}

sleep()

Thread.sleep(millisec) 方法會休眠當前正在執行的線程,millisec 單位爲毫秒。

sleep() 可能會拋出 InterruptedException,由於異常不能跨線程傳播回 main() 中,所以必須在本地進行處理。線程中拋出的其它異常也一樣須要在本地進行處理。

public void run() {
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

yield()

對靜態方法 Thread.yield() 的調用聲明瞭當前線程已經完成了生命週期中最重要的部分,能夠切換給其它線程來執行。該方法只是對線程調度器的一個建議,並且也只是建議具備相同優先級的其它線程能夠運行。

public void run() {
    Thread.yield();
}

線程阻塞

線程能夠阻塞於四種狀態:

  • 當線程執行 Thread.sleep() 時,它一直阻塞到指定的毫秒時間以後,或者阻塞被另外一個線程打斷;
  • 當線程碰到一條 wait() 語句時,它會一直阻塞到接到通知 notify()、被中斷或通過了指定毫秒時間爲止(若制定了超時值的話)
  • 線程阻塞與不一樣 I/O 的方式有多種。常見的一種方式是 InputStream 的 read() 方法,該方法一直阻塞到從流中讀取一個字節的數據爲止,它能夠無限阻塞,所以不能指定超時時間;
  • 線程也能夠阻塞等待獲取某個對象鎖的排他性訪問權限(即等待得到 synchronized 語句必須的鎖時阻塞)。

注意,並不是全部的阻塞狀態都是可中斷的,以上阻塞狀態的前兩種能夠被中斷,後兩種不會對中斷作出反應

4. 中斷

一個線程執行完畢以後會自動結束,若是在運行過程當中發生異常也會提早結束。

InterruptedException

經過調用一個線程的 interrupt() 來中斷該線程,若是該線程處於阻塞、限期等待或者無限期等待狀態,那麼就會拋出 InterruptedException,從而提早結束該線程。可是不能中斷 I/O 阻塞和 synchronized 鎖阻塞。

對於如下代碼,在 main() 中啓動一個線程以後再中斷它,因爲線程中調用了 Thread.sleep() 方法,所以會拋出一個 InterruptedException,從而提早結束線程,不執行以後的語句。

public class InterruptExample {

    private static class MyThread1 extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(2000);
                System.out.println("Thread run");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public static void main(String[] args) throws InterruptedException {
    Thread thread1 = new MyThread1();
    thread1.start();
    thread1.interrupt();
    System.out.println("Main run");
}
Main run
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at InterruptExample.lambda$main$0(InterruptExample.java:5)
    at InterruptExample$$Lambda$1/713338599.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:745)

interrupted()

若是一個線程的 run() 方法執行一個無限循環,而且沒有執行 sleep() 等會拋出 InterruptedException 的操做,那麼調用線程的 interrupt() 方法就沒法使線程提早結束。

可是調用 interrupt() 方法會設置線程的中斷標記,此時調用 interrupted() 方法會返回 true。所以能夠在循環體中使用 interrupted() 方法來判斷線程是否處於中斷狀態,從而提早結束線程。

public class InterruptExample {

    private static class MyThread2 extends Thread {
        @Override
        public void run() {
            while (!interrupted()) {
                // ..
            }
            System.out.println("Thread end");
        }
    }
}
public static void main(String[] args) throws InterruptedException {
    Thread thread2 = new MyThread2();
    thread2.start();
    thread2.interrupt();
}
Thread end

Executor 的中斷操做

調用 Executor 的 shutdown() 方法會等待線程都執行完畢以後再關閉,可是若是調用的是 shutdownNow() 方法,則至關於調用每一個線程的 interrupt() 方法。

如下使用 Lambda 建立線程,至關於建立了一個匿名內部線程。

public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> {
        try {
            Thread.sleep(2000);
            System.out.println("Thread run");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    executorService.shutdownNow();
    System.out.println("Main run");
}
Main run
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at ExecutorInterruptExample.lambda$main$0(ExecutorInterruptExample.java:9)
    at ExecutorInterruptExample$$Lambda$1/1160460865.run(Unknown Source)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)

若是隻想中斷 Executor 中的一個線程,能夠經過使用 submit() 方法來提交一個線程,它會返回一個 Future<?> 對象,經過調用該對象的 cancel(true) 方法就能夠中斷線程。

Future<?> future = executorService.submit(() -> {
    // ..
});
future.cancel(true);

5. 互斥同步

Java 提供了兩種鎖機制來控制多個線程對共享資源的互斥訪問,第一個是 JVM 實現的 synchronized,而另外一個是 JDK 實現的 ReentrantLock。

synchronized

1. 同步一個代碼塊

public void func() {
    synchronized (this) {
        // ...
    }
}

它只做用於同一個對象,若是調用兩個對象上的同步代碼塊,就不會進行同步。

對於如下代碼,使用 ExecutorService 執行了兩個線程,因爲調用的是同一個對象的同步代碼塊,所以這兩個線程會進行同步,當一個線程進入同步語句塊時,另外一個線程就必須等待。

public class SynchronizedExample {
    public void func1() {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        }
    }
}
public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e1.func1());
}
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

對於如下代碼,兩個線程調用了不一樣對象的同步代碼塊,所以這兩個線程就不須要同步。從輸出結果能夠看出,兩個線程交叉執行。

public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e2.func1());
}
0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9

2. 同步一個方法

public synchronized void func () {
    // ...
}

它和同步代碼塊同樣,做用於同一個對象。

3. 同步一個類

public void func() {
    synchronized (SynchronizedExample.class) {
        // ...
    }
}

做用於整個類,也就是說兩個線程調用同一個類的不一樣對象上的這種同步語句,也會進行同步。

public class SynchronizedExample {
    public void func2() {
        synchronized (SynchronizedExample.class) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        }
    }
}
public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func2());
    executorService.execute(() -> e2.func2());
}
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

4. 同步一個靜態方法

  • 非靜態同步函數的鎖是:this
  • 靜態的同步函數的鎖是:字節碼對象
public synchronized static void fun() {
    // ...
}

做用於整個類。

ReentrantLock

重入鎖(ReentrantLock)是一種遞歸無阻塞的同步機制。

public class LockExample {
    private Lock lock = new ReentrantLock();

    public void func() {
        lock.lock();
        try {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        } finally {
            lock.unlock(); // 確保釋放鎖,從而避免發生死鎖。
        }
    }
}
public static void main(String[] args) {
    LockExample lockExample = new LockExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> lockExample.func());
    executorService.execute(() -> lockExample.func());
}
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

ReentrantLock 是 java.util.concurrent(J.U.C)包中的鎖,相比於 synchronized,它多瞭如下高級功能:

1. 等待可中斷

當持有鎖的線程長期不釋放鎖的時候,正在等待的線程能夠選擇放棄等待,改成處理其餘事情。

2. 可實現公平鎖

公平鎖是指多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次得到鎖。

synchronized 中的鎖是非公平的,ReentrantLock 默認狀況下也是非公平的,但能夠經過帶布爾值的構造函數要求使用公平鎖。

3. 鎖綁定多個條件

一個 ReentrantLock 對象能夠同時綁定多個 Condition 對象。

synchronized 和 ReentrantLock 比較

1. 鎖的實現

synchronized 是 JVM 實現的,而 ReentrantLock 是 JDK 實現的。

2. 性能

新版本 Java 對 synchronized 進行了不少優化,例如自旋鎖等。目前來看它和 ReentrantLock 的性能基本持平了,所以性能因素再也不是選擇 ReentrantLock 的理由。synchronized 有更大的性能優化空間,應該優先考慮 synchronized。

3. 功能

ReentrantLock 多了一些高級功能。

4. 使用選擇

除非須要使用 ReentrantLock 的高級功能,不然優先使用 synchronized。這是由於 synchronized 是 JVM 實現的一種鎖機制,JVM 原生地支持它,而 ReentrantLock 不是全部的 JDK 版本都支持。而且使用 synchronized 不用擔憂沒有釋放鎖而致使死鎖問題,由於 JVM 會確保鎖的釋放。

synchronized與lock的區別,使用場景。看過synchronized的源碼沒?

  • (用法)synchronized(隱式鎖):在須要同步的對象中加入此控制,synchronized 能夠加在方法上,也能夠加在特定代碼塊中,括號中表示須要鎖的對象。
  • (用法)lock(顯示鎖):須要顯示指定起始位置和終止位置。通常使用 ReentrantLock 類作爲鎖,多個線程中必需要使用一個 ReentrantLock 類作爲對象才能保證鎖的生效。且在加鎖和解鎖處須要經過 lock() 和 unlock() 顯示指出。因此通常會在 finally 塊中寫 unlock() 以防死鎖。
  • (性能)synchronized 是託管給 JVM 執行的,而 lock 是 Java 寫的控制鎖的代碼。在 Java1.5 中,synchronize 是性能低效的。由於這是一個重量級操做,須要調用操做接口,致使有可能加鎖消耗的系統時間比加鎖之外的操做還多。相比之下使用 Java 提供的 Lock 對象,性能更高一些。可是到了 Java1.6 ,發生了變化。synchronize 在語義上很清晰,能夠進行不少優化,有適應自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。致使 在 Java1.6 上 synchronize 的性能並不比 Lock 差。
  • (機制)synchronized 原始採用的是 CPU 悲觀鎖機制,即線程得到的是獨佔鎖。獨佔鎖意味着其餘線程只能依靠阻塞來等待線程釋放鎖。Lock 用的是樂觀鎖方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操做,若是由於衝突失敗就重試,直到成功爲止。樂觀鎖實現的機制就是 CAS 操做(Compare and Swap)。

什麼是CAS

蘑菇街面試,這裏簡單論述一下

入門例子

在 Java 併發包中有這樣一個包,java.util.concurrent.atomic,該包是對 Java 部分數據類型的原子封裝,在原有數據類型的基礎上,提供了原子性的操做方法,保證了線程安全。下面以 AtomicInteger 爲例,來看一下是如何實現的。

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

public final int decrementAndGet() {
    for (;;) {
        int current = get();
        int next = current - 1;
        if (compareAndSet(current, next))
            return next;
    }
}

以這兩個方法爲例,incrementAndGet 方法至關於原子性的 ++i,decrementAndGet 方法至關於原子性的 --i,這兩個方法中都沒有使用阻塞式的方式來保證原子性(如 Synchronized ),那它們是如何保證原子性的呢,下面引出 CAS。

Compare And Swap

CAS 指的是現代 CPU 普遍支持的一種對內存中的共享數據進行操做的一種特殊指令。這個指令會對內存中的共享數據作原子的讀寫操做。

簡單介紹一下這個指令的操做過程:

  • 首先,CPU 會將內存中將要被更改的數據與指望的值作比較。
  • 而後,當這兩個值相等時,CPU 纔會將內存中的數值替換爲新的值。不然便不作操做。
  • 最後,CPU 會將舊的數值返回。

這一系列的操做是原子的。它們雖然看似複雜,但倒是 Java 5 併發機制優於原有鎖機制的根本。簡單來講,CAS 的含義是:我認爲原有的值應該是什麼,若是是,則將原有的值更新爲新值,不然不作修改,並告訴我原來的值是多少。
​ 簡單的來講,CAS 有 3 個操做數,內存值 V,舊的預期值 A,要修改的新值 B。當且僅當預期值 A 和內存值 V 相同時,將內存值 V 修改成 B,不然返回 V。這是一種樂觀鎖的思路,它相信在它修改以前,沒有其它線程去修改它;而 Synchronized 是一種悲觀鎖,它認爲在它修改以前,必定會有其它線程去修改它,悲觀鎖效率很低。

什麼是樂觀鎖和悲觀鎖

  • 爲何須要鎖(併發控制)
    • 在多用戶環境中,在同一時間可能會有多個用戶更新相同的記錄,這會產生衝突。這就是著名的併發性問題。
    • 典型的衝突有:
      • 丟失更新:一個事務的更新覆蓋了其它事務的更新結果,就是所謂的更新丟失。例如:用戶 A 把值從 6 改成 2,用戶 B 把值從 2 改成 6,則用戶 A 丟失了他的更新。
      • 髒讀:當一個事務讀取其它完成一半事務的記錄時,就會發生髒讀取。例如:用戶 A,B 看到的值都是6,用戶 B 把值改成 2,用戶 A 讀到的值仍爲 6。
    • 爲了解決這些併發帶來的問題。 咱們須要引入併發控制機制。
  • 併發控制機制
    • 悲觀鎖:假定會發生併發衝突,獨佔鎖,屏蔽一切可能違反數據完整性的操做。
    • 樂觀鎖:假設不會發生併發衝突,只在提交操做時檢查是否違反數據完整性。樂觀鎖不能解決髒讀的問題。

參考資料:

Synchronized(對象鎖)和Static Synchronized(類鎖)區別

  • 一個是實例鎖(鎖在某一個實例對象上,若是該類是單例,那麼該鎖也具備全局鎖的概念),一個是全局鎖(該鎖針對的是類,不管實例多少個對象,那麼線程都共享該鎖)。

    實例鎖對應的就是 synchronized關 鍵字,而類鎖(全局鎖)對應的就是 static synchronized(或者是鎖在該類的 class 或者 classloader 對象上)。

/**
 * static synchronized 和synchronized的區別!
 * 關鍵是區別第四種狀況!
 */
public class StaticSynchronized {

    /**
     * synchronized方法
     */
    public synchronized void isSynA(){
        System.out.println("isSynA");
    }
    public synchronized void isSynB(){
        System.out.println("isSynB");
    }

    /**
     * static synchronized方法
     */
    public static synchronized void cSynA(){
        System.out.println("cSynA");
    }
    public static synchronized void cSynB(){
        System.out.println("cSynB");
    }

    public static void main(String[] args) {
        StaticSynchronized x = new StaticSynchronized();
        StaticSynchronized y = new StaticSynchronized();
        /**
         *  x.isSynA()與x.isSynB(); 不能同時訪問(同一個對象訪問synchronized方法)
         *  x.isSynA()與y.isSynB(); 能同時訪問(不一樣對象訪問synchronized方法)
         *  x.cSynA()與y.cSynB(); 不能同時訪問(不一樣對象也不能訪問static synchronized方法)
         *  x.isSynA()與y.cSynA(); 能同時訪問(static synchronized方法佔用的是類鎖,
         *                        而訪問synchronized方法佔用的是對象鎖,不存在互斥現象)
         */
    }
}

6. 線程之間的協做

當多個線程能夠一塊兒工做去解決某個問題時,若是某些部分必須在其它部分以前完成,那麼就須要對線程進行協調。

join()

在線程中調用另外一個線程的 join() 方法,會將當前線程掛起,而不是忙等待,直到目標線程結束。

對於如下代碼,雖然 b 線程先啓動,可是由於在 b 線程中調用了 a 線程的 join() 方法,b 線程會等待 a 線程結束才繼續執行,所以最後可以保證 a 線程的輸出先於 b 線程的輸出。

public class JoinExample {
    private class A extends Thread {
        @Override
        public void run() {
            System.out.println("A");
        }
    }

    private class B extends Thread {
        private A a;
        B(A a) {
            this.a = a;
        }

        @Override
        public void run() {
            try {
                a.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("B");
        }
    }

    public void test() {
        A a = new A();
        B b = new B(a);
        b.start();
        a.start();
    }
}
public static void main(String[] args) {
    JoinExample example = new JoinExample();
    example.test();
}
A
B

wait() notify() notifyAll()

調用 wait() 使得線程等待某個條件知足,線程在等待時會被掛起,當其餘線程的運行使得這個條件知足時,其它線程會調用 notify()(隨機叫醒一個) 或者 notifyAll() (叫醒全部 wait 線程,爭奪時間片的線程只有一個)來喚醒掛起的線程。

它們都屬於 Object 的一部分,而不屬於 Thread。

只能用在同步方法或者同步控制塊中使用!不然會在運行時拋出 IllegalMonitorStateExeception。

使用 wait() 掛起期間,線程會釋放鎖。這是由於,若是沒有釋放鎖,那麼其它線程就沒法進入對象的同步方法或者同步控制塊中,那麼就沒法執行 notify() 或者 notifyAll() 來喚醒掛起的線程,形成死鎖。

public class WaitNotifyExample {
    public synchronized void before() {
        System.out.println("before");
        notifyAll();
    }

    public synchronized void after() {
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("after");
    }
}
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    WaitNotifyExample example = new WaitNotifyExample();
    executorService.execute(() -> example.after());
    executorService.execute(() -> example.before());
}
before
after

await() signal() signalAll()

java.util.concurrent 類庫中提供了 Condition 類來實現線程之間的協調,能夠在 Condition 上調用 await() 方法使線程等待,其它線程調用 signal() 或 signalAll() 方法喚醒等待的線程。相比於 wait() 這種等待方式,await() 能夠指定等待的條件,所以更加靈活。

使用 Lock 來獲取一個 Condition 對象。

public class AwaitSignalExample {
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void before() {
        lock.lock();
        try {
            System.out.println("before");
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void after() {
        lock.lock();
        try {
            condition.await();
            System.out.println("after");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    AwaitSignalExample example = new AwaitSignalExample();
    executorService.execute(() -> example.after());
    executorService.execute(() -> example.before());
}
before
after

sleep和wait有什麼區別

  • sleep 和 wait
    • wait() 是 Object 的方法,而 sleep() 是 Thread 的靜態方法;
    • wait() 會釋放鎖,sleep() 不會。
  • 有什麼區別
    • sleep() 方法(休眠)是線程類(Thread)的靜態方法,調用此方法會讓當前線程暫停執行指定的時間,將執行機會(CPU)讓給其餘線程,可是對象的鎖依然保持,所以休眠時間結束後會自動恢復(線程回到就緒狀態)。
    • wait() 是 Object 類的方法,調用對象的 wait() 方法致使當前線程放棄對象的鎖(線程暫停執行),進入對象的等待池(wait pool),只有調用對象的 notify() 方法(或 notifyAll() 方法)時才能喚醒等待池中的線程進入等鎖池(lock pool),若是線程從新得到對象的鎖就能夠進入就緒狀態。

7. J.U.C - AQS

AQS 是 AbstractQueuedSynchronizer 的簡稱,java.util.concurrent(J.U.C)大大提升了併發性能,AQS (AbstractQueuedSynchronizer) 被認爲是 J.U.C 的核心。它提供了一個基於 FIFO 隊列,這個隊列能夠用來構建鎖或者其餘相關的同步裝置的基礎框架。下圖是 AQS 底層的數據結構:

它底層使用的是雙向列表,是隊列的一種實現 , 所以也能夠將它當成一種隊列。

  • Sync queue 是同步列表,它是雙向列表 , 包括 head,tail 節點。其中 head 節點主要用來後續的調度 ;
  • Condition queue 是單向鏈表 , 不是必須的 , 只有當程序中須要 Condition 的時候,纔會存在這個單向鏈表 , 而且可能會有多個 Condition queue。

簡單的來講:

  • AQS其實就是一個能夠給咱們實現鎖的框架

  • 內部實現的關鍵是:先進先出的隊列、state 狀態

  • 定義了內部類 ConditionObject

  • 擁有兩種線程模式

    • 獨佔模式
    • 共享模式
  • 在 LOCK 包中的相關鎖(經常使用的有 ReentrantLock、 ReadWriteLock )都是基於 AQS 來構建
  • 通常咱們叫 AQS 爲同步器。

CountdownLatch

CountDownLatch 類位於 java.util.concurrent 包下,利用它能夠實現相似計數器的功能。好比有一個任務 A,它要等待其餘 4 個任務執行完畢以後才能執行,此時就能夠利用 CountDownLatch 來實現這種功能了。

維護了一個計數器 cnt,每次調用 countDown() 方法會讓計數器的值減 1,減到 0 的時候,那些由於調用 await() 方法而在等待的線程就會被喚醒。

CountDownLatch 類只提供了一個構造器:

public CountDownLatch(int count) {  };  // 參數count爲計數值

而後下面這 3 個方法是 CountDownLatch 類中最重要的方法:

`public` `void` `await() ``throws` `InterruptedException { };   ``//調用await()方法的線程會被掛起,它會等待直到count值爲0才繼續執行``public` `boolean` `await(``long` `timeout, TimeUnit unit) ``throws` `InterruptedException { };  ``//和await()相似,只不過等待必定的時間後count值還沒變爲0的話就會繼續執行``public` `void` `countDown() { };  ``//將count值減1`

下面看一個例子你們就清楚 CountDownLatch 的用法了:

`public` `class` `Test {``     ``public` `static` `void` `main(String[] args) {   ``         ``final` `CountDownLatch latch = ``new` `CountDownLatch(``2``);``         ` `         ``new` `Thread(){``             ``public` `void` `run() {``                 ``try` `{``                     ``System.out.println(``"子線程"``+Thread.currentThread().getName()+``"正在執行"``);``                    ``Thread.sleep(``3000``);``                    ``System.out.println(``"子線程"``+Thread.currentThread().getName()+``"執行完畢"``);``                    ``latch.countDown();``                ``} ``catch` `(InterruptedException e) {``                    ``e.printStackTrace();``                ``}``             ``};``         ``}.start();``         ` `         ``new` `Thread(){``             ``public` `void` `run() {``                 ``try` `{``                     ``System.out.println(``"子線程"``+Thread.currentThread().getName()+``"正在執行"``);``                     ``Thread.sleep(``3000``);``                     ``System.out.println(``"子線程"``+Thread.currentThread().getName()+``"執行完畢"``);``                     ``latch.countDown();``                ``} ``catch` `(InterruptedException e) {``                    ``e.printStackTrace();``                ``}``             ``};``         ``}.start();``         ` `         ``try` `{``             ``System.out.println(``"等待2個子線程執行完畢..."``);``            ``latch.await();``            ``System.out.println(``"2個子線程已經執行完畢"``);``            ``System.out.println(``"繼續執行主線程"``);``        ``} ``catch` `(InterruptedException e) {``            ``e.printStackTrace();``        ``}``     ``}``}`

執行結果:

線程Thread-0正在執行
線程Thread-1正在執行
等待2個子線程執行完畢...
線程Thread-0執行完畢
線程Thread-1執行完畢
2個子線程已經執行完畢
繼續執行主線程

CyclicBarrier

用來控制多個線程互相等待,只有當多個線程都到達時,這些線程纔會繼續執行。

和 CountdownLatch 類似,都是經過維護計數器來實現的。可是它的計數器是遞增的,每次執行 await() 方法以後,計數器會加 1,直到計數器的值和設置的值相等,等待的全部線程纔會繼續執行。和 CountdownLatch 的另外一個區別是,CyclicBarrier 的計數器能夠循環使用,因此它才叫作循環屏障。

下圖應該從下往上看才正確。

public class CyclicBarrierExample {
    public static void main(String[] args) throws InterruptedException {
        final int totalThread = 10;
        CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < totalThread; i++) {
            executorService.execute(() -> {
                System.out.print("before..");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.print("after..");
            });
        }
        executorService.shutdown();
    }
}
before..before..before..before..before..before..before..before..before..before..after..after..after..after..after..after..after..after..after..after..

Semaphore

Semaphore 就是操做系統中的信號量,能夠控制對互斥資源的訪問線程數。Semaphore 能夠控同時訪問的線程個數,經過 acquire() 獲取一個許可,若是沒有就等待,而 release() 釋放一個許可。


Semaphore 類位於 java.util.concurrent 包下,它提供了2個構造器:

`public` `Semaphore(``int` `permits) {          ``//參數permits表示許可數目,即同時能夠容許多少線程進行訪問``    ``sync = ``new` `NonfairSync(permits);``}``public` `Semaphore(``int` `permits, ``boolean` `fair) {    ``//這個多了一個參數fair表示是不是公平的,即等待時間越久的越先獲取許可``    ``sync = (fair)? ``new` `FairSync(permits) : ``new` `NonfairSync(permits);``}`

下面說一下 Semaphore 類中比較重要的幾個方法,首先是 acquire()、release() 方法:

`public` `void` `acquire() ``throws` `InterruptedException {  }     ``//獲取一個許可``public` `void` `acquire(``int` `permits) ``throws` `InterruptedException { }    ``//獲取permits個許可``public` `void` `release() { }          ``//釋放一個許可``public` `void` `release(``int` `permits) { }    ``//釋放permits個許可`

  acquire() 用來獲取一個許可,若無許可可以得到,則會一直等待,直到得到許可。

  release() 用來釋放許可。注意,在釋放許可以前,必須先獲得到許可。

這 4 個方法都會被阻塞,若是想當即獲得執行結果,能夠使用下面幾個方法:

`public` `boolean` `tryAcquire() { };    ``//嘗試獲取一個許可,若獲取成功,則當即返回true,若獲取失敗,則當即返回false``public` `boolean` `tryAcquire(``long` `timeout, TimeUnit unit) ``throws` `InterruptedException { };  ``//嘗試獲取一個許可,若在指定的時間內獲取成功,則當即返回true,不然則當即返回false``public` `boolean` `tryAcquire(``int` `permits) { }; ``//嘗試獲取permits個許可,若獲取成功,則當即返回true,若獲取失敗,則當即返回false``public` `boolean` `tryAcquire(``int` `permits, ``long` `timeout, TimeUnit unit) ``throws` `InterruptedException { }; ``//嘗試獲取permits個許可,若在指定的時間內獲取成功,則當即返回true,不然則當即返回false`

  另外還能夠經過 availablePermits() 方法獲得可用的許可數目。

  下面經過一個例子來看一下 Semaphore 的具體使用:

  倘若一個工廠有 5 臺機器,可是有 8 個工人,一臺機器同時只能被一個工人使用,只有使用完了,其餘工人才能繼續使用。那麼咱們就能夠經過 Semaphore 來實現:

`public` `class` `Test {``    ``public` `static` `void` `main(String[] args) {``        ``int` `N = ``8``;            ``//工人數``        ``Semaphore semaphore = ``new` `Semaphore(``5``); ``//機器數目``        ``for``(``int` `i=``0``;i<N;i++)``            ``new` `Worker(i,semaphore).start();``    ``}``    ` `    ``static` `class` `Worker ``extends` `Thread{``        ``private` `int` `num;``        ``private` `Semaphore semaphore;``        ``public` `Worker(``int` `num,Semaphore semaphore){``            ``this``.num = num;``            ``this``.semaphore = semaphore;``        ``}``        ` `        ``@Override``        ``public` `void` `run() {``            ``try` `{``                ``semaphore.acquire();``                ``System.out.println(``"工人"``+``this``.num+``"佔用一個機器在生產..."``);``                ``Thread.sleep(``2000``);``                ``System.out.println(``"工人"``+``this``.num+``"釋放出機器"``);``                ``semaphore.release();           ``            ``} ``catch` `(InterruptedException e) {``                ``e.printStackTrace();``            ``}``        ``}``    ``}``}`

執行結果:

工人0佔用一個機器在生產...
工人1佔用一個機器在生產...
工人2佔用一個機器在生產...
工人4佔用一個機器在生產...
工人5佔用一個機器在生產...
工人0釋放出機器
工人2釋放出機器
工人3佔用一個機器在生產...
工人7佔用一個機器在生產...
工人4釋放出機器
工人5釋放出機器
工人1釋放出機器
工人6佔用一個機器在生產...
工人3釋放出機器
工人7釋放出機器
工人6釋放出機器

總結

下面對上面說的三個輔助類進行一個總結:

  • CountDownLatch 和 CyclicBarrier 都可以實現線程之間的等待,只不過它們側重點不一樣:
    • CountDownLatch 通常用於某個線程A等待若干個其餘線程執行完任務以後,它才執行;
    • CyclicBarrier 通常用於一組線程互相等待至某個狀態,而後這一組線程再同時執行;
    • 另外,CountDownLatch 是不可以重用的,而 CyclicBarrier 是能夠重用的。
  • Semaphore 其實和鎖有點相似,它通常用於控制對某組資源的訪問權限。

8. J.U.C - 其它組件

FutureTask

在介紹 Callable 時咱們知道它能夠有返回值,返回值經過 Future 進行封裝。FutureTask 實現了 RunnableFuture 接口,該接口繼承自 Runnable 和 Future 接口,這使得 FutureTask 既能夠當作一個任務執行,也能夠有返回值。

public class FutureTask<V> implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable, Future<V>

FutureTask 可用於異步獲取執行結果或取消執行任務的場景。當一個計算任務須要執行很長時間,那麼就能夠用 FutureTask 來封裝這個任務,主線程在完成本身的任務以後再去獲取結果。

public class FutureTaskExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int result = 0;
                for (int i = 0; i < 100; i++) {
                    Thread.sleep(10);
                    result += i;
                }
                return result;
            }
        });

        Thread computeThread = new Thread(futureTask);
        computeThread.start();

        Thread otherThread = new Thread(() -> {
            System.out.println("other task is running...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        otherThread.start();
        System.out.println(futureTask.get());
    }
}
other task is running...
4950

BlockingQueue

java.util.concurrent.BlockingQueue 接口有如下阻塞隊列的實現:

  • FIFO 隊列 :LinkedBlockingQueue、ArrayBlockingQueue(固定長度)
  • 優先級隊列 :PriorityBlockingQueue

提供了阻塞的 take() 和 put() 方法:若是隊列爲空 take() 將阻塞,直到隊列中有內容;若是隊列爲滿 put() 將阻塞,直到隊列有空閒位置。

使用 BlockingQueue 實現生產者消費者問題

public class ProducerConsumer {

    private static BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);

    private static class Producer extends Thread {
        @Override
        public void run() {
            try {
                queue.put("product");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.print("produce..");
        }
    }

    private static class Consumer extends Thread {

        @Override
        public void run() {
            try {
                String product = queue.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.print("consume..");
        }
    }
}
public static void main(String[] args) {
    for (int i = 0; i < 2; i++) {
        Producer producer = new Producer();
        producer.start();
    }
    for (int i = 0; i < 5; i++) {
        Consumer consumer = new Consumer();
        consumer.start();
    }
    for (int i = 0; i < 3; i++) {
        Producer producer = new Producer();
        producer.start();
    }
}
produce..produce..consume..consume..produce..consume..produce..consume..produce..consume..

ForkJoin

主要用於並行計算中,和 MapReduce 原理相似,都是把大的計算任務拆分紅多個小任務並行計算。

public class ForkJoinExample extends RecursiveTask<Integer> {
    private final int threshold = 5;
    private int first;
    private int last;

    public ForkJoinExample(int first, int last) {
        this.first = first;
        this.last = last;
    }

    @Override
    protected Integer compute() {
        int result = 0;
        if (last - first <= threshold) {
            // 任務足夠小則直接計算
            for (int i = first; i <= last; i++) {
                result += i;
            }
        } else {
            // 拆分紅小任務
            int middle = first + (last - first) / 2;
            ForkJoinExample leftTask = new ForkJoinExample(first, middle);
            ForkJoinExample rightTask = new ForkJoinExample(middle + 1, last);
            leftTask.fork();
            rightTask.fork();
            result = leftTask.join() + rightTask.join();
        }
        return result;
    }
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
    ForkJoinExample example = new ForkJoinExample(1, 10000);
    ForkJoinPool forkJoinPool = new ForkJoinPool();
    Future result = forkJoinPool.submit(example);
    System.out.println(result.get());
}

ForkJoin 使用 ForkJoinPool 來啓動,它是一個特殊的線程池,線程數量取決於 CPU 核數。

public class ForkJoinPool extends AbstractExecutorService

ForkJoinPool 實現了工做竊取算法來提升 CPU 的利用率。每一個線程都維護了一個雙端隊列,用來存儲須要執行的任務。工做竊取算法容許空閒的線程從其它線程的雙端隊列中竊取一個任務來執行。竊取的任務必須是最晚的任務,避免和隊列所屬線程發生競爭。例以下圖中,Thread2 從 Thread1 的隊列中拿出最晚的 Task1 任務,Thread1 會拿出 Task2 來執行,這樣就避免發生競爭。可是若是隊列中只有一個任務時仍是會發生競爭。


9. 線程不安全示例

若是多個線程對同一個共享數據進行訪問而不採起同步操做的話,那麼操做的結果是不一致的。

如下代碼演示了 1000 個線程同時對 cnt 執行自增操做,操做結束以後它的值爲 997 而不是 1000。

public class ThreadUnsafeExample {
    private int cnt = 0;
    public void add() {
        cnt++;
    }
    public int get() {
        return cnt;
    }
}

public static void main(String[] args) throws InterruptedException {
    final int threadSize = 1000;
    ThreadUnsafeExample example = new ThreadUnsafeExample();
    final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < threadSize; i++) {
        executorService.execute(() -> {
            example.add();
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println(example.get());
}
997

10. Java 內存模型(JMM)

Java 內存模型試圖屏蔽各類硬件和操做系統的內存訪問差別,以實現讓 Java 程序在各類平臺下都能達到一致的內存訪問效果。

主內存與工做內存

處理器上的寄存器的讀寫的速度比內存快幾個數量級,爲了解決這種速度矛盾,在它們之間加入了高速緩存。

加入高速緩存帶來了一個新的問題:緩存一致性。若是多個緩存共享同一塊主內存區域,那麼多個緩存的數據可能會不一致,須要一些協議來解決這個問題。

全部的變量都存儲在主內存中,每一個線程還有本身的工做內存,工做內存存儲在高速緩存或者寄存器中,保存了該線程使用的變量的主內存副本拷貝。

線程只能直接操做工做內存中的變量,不一樣線程之間的變量值傳遞須要經過主內存來完成。

Java內存模型和硬件關係圖

Java內存模型抽象結構圖

內存間交互操做

Java 內存模型定義了 8 個操做來完成主內存和工做內存的交互操做。

  • read:把一個變量的值從主內存傳輸到工做內存中
  • load:在 read 以後執行,把 read 獲得的值放入工做內存的變量副本中
  • use:把工做內存中一個變量的值傳遞給執行引擎
  • assign:把一個從執行引擎接收到的值賦給工做內存的變量
  • store:把工做內存的一個變量的值傳送到主內存中
  • write:在 store 以後執行,把 store 獲得的值放入主內存的變量中
  • lock:做用於主內存的變量,把一個變量標識爲一條線程獨佔狀態
  • unlock:做用於主內存變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定

若是要把一個變量從主內存中複製到工做內存,就須要按順尋地執行 read 和 load 操做,若是把變量從工做內存中同步回主內存中,就要按順序地執行 store 和 write 操做。Java內存模型只要求上述操做必須按順序執行,而沒有保證必須是連續執行。也就是 read 和 load 之間,store 和 write 之間是能夠插入其餘指令的,如對主內存中的變量a、b進行訪問時,可能的順序是read a,read b,load b, load a。

Java內存模型還規定了在執行上述8種基本操做時,必須知足以下規則:

  • 不容許 read 和 load、store 和 write 操做之一單獨出現
  • 不容許一個線程丟棄它的最近 assign 的操做,即變量在工做內存中改變了以後必須同步到主內存中
  • 不容許一個線程無緣由地(沒有發生過任何assign操做)把數據從工做內存同步回主內存中
  • 一個新的變量只能在主內存中誕生,不容許在工做內存中直接使用一個未被初始化(load 或 assign)的變量。即就是對一個變量實施 use 和 store 操做以前,必須先執行過了 assign 和 load 操做。
  • 一個變量在同一時刻只容許一條線程對其進行lock操做,lock 和 unlock必須成對出現
  • 若是對一個變量執行 lock 操做,將會清空工做內存中此變量的值,在執行引擎使用這個變量前須要從新執行 load 或 assign 操做初始化變量的值
  • 若是一個變量事先沒有被 lock 操做鎖定,則不容許對它執行 unlock 操做;也不容許去 unlock 一個被其餘線程鎖定的變量。
  • 對一個變量執行 unlock 操做以前,必須先把此變量同步到主內存中(執行 store 和 write 操做)。

參考資料:

volatile關鍵字與Java內存模型(JMM) - yzwall - 博客園

內存模型三大特性

1. 原子性

  • 概念
    • 事物有原子性,這個概念大概都清楚,即一個操做或多個操做要麼執行的過程當中不被任何因素打斷,要麼不執行。
  • 如何實現原子性?
    • 經過同步代碼塊 synchronized 或者 local 鎖來確保原子性

Java 內存模型保證了 read、load、use、assign、store、write、lock 和 unlock 操做具備原子性,例如對一個 int 類型的變量執行 assign 賦值操做,這個操做就是原子性的。可是 Java 內存模型容許虛擬機將沒有被 volatile 修飾的 64 位數據(long,double)的讀寫操做劃分爲兩次 32 位的操做來進行,即 load、store、read 和 write 操做能夠不具有原子性。

有一個錯誤認識就是,int 等原子性的變量在多線程環境中不會出現線程安全問題。前面的線程不安全示例代碼中,cnt 變量屬於 int 類型變量,1000 個線程對它進行自增操做以後,獲得的值爲 997 而不是 1000。

爲了方便討論,將內存間的交互操做簡化爲 3 個:load、assign、store。

下圖演示了兩個線程同時對 cnt 變量進行操做,load、assign、store 這一系列操做總體上看不具有原子性,那麼在 T1 修改 cnt 而且尚未將修改後的值寫入主內存,T2 依然能夠讀入該變量的值。能夠看出,這兩個線程雖然執行了兩次自增運算,可是主內存中 cnt 的值最後爲 1 而不是 2。所以對 int 類型讀寫操做知足原子性只是說明 load、assign、store 這些單個操做具有原子性。


AtomicInteger 能保證多個線程修改的原子性。


使用 AtomicInteger 重寫以前線程不安全的代碼以後獲得如下線程安全實現:

public class AtomicExample {
    private AtomicInteger cnt = new AtomicInteger();

    public void add() {
        cnt.incrementAndGet();
    }

    public int get() {
        return cnt.get();
    }
}
public static void main(String[] args) throws InterruptedException {
    final int threadSize = 1000;
    AtomicExample example = new AtomicExample(); // 只修改這條語句
    final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < threadSize; i++) {
        executorService.execute(() -> {
            example.add();
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println(example.get());
}
1000

除了使用原子類以外,也能夠使用 synchronized 互斥鎖來保證操做的原子性。它對應的內存間交互操做爲:lock 和 unlock,在虛擬機實現上對應的字節碼指令爲 monitorenter 和 monitorexit。

public class AtomicSynchronizedExample {
    private int cnt = 0;

    public synchronized void add() {
        cnt++;
    }

    public synchronized int get() {
        return cnt;
    }
}
public static void main(String[] args) throws InterruptedException {
    final int threadSize = 1000;
    AtomicSynchronizedExample example = new AtomicSynchronizedExample();
    final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < threadSize; i++) {
        executorService.execute(() -> {
            example.add();
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println(example.get());
}
1000

2. 可見性

可見性指當一個線程修改了共享變量的值,其它線程可以當即得知這個修改。Java 內存模型是經過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值來實現可見性的。

主要有有三種實現可見性的方式:

  • volatile
  • synchronized,對一個變量執行 unlock 操做以前,必須把變量值同步回主內存。
  • final,被 final 關鍵字修飾的字段在構造器中一旦初始化完成,而且沒有發生 this 逃逸(其它線程經過 this 引用訪問到初始化了一半的對象),那麼其它線程就能看見 final 字段的值。

對前面的線程不安全示例中的 cnt 變量使用 volatile 修飾,不能解決線程不安全問題,由於 volatile 並不能保證操做的原子性。

3. 有序性

有序性是指:在本線程內觀察,全部操做都是有序的。在一個線程觀察另外一個線程,全部操做都是無序的,無序是由於發生了指令重排序。

在 Java 內存模型中,容許編譯器和處理器對指令進行重排序,重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。

volatile 關鍵字經過添加內存屏障的方式來禁止指令重排,即重排序時不能把後面的指令放到內存屏障以前。

也能夠經過 synchronized 來保證有序性,它保證每一個時刻只有一個線程執行同步代碼,至關因而讓線程順序執行同步代碼。

指令重排序

在執行程序時爲了提升性能,編譯器和處理器經常會對指令作重排序。

指令重排序包括:編譯器重排序處理器重排序

重排序分三種類型:

  1. 編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,能夠從新安排語句的執行順序。
  2. 指令級並行的重排序。現代處理器採用了指令級並行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執行。若是不存在數據依賴性,處理器能夠改變語句對應機器指令的執行順序。
  3. 內存系統的重排序。因爲處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操做看上去多是在亂序執行。

從 Java 源代碼到最終實際執行的指令序列,會分別經歷下面三種重排序:

上述的 1 屬於編譯器重排序,2 和 3 屬於處理器重排序。這些重排序均可能會致使多線程程序出現內存可見性問題。對於編譯器,JMM 的編譯器重排序規則會禁止特定類型的編譯器重排序(不是全部的編譯器重排序都要禁止)。對於處理器重排序,JMM 的處理器重排序規則會要求 Java 編譯器在生成指令序列時,插入特定類型的內存屏障(memory barriers,intel 稱之爲 memory fence)指令,經過內存屏障指令來禁止特定類型的處理器重排序(不是全部的處理器重排序都要禁止)。

JMM 屬於語言級的內存模型,它確保在不一樣的編譯器和不一樣的處理器平臺之上,經過禁止特定類型的編譯器重排序和處理器重排序,爲程序員提供一致的內存可見性保證。

數據依賴性

若是兩個操做訪問同一個變量,且這兩個操做中有一個爲寫操做,此時這兩個操做之間就存在數據依賴性。數據依賴分下列三種類型:

名稱 代碼示例 說明
寫後讀 a = 1;b = a; 寫一個變量以後,再讀這個位置。
寫後寫 a = 1;a = 2; 寫一個變量以後,再寫這個變量。
讀後寫 a = b;b = 1; 讀一個變量以後,再寫這個變量。

上面三種狀況,只要重排序兩個操做的執行順序,程序的執行結果將會被改變。

前面提到過,編譯器和處理器可能會對操做作重排序。編譯器和處理器在重排序時,會遵照數據依賴性,編譯器和處理器不會改變存在數據依賴關係的兩個操做的執行順序。

注意,這裏所說的數據依賴性僅針對單個處理器中執行的指令序列和單個線程中執行的操做,不一樣處理器之間和不一樣線程之間的數據依賴性不被編譯器和處理器考慮。

as-if-serial語義

as-if-serial 語義的意思指:無論怎麼重排序(編譯器和處理器爲了提升並行度),(單線程)程序的執行結果不能被改變。編譯器,runtime 和 處理器 都必須遵照 as-if-serial 語義。

爲了遵照 as-if-serial 語義,編譯器和處理器不會對存在數據依賴關係的操做作重排序,由於這種重排序會改變執行結果。可是,若是操做之間不存在數據依賴關係,這些操做可能被編譯器和處理器重排序。爲了具體說明,請看下面計算圓面積的代碼示例:

double pi  = 3.14;    //A
double r   = 1.0;     //B
double area = pi * r * r; //C

上面三個操做的數據依賴關係以下圖所示:

如上圖所示,A 和 C 之間存在數據依賴關係,同時 B 和 C 之間也存在數據依賴關係。所以在最終執行的指令序列中,C 不能被重排序到 A 和 B 的前面(C 排到 A 和 B 的前面,程序的結果將會被改變)。但 A 和 B 之間沒有數據依賴關係,編譯器和處理器能夠重排序 A 和 B 之間的執行順序。下圖是該程序的兩種執行順序:

as-if-serial 語義把單線程程序保護了起來,遵照 as-if-serial 語義的編譯器,runtime 和處理器共同爲編寫單線程程序的程序員建立了一個幻覺:單線程程序是按程序的順序來執行的。as-if-serial 語義使單線程程序員無需擔憂重排序會干擾他們,也無需擔憂內存可見性問題。

程序順序規則

根據 happens- before 的程序順序規則,上面計算圓的面積的示例代碼存在三個 happens- before 關係:

  1. A happens- before B;
  2. B happens- before C;
  3. A happens- before C;

這裏的第 3 個 happens- before 關係,是根據 happens- before 的傳遞性推導出來的。

這裏 A happens- before B,但實際執行時 B 卻能夠排在 A 以前執行(看上面的重排序後的執行順序)。若是A happens- before B,JMM 並不要求 A 必定要在 B 以前執行。JMM 僅僅要求前一個操做(執行的結果)對後一個操做可見,且前一個操做按順序排在第二個操做以前。這裏操做 A 的執行結果不須要對操做 B 可見;並且重排序操做 A 和操做 B 後的執行結果,與操做 A 和操做 B 按 happens- before 順序執行的結果一致。在這種狀況下, JMM 會認爲這種重排序並不非法(not illegal),JMM 容許這種重排序。

在計算機中,軟件技術和硬件技術有一個共同的目標:在不改變程序執行結果的前提下,儘量的開發並行度。編譯器和處理器聽從這一目標,從 happens- before 的定義咱們能夠看出,JMM 一樣聽從這一目標。

重排序對多線程的影響

如今讓咱們來看看,重排序是否會改變多線程程序的執行結果。請看下面的示例代碼:

class ReorderExample {
    int a = 0;
    boolean flag = false;

    public void writer() {
        a = 1;                   // 1
        flag = true;             // 2
    }

    Public void reader() {
        if (flag) {                // 3
            int i =  a * a;        // 4
            ……
        }
    }
}

flag 變量是個標記,用來標識變量 a 是否已被寫入。這裏假設有兩個線程 A 和 B,A首先執行 writer() 方法,隨後 B 線程接着執行 reader() 方法。線程 B 在執行操做 4 時,可否看到線程 A 在操做 1 對共享變量 a 的寫入?

答案是:不必定能看到。

因爲操做 1 和操做 2 沒有數據依賴關係,編譯器和處理器能夠對這兩個操做重排序;一樣,操做 3 和操做 4 沒有數據依賴關係,編譯器和處理器也能夠對這兩個操做重排序。讓咱們先來看看,當操做 1 和操做 2 重排序時,可能會產生什麼效果?請看下面的程序執行時序圖:

如上圖所示,操做 1 和操做 2 作了重排序。程序執行時,線程 A 首先寫標記變量 flag,隨後線程 B 讀這個變量。因爲條件判斷爲真,線程 B 將讀取變量 a。此時,變量 a 還根本沒有被線程 A 寫入,在這裏多線程程序的語義被重排序破壞了!

※注:本文統一用紅色的虛箭線表示錯誤的讀操做,用綠色的虛箭線表示正確的讀操做。

下面再讓咱們看看,當操做 3 和操做 4 重排序時會產生什麼效果(藉助這個重排序,能夠順便說明控制依賴性)。下面是操做 3 和操做 4 重排序後,程序的執行時序圖:

在程序中,操做 3 和操做 4 存在控制依賴關係。當代碼中存在控制依賴性時,會影響指令序列執行的並行度。爲此,編譯器和處理器會採用猜想(Speculation)執行來克服控制相關性對並行度的影響。以處理器的猜想執行爲例,執行線程 B 的處理器能夠提早讀取並計算 a*a,而後把計算結果臨時保存到一個名爲重排序緩衝(reorder buffer ROB)的硬件緩存中。當接下來操做3的條件判斷爲真時,就把該計算結果寫入變量 i 中。

從圖中咱們能夠看出,猜想執行實質上對操做 3 和 4 作了重排序。重排序在這裏破壞了多線程程序的語義!

在單線程程序中,對存在控制依賴的操做重排序,不會改變執行結果(這也是 as-if-serial 語義容許對存在控制依賴的操做作重排序的緣由);但在多線程程序中,對存在控制依賴的操做重排序,可能會改變程序的執行結果。

參考資料:

先行發生原則(happens-before)

Happens-before 是用來指定兩個操做之間的執行順序。提供跨線程的內存可見性。

在 Java 內存模型中,若是一個操做執行的結果須要對另外一個操做可見,那麼這兩個操做之間必然存在 happens-before 關係。

上面提到了能夠用 volatile 和 synchronized 來保證有序性。除此以外,JVM 還規定了先行發生原則,讓一個操做無需控制就能先於另外一個操做完成。

主要有如下這些原則:

1. 單一線程原則

Single Thread rule

在一個線程內,在程序前面的操做先行發生於後面的操做。

2. 管程鎖定規則

Monitor Lock Rule

對一個鎖的解鎖(unlock ),老是 happens-before 於隨後對這個鎖的加鎖(lock)

3. volatile 變量規則

Volatile Variable Rule

對一個 volatile 變量的寫操做先行發生於後面對這個變量的讀操做。

4. 線程啓動規則

Thread Start Rule

Thread 對象的 start() 方法調用先行發生於此線程的每個動做。

5. 線程加入規則

Thread Join Rule

Thread 對象的結束先行發生於 join() 方法返回。

6. 線程中斷規則

Thread Interruption Rule

對線程 interrupt() 方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,能夠經過 interrupted() 方法檢測到是否有中斷髮生。

7. 對象終結規則

Finalizer Rule

一個對象的初始化完成(構造函數執行結束)先行發生於它的 finalize() 方法的開始。

8. 傳遞性

Transitivity

若是操做 A 先行發生於操做 B,操做 B 先行發生於操做 C,那麼操做 A 先行發生於操做 C。

11. 線程安全

線程安全定義

一個類在能夠被多個線程安全調用時就是線程安全的。

線程安全分類

線程安全不是一個非真即假的命題,能夠將共享數據按照安全程度的強弱順序分紅如下五類:不可變、絕對線程安全、相對線程安全、線程兼容和線程對立。

1. 不可變

不可變(Immutable)的對象必定是線程安全的,不管是對象的方法實現仍是方法的調用者,都不須要再採起任何的線程安全保障措施,只要一個不可變的對象被正確地構建出來,那其外部的可見狀態永遠也不會改變,永遠也不會看到它在多個線程之中處於不一致的狀態。

不可變的類型:

  • final 關鍵字修飾的基本數據類型;
  • String
  • 枚舉類型
  • Number 部分子類,如 Long 和 Double 等數值包裝類型,BigInteger 和 BigDecimal 等大數據類型。但同爲 Number 的子類型的原子類 AtomicInteger 和 AtomicLong 則並不是不可變的。

對於集合類型,能夠使用 Collections.unmodifiableXXX() 方法來獲取一個不可變的集合。

public class ImmutableExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(map);
        unmodifiableMap.put("a", 1);
    }
}
Exception in thread "main" java.lang.UnsupportedOperationException
    at java.util.Collections$UnmodifiableMap.put(Collections.java:1457)
    at ImmutableExample.main(ImmutableExample.java:9)

Collections.unmodifiableXXX() 先對原始的集合進行拷貝,須要對集合進行修改的方法都直接拋出異常。

public V put(K key, V value) {
    throw new UnsupportedOperationException();
}

多線程環境下,應當儘可能使對象成爲不可變,來知足線程安全。

2. 絕對線程安全

無論運行時環境如何,調用者都不須要任何額外的同步措施。

3. 相對線程安全

相對的線程安全須要保證對這個對象單獨的操做是線程安全的,在調用的時候不須要作額外的保障措施,可是對於一些特定順序的連續調用,就可能須要在調用端使用額外的同步手段來保證調用的正確性。

在 Java 語言中,大部分的線程安全類都屬於這種類型,例如 Vector、HashTable、Collections 的 synchronizedCollection() 方法包裝的集合等。

對於下面的代碼,若是刪除元素的線程刪除了一個元素,而獲取元素的線程試圖訪問一個已經被刪除的元素,那麼就會拋出 ArrayIndexOutOfBoundsException。

public class VectorUnsafeExample {
    private static Vector<Integer> vector = new Vector<>();

    public static void main(String[] args) {
        while (true) {
            for (int i = 0; i < 100; i++) {
                vector.add(i);
            }
            ExecutorService executorService = Executors.newCachedThreadPool();
            executorService.execute(() -> {
                for (int i = 0; i < vector.size(); i++) {
                    vector.remove(i);
                }
            });
            executorService.execute(() -> {
                for (int i = 0; i < vector.size(); i++) {
                    vector.get(i);
                }
            });
            executorService.shutdown();
        }
    }
}
Exception in thread "Thread-159738" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 3
    at java.util.Vector.remove(Vector.java:831)
    at VectorUnsafeExample.lambda$main$0(VectorUnsafeExample.java:14)
    at VectorUnsafeExample$$Lambda$1/713338599.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:745)

若是要保證上面的代碼能正確執行下去,就須要對刪除元素和獲取元素的代碼進行同步。

executorService.execute(() -> {
    synchronized (vector) {
        for (int i = 0; i < vector.size(); i++) {
            vector.remove(i);
        }
    }
});
executorService.execute(() -> {
    synchronized (vector) {
        for (int i = 0; i < vector.size(); i++) {
            vector.get(i);
        }
    }
});

4. 線程兼容

線程兼容是指對象自己並非線程安全的,可是能夠經過在調用端正確地使用同步手段來保證對象在併發環境中能夠安全地使用,咱們日常說一個類不是線程安全的,絕大多數時候指的是這一種狀況。Java API 中大部分的類都是屬於線程兼容的,如與前面的 Vector 和 HashTable 相對應的集合類 ArrayList 和 HashMap 等。

5. 線程對立

線程對立是指不管調用端是否採起了同步措施,都沒法在多線程環境中併發使用的代碼。因爲 Java 語言天生就具有多線程特性,線程對立這種排斥多線程的代碼是不多出現的,並且一般都是有害的,應當儘可能避免。

線程安全的實現方法

1. 阻塞同步(互斥同步)

synchronized 和 ReentrantLock。

互斥同步最主要的問題就是進行線程阻塞和喚醒所帶來的性能問題,所以這種同步也稱爲阻塞同步。

互斥同步屬於一種悲觀的併發策略,老是認爲只要不去作正確的同步措施,那就確定會出現問題。不管共享數據是否真的會出現競爭,它都要進行加鎖(這裏討論的是概念模型,實際上虛擬機會優化掉很大一部分沒必要要的加鎖)、用戶態核心態轉換、維護鎖計數器和檢查是否有被阻塞的線程須要喚醒等操做。

2. 非阻塞同步

隨着硬件指令集的發展,咱們能夠使用基於衝突檢測的樂觀併發策略:先進行操做,若是沒有其它線程爭用共享數據,那操做就成功了,不然採起補償措施(不斷地重試,直到成功爲止)。這種樂觀的併發策略的許多實現都不須要把線程掛起,所以這種同步操做稱爲非阻塞同步。

樂觀鎖須要操做和衝突檢測這兩個步驟具有原子性,這裏就不能再使用互斥同步來保證了,只能靠硬件來完成。

硬件支持的原子性操做最典型的是:比較並交換(Compare-and-Swap,CAS)。CAS 指令須要有 3 個操做數,分別是內存地址 V、舊的預期值 A 和新值 B。當執行操做時,只有當 V 的值等於 A,纔將 V 的值更新爲 B。

J.U.C 包裏面的整數原子類 AtomicInteger,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 類的 CAS 操做。

如下代碼使用了 AtomicInteger 執行了自增的操做。

private AtomicInteger cnt = new AtomicInteger();

public void add() {
    cnt.incrementAndGet();
}

如下代碼是 incrementAndGet() 的源碼,它調用了 unsafe 的 getAndAddInt() 。

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

如下代碼是 getAndAddInt() 源碼,var1 指示對象內存地址,var2 指示該字段相對對象內存地址的偏移,var4 指示操做須要加的數值,這裏爲 1。經過 getIntVolatile(var1, var2) 獲得舊的預期值,經過調用 compareAndSwapInt() 來進行 CAS 比較,若是該字段內存地址中的值 ==var5,那麼就更新內存地址爲 var1+var2 的變量爲 var5+var4。

能夠看到 getAndAddInt() 在一個循環中進行,發生衝突的作法是不斷的進行重試。

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

ABA :若是一個變量初次讀取的時候是 A 值,它的值被改爲了 B,後來又被改回爲 A,那 CAS 操做就會誤認爲它歷來沒有被改變過。

J.U.C 包提供了一個帶有標記的原子引用類 AtomicStampedReference 來解決這個問題,它能夠經過控制變量值的版原本保證 CAS 的正確性。大部分狀況下 ABA 問題不會影響程序併發的正確性,若是須要解決 ABA 問題,改用傳統的互斥同步可能會比原子類更高效。

3. 無同步方案

要保證線程安全,並非必定就要進行同步,二者沒有因果關係。同步只是保證共享數據爭用時的正確性的手段,若是一個方法原本就不涉及共享數據,那它天然就無須任何同步措施去保證正確性,所以會有一些代碼天生就是線程安全的。

(一)可重入代碼(Reentrant Code)

這種代碼也叫作純代碼(Pure Code),能夠在代碼執行的任什麼時候刻中斷它,轉而去執行另一段代碼(包括遞歸調用它自己),而在控制權返回後,原來的程序不會出現任何錯誤。

可重入代碼有一些共同的特徵,例如不依賴存儲在堆上的數據和公用的系統資源、用到的狀態量都由參數中傳入、不調用非可重入的方法等。

(二)棧封閉

多個線程訪問同一個方法的局部變量時,不會出現線程安全問題,由於局部變量存儲在棧中,屬於線程私有的。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class StackClosedExample {
    public void add100() {
        int cnt = 0;
        for (int i = 0; i < 100; i++) {
            cnt++;
        }
        System.out.println(cnt);
    }
}
public static void main(String[] args) {
    StackClosedExample example = new StackClosedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> example.add100());
    executorService.execute(() -> example.add100());
    executorService.shutdown();
}
100
100
(三)線程本地存儲(Thread Local Storage)

若是一段代碼中所須要的數據必須與其餘代碼共享,那就看看這些共享數據的代碼是否能保證在同一個線程中執行。若是能保證,咱們就能夠把共享數據的可見範圍限制在同一個線程以內,這樣,無須同步也能保證線程之間不出現數據爭用的問題。

符合這種特色的應用並很多見,大部分使用消費隊列的架構模式(如「生產者-消費者」模式)都會將產品的消費過程儘可能在一個線程中消費完,其中最重要的一個應用實例就是經典 Web 交互模型中的 「一個請求對應一個服務器線程」(Thread-per-Request)的處理方式,這種處理方式的普遍應用使得不少 Web 服務端應用均可以使用線程本地存儲來解決線程安全問題。

能夠使用 java.lang.ThreadLocal 類來實現線程本地存儲功能。

這是一個很是好的例題,請參考整理:

關於ThreadLocal類如下說法正確的是?_迅雷筆試題_牛客網

示例用法

先經過下面這個實例來理解 ThreadLocal 的用法。先聲明一個 ThreadLocal 對象,存儲布爾類型的數值。而後分別在main線程、Thread一、Thread2中爲 ThreadLocal 對象設置不一樣的數值:

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

        // 聲明 ThreadLocal對象
        ThreadLocal<Boolean> mThreadLocal = new ThreadLocal<Boolean>();

        // 在主線程、子線程一、子線程2中去設置訪問它的值
        mThreadLocal.set(true);

        System.out.println("Main " + mThreadLocal.get());

        new Thread("Thread#1"){
            @Override
            public void run() {
                mThreadLocal.set(false);
                System.out.println("Thread#1 " + mThreadLocal.get());
            }
        }.start();

        new Thread("Thread#2"){
            @Override
            public void run() {
                System.out.println("Thread#2 " + mThreadLocal.get());
            }
        }.start();
    }
}

打印的結果輸出以下所示:

MainThread true
Thread#1 false
Thread#2 null

能夠看見,在不一樣線程對同一個 ThreadLocal對象設置數值,在不一樣的線程中取出來的值不同。接下來就分析一下源碼,看看其內部結構。

結構概覽


清晰的看到一個線程 Thread 中存在一個 ThreadLocalMap,ThreadLocalMap 中的 key 對應 ThreadLocal,在此處可見 Map 能夠存儲多個 key 即 (ThreadLocal)。另外 Value 就對應着在 ThreadLocal 中存儲的 Value。

所以總結出:每一個 Thread 中都具有一個 ThreadLocalMap,而 ThreadLocalMap 能夠存儲以 ThreadLocal 爲key的鍵值對。這裏解釋了爲何每一個線程訪問同一個 ThreadLocal,獲得的確是不一樣的數值。若是此處你以爲有點突兀,接下來看源碼分析!

源碼分析

1. ThreadLocal#set

public void set(T value) {
    // 獲取當前線程對象
    Thread t = Thread.currentThread();
    // 根據當前線程的對象獲取其內部Map
    ThreadLocalMap map = getMap(t);
    
    // 註釋1
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

如上所示,大部分解釋已經在代碼中作出,注意註釋1處,獲得 map 對象以後,用的 this 做爲 key,this 在這裏表明的是當前線程的 ThreadLocal 對象。 另外就是第二句根據 getMap 獲取一個 ThreadLocalMap,其中getMap 中傳入了參數 t (當前線程對象),這樣就可以獲取每一個線程的 ThreadLocal 了。

繼續跟進到 ThreadLocalMap 中查看 set 方法:

2. ThreadLocalMap

ThreadLocalMap 是 ThreadLocal 的一個內部類,在分析其 set 方法以前,查看一下其類結構和成員變量。

static class ThreadLocalMap {
     // Entry類繼承了WeakReference<ThreadLocal<?>>
     // 即每一個Entry對象都有一個ThreadLocal的弱引用(做爲key),這是爲了防止內存泄露。
     // 一旦線程結束,key變爲一個不可達的對象,這個Entry就能夠被GC了。
     static class Entry extends WeakReference<ThreadLocal<?>> {
         /** The value associated with this ThreadLocal. */
         Object value;
         Entry(ThreadLocal<?> k, Object v) {
             super(k);
             value = v;
         }
     }
     // ThreadLocalMap 的初始容量,必須爲2的倍數
     private static final int INITIAL_CAPACITY = 16;

     // resized時候須要的table
     private Entry[] table;

     // table中的entry個數
     private int size = 0;

     // 擴容數值
     private int threshold; // Default to 0
 }

一塊兒看一下其經常使用的構造函數:

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

構造函數的第一個參數就是本 ThreadLocal 實例 (this),第二個參數就是要保存的線程本地變量。構造函數首先建立一個長度爲16的 Entry 數組,而後計算出 firstKey 對應的哈希值,而後存儲到 table 中,並設置 size 和 threshold。

注意一個細節,計算 hash 的時候裏面採用了 hashCode & (size - 1) 的算法,這至關於取模運算 hashCode % size 的一個更高效的實現(和HashMap中的思路相同)。正是由於這種算法,咱們要求 size必須是 2 的指數,由於這能夠使得 hash 發生衝突的次數減少。

3. ThreadLocalMap#set

ThreadLocal 中 put 函數最終調用了 ThreadLocalMap 中的 set 函數,跟進去看一看:

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         // 衝突了
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

在上述代碼中若是 Entry 在存放過程當中衝突了,調用 nextIndex 來處理,以下所示。是否還記得 hashmap 中對待衝突的處理?這裏好像是另外一種套路:只要 i 的數值小於 len,就加1取值,官方術語稱爲:線性探測法。

private static int nextIndex(int i, int len) {
     return ((i + 1 < len) ? i + 1 : 0);
 }

以上步驟ok了以後,再次關注一下源碼中的 cleanSomeSlots,該函數主要的做用就是清理無用的 entry,避免出現內存泄露:

private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}

4. ThreadLocal#get

看完了 set 函數,確定是要關注 get 的,源碼以下所示:

public T get() {
    // 獲取Thread對象t
    Thread t = Thread.currentThread();
    // 獲取t中的map
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 若是t中的map爲空
    return setInitialValue();
}

若是 map 爲 null,就返回 setInitialValue() 這個方法,跟進這個方法看一下:

private T setInitialValue() {
     T value = initialValue();
     Thread t = Thread.currentThread();
     ThreadLocalMap map = getMap(t);
     if (map != null)
         map.set(this, value);
     else
         createMap(t, value);
     return value;
 }

最後返回的是 value,而 value 來自 initialValue(),進入這個源碼中查看:

protected T initialValue() {
    return null;
}

原來如此,若是不設置 ThreadLocal 的數值,默認就是 null,來自於此。

ThreadLocal 從理論上講並非用來解決多線程併發問題的,由於根本不存在多線程競爭。在一些場景 (尤爲是使用線程池) 下,因爲 ThreadLocal.ThreadLocalMap 的底層數據結構致使 ThreadLocal 有內存泄漏的狀況,儘量在每次使用 ThreadLocal 後手動調用 remove(),以免出現 ThreadLocal 經典的內存泄漏甚至是形成自身業務混亂的風險。

參考資料:

12. 鎖優化

這裏的鎖優化主要是指虛擬機對 synchronized 的優化。

自旋鎖

互斥同步的進入阻塞狀態的開銷都很大,應該儘可能避免。在許多應用中,共享數據的鎖定狀態只會持續很短的一段時間。自旋鎖的思想是讓一個線程在請求一個共享數據的鎖時執行忙循環(自旋)一段時間,若是在這段時間內能得到鎖,就能夠避免進入阻塞狀態。

自旋鎖雖然能避免進入阻塞狀態從而減小開銷,可是它須要進行忙循環操做佔用 CPU 時間,它只適用於共享數據的鎖定狀態很短的場景。

在 JDK 1.6 中引入了自適應的自旋鎖。自適應意味着自旋的次數再也不固定了,而是由前一次在同一個鎖上的自旋次數及鎖的擁有者的狀態來決定。

鎖消除

鎖消除是指對於被檢測出不可能存在競爭的共享數據的鎖進行消除。

鎖消除主要是經過逃逸分析來支持,若是堆上的共享數據不可能逃逸出去被其它線程訪問到,那麼就能夠把它們當成私有數據對待,也就能夠將它們的鎖進行消除。

對於一些看起來沒有加鎖的代碼,其實隱式的加了不少鎖。例以下面的字符串拼接代碼就隱式加了鎖:

public static String concatString(String s1, String s2, String s3) {
    return s1 + s2 + s3;
}

String 是一個不可變的類,編譯器會對 String 的拼接自動優化。在 JDK 1.5 以前,會轉化爲 StringBuffer 對象的連續 append() 操做:

public static String concatString(String s1, String s2, String s3) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    sb.append(s3);
    return sb.toString();
}

每一個 append() 方法中都有一個同步塊。虛擬機觀察變量 sb,很快就會發現它的動態做用域被限制在 concatString() 方法內部。也就是說,sb 的全部引用永遠不會「逃逸」到 concatString() 方法以外,其餘線程沒法訪問到它,所以能夠進行消除。

鎖粗化

若是一系列的連續操做都對同一個對象反覆加鎖和解鎖,頻繁的加鎖操做就會致使性能損耗。

上一節的示例代碼中連續的 append() 方法就屬於這類狀況。若是虛擬機探測到由這樣的一串零碎的操做都對同一個對象加鎖,將會把加鎖的範圍擴展(粗化)到整個操做序列的外部。對於上一節的示例代碼就是擴展到第一個 append() 操做以前直至最後一個 append() 操做以後,這樣只須要加鎖一次就能夠了。

輕量級鎖

JDK 1.6 引入了偏向鎖和輕量級鎖,從而讓鎖擁有了四個狀態:無鎖狀態(unlocked)、偏向鎖狀態(biasble)、輕量級鎖狀態(lightweight locked)和重量級鎖狀態(inflated)。

如下是 HotSpot 虛擬機對象頭的內存佈局,這些數據被稱爲 mark word。其中 tag bits 對應了五個狀態,這些狀態在右側的 state 表格中給出,應該注意的是 state 表格不是存儲在對象頭中的。除了 marked for gc 狀態,其它四個狀態已經在前面介紹過了。


下圖左側是一個線程的虛擬機棧,其中有一部分稱爲 Lock Record 的區域,這是在輕量級鎖運行過程建立的,用於存放鎖對象的 Mark Word。而右側就是一個鎖對象,包含了 Mark Word 和其它信息。

輕量級鎖是相對於傳統的重量級鎖而言,它使用 CAS 操做來避免重量級鎖使用互斥量的開銷。對於絕大部分的鎖,在整個同步週期內都是不存在競爭的,所以也就不須要都使用互斥量進行同步,能夠先採用 CAS 操做進行同步,若是 CAS 失敗了再改用互斥量進行同步。

當嘗試獲取一個鎖對象時,若是鎖對象標記爲 0 01,說明鎖對象的鎖未鎖定(unlocked)狀態。此時虛擬機在當前線程棧中建立 Lock Record,而後使用 CAS 操做將對象的 Mark Word 更新爲 Lock Record 指針。若是 CAS 操做成功了,那麼線程就獲取了該對象上的鎖,而且對象的 Mark Word 的鎖標記變爲 00,表示該對象處於輕量級鎖狀態。

若是 CAS 操做失敗了,虛擬機首先會檢查對象的 Mark Word 是否指向當前線程的虛擬機棧,若是是的話說明當前線程已經擁有了這個鎖對象,那就能夠直接進入同步塊繼續執行,不然說明這個鎖對象已經被其餘線程線程搶佔了。若是有兩條以上的線程爭用同一個鎖,那輕量級鎖就再也不有效,要膨脹爲重量級鎖。

偏向鎖

偏向鎖的思想是偏向於讓第一個獲取鎖對象的線程,這個線程在以後獲取該鎖就再也不須要進行同步操做,甚至連 CAS 操做也再也不須要。

當鎖對象第一次被線程得到的時候,進入偏向狀態,標記爲 1 01。同時使用 CAS 操做將線程 ID 記錄到 Mark Word 中,若是 CAS 操做成功,這個線程之後每次進入這個鎖相關的同步塊就不須要再進行任何同步操做。

當有另一個線程去嘗試獲取這個鎖對象時,偏向狀態就宣告結束,此時撤銷偏向(Revoke Bias)後恢復到未鎖定狀態或者輕量級鎖狀態。

13. 多線程開發良好的實踐

  • 給線程起個有意義的名字,這樣能夠方便找 Bug。
  • 縮小同步範圍,例如對於 synchronized,應該儘可能使用同步塊而不是同步方法。
  • 多用同步類少用 wait() 和 notify()。首先,CountDownLatch, CyclicBarrier, Semaphore 和 Exchanger 這些同步類簡化了編碼操做,而用 wait() 和 notify() 很難實現對複雜的控制流;其次,這些同步類是由最好的企業編寫和維護,在後續的 JDK 中還會不斷優化和完善,使用這些更高等級的同步工具你的程序能夠不費吹灰之力得到優化。
  • 多用併發集合少用同步集合,例如應該使用 ConcurrentHashMap 而不是 Hashtable。
  • 使用本地變量和不可變類來保證線程安全。
  • 使用線程池而不是直接建立 Thread 對象,這是由於建立線程代價很高,線程池能夠有效地利用有限的線程來啓動任務。
  • 使用 BlockingQueue 實現生產者消費者問題。

14. 線程池實現原理

蘑菇街面試,設計一個線程池

ThrealpoolExecutor_framework

併發隊列

入隊

非阻塞隊列:當隊列中滿了時候,放入數據,數據丟失

阻塞隊列:當隊列滿了的時候,進行等待,何時隊列中有出隊的數據,那麼第11個再放進去

出隊

非阻塞隊列:若是如今隊列中沒有元素,取元素,獲得的是null

阻塞隊列:等待,何時放進去,再取出來

線程池使用的是阻塞隊列

線程池概念

線程是稀缺資源,若是被無限制的建立,不只會消耗系統資源,還會下降系統的穩定性,合理的使用線程池對線程進行統一分配、調優和監控,有如下好處:

  1. 下降資源消耗;
  2. 提升響應速度;
  3. 提升線程的可管理性。

Java1.5 中引入的 Executor 框架把任務的提交和執行進行解耦,只須要定義好任務,而後提交給線程池,而不用關心該任務是如何執行、被哪一個線程執行,以及何時執行。

Executor類圖

線程池工做原理

線程池中的核心線程數,當提交一個任務時,線程池建立一個新線程執行任務,直到當前線程數等於corePoolSize;若是當前線程數爲 corePoolSize,繼續提交的任務被保存到阻塞隊列中,等待被執行;若是阻塞隊列滿了,那就建立新的線程執行當前任務;直到線程池中的線程數達到 maxPoolSize,這時再有任務來,只能執行 reject() 處理該任務。

初始化線程池

  • newFixedThreadPool()
    說明:初始化一個指定線程數的線程池,其中 corePoolSize == maxiPoolSize,使用 LinkedBlockingQuene 做爲阻塞隊列
    特色:即便當線程池沒有可執行任務時,也不會釋放線程。
  • newCachedThreadPool()
    說明:初始化一個能夠緩存線程的線程池,默認緩存60s,線程池的線程數可達到 Integer.MAX_VALUE,即 2147483647,內部使用 SynchronousQueue 做爲阻塞隊列;
    特色:在沒有任務執行時,當線程的空閒時間超過 keepAliveTime,會自動釋放線程資源;當提交新任務時,若是沒有空閒線程,則建立新線程執行任務,會致使必定的系統開銷;
    所以,使用時要注意控制併發的任務數,防止因建立大量的線程致使而下降性能。
  • newSingleThreadExecutor()
    說明:初始化只有一個線程的線程池,內部使用 LinkedBlockingQueue 做爲阻塞隊列。
    特色:若是該線程異常結束,會從新建立一個新的線程繼續執行任務,惟一的線程能夠保證所提交任務的順序執行
  • newScheduledThreadPool()
    特色:初始化的線程池能夠在指定的時間內週期性的執行所提交的任務,在實際的業務場景中能夠使用該線程池按期的同步數據。

初始化方法

// 使用Executors靜態方法進行初始化
ExecutorService service = Executors.newSingleThreadExecutor();
// 經常使用方法
service.execute(new Thread());
service.submit(new Thread());
service.shutDown();
service.shutDownNow();

經常使用方法

execute與submit的區別

  1. 接收的參數不同
  2. submit有返回值,而execute沒有

用到返回值的例子,好比說我有不少個作 validation 的 task,我但願全部的 task 執行完,而後每一個 task 告訴我它的執行結果,是成功仍是失敗,若是是失敗,緣由是什麼。而後我就能夠把全部失敗的緣由綜合起來發給調用者。

  1. submit方便Exception處理

若是你在你的 task 裏會拋出 checked 或者 unchecked exception,而你又但願外面的調用者可以感知這些 exception 並作出及時的處理,那麼就須要用到 submit,經過捕獲 Future.get 拋出的異常。

shutDown與shutDownNow的區別

當線程池調用該方法時,線程池的狀態則馬上變成 SHUTDOWN 狀態。此時,則不能再往線程池中添加任何任務,不然將會拋出 RejectedExecutionException 異常。可是,此時線程池不會馬上退出,直到添加到線程池中的任務都已經處理完成,纔會退出。

內部實現

public ThreadPoolExecutor(
    int corePoolSize,     // 核心線程數
    int maximumPoolSize,  // 最大線程數
    long keepAliveTime,   // 線程存活時間(在 corePore<*<maxPoolSize 狀況下有用)
    TimeUnit unit,        // 存活時間的時間單位
    BlockingQueue<Runnable> workQueue    // 阻塞隊列(用來保存等待被執行的任務)
    ThreadFactory threadFactory,    // 線程工廠,主要用來建立線程;
    RejectedExecutionHandler handler // 當拒絕處理任務時的策略
){
    
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

關於 workQueue 參數,有四種隊列可供選擇:

  • ArrayBlockingQueue:基於數組結構的有界阻塞隊列,按 FIFO 排序任務;
  • LinkedBlockingQuene:基於鏈表結構的阻塞隊列,按 FIFO 排序任務;
  • SynchronousQuene:一個不存儲元素的阻塞隊列,每一個插入操做必須等到另外一個線程調用移除操做,不然插入操做一直處於阻塞狀態,吞吐量一般要高於 ArrayBlockingQuene;
  • PriorityBlockingQuene:具備優先級的無界阻塞隊列;

關於 handler 參數,線程池的飽和策略,當阻塞隊列滿了,且沒有空閒的工做線程,若是繼續提交任務,必須採起一種策略處理該任務,線程池提供了 4 種策略:

  • ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。
  • ThreadPoolExecutor.DiscardPolicy:丟棄任務,可是不拋出異常。
  • ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,而後從新嘗試執行任務(重複此過程)
  • ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務

固然也能夠根據應用場景實現 RejectedExecutionHandler 接口,自定義飽和策略,如記錄日誌或持久化存儲不能處理的任務。

線程池的狀態

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

其中 AtomicInteger 變量 ctl 的功能很是強大:利用低 29 位表示線程池中線程數,經過高 3 位表示線程池的運行狀態:

  • RUNNING:-1 << COUNT_BITS,即高 3 位爲 111,該狀態的線程池會接收新任務,並處理阻塞隊列中的任務;
  • SHUTDOWN: 0 << COUNT_BITS,即高 3 位爲 000,該狀態的線程池不會接收新任務,但會處理阻塞隊列中的任務;
  • STOP : 1 << COUNT_BITS,即高 3 位爲 001,該狀態的線程不會接收新任務,也不會處理阻塞隊列中的任務,並且會中斷正在運行的任務;
  • TIDYING : 2 << COUNT_BITS,即高 3 位爲 010,該狀態表示線程池對線程進行整理優化;
  • TERMINATED: 3 << COUNT_BITS,即高 3 位爲 011,該狀態表示線程池中止工做;

線程池其餘經常使用方法

若是執行了線程池的 prestartAllCoreThreads() 方法,線程池會提早建立並啓動全部核心線程。
ThreadPoolExecutor 提供了動態調整線程池容量大小的方法:setCorePoolSize() 和 setMaximumPoolSize()。

如何合理設置線程池的大小

通常須要根據任務的類型來配置線程池大小:
若是是 CPU 密集型任務,就須要儘可能壓榨 CPU,參考值能夠設爲 NCPU+1
若是是 IO 密集型任務,參考值能夠設置爲 2*NCPU

第二部分:面試指南

在這裏將總結面試中和併發編程相關的常見知識點,如在第一部分中出現的這裏將不進行詳細闡述。面試指南中,我將用最簡潔的語言描述,更可能是以一種大綱的形式列出問答點,根據本身掌握的狀況回答。

參考資料:

1. volatile 與 synchronized 的區別

(1)僅靠volatile不能保證線程的安全性。(原子性)

  • ① volatile 輕量級,只能修飾變量。synchronized重量級,還可修飾方法
  • ② volatile 只能保證數據的可見性,不能用來同步,由於多個線程併發訪問 volatile 修飾的變量不會阻塞。

synchronized 不只保證可見性,並且還保證原子性,由於,只有得到了鎖的線程才能進入臨界區,從而保證臨界區中的全部語句都所有執行。多個線程爭搶 synchronized 鎖對象時,會出現阻塞。

(2)線程安全性

線程安全性包括兩個方面,①可見性。②原子性。

從上面自增的例子中能夠看出:僅僅使用 volatile 並不能保證線程安全性。而 synchronized 則可實現線程的安全性。

2. 什麼是線程池?若是讓你設計一個動態大小的線程池,如何設計,應該有哪些方法?線程池建立的方式?

  • 什麼是線程池

    • 線程池顧名思義就是事先建立若干個可執行的線程放入一個池(容器)中,須要的時候從池中獲取線程不用自行建立,使用完畢不須要銷燬線程而是放回池中,從而減小建立和銷燬線程對象的開銷。
  • 設計一個動態大小的線程池,如何設計,應該有哪些方法

    • 一個線程池包括如下四個基本組成部分:
      • 線程管理器 (ThreadPool):用於建立並管理線程池,包括建立線程,銷燬線程池,添加新任務;
      • 工做線程 (PoolWorker):線程池中線程,在沒有任務時處於等待狀態,能夠循環的執行任務;
      • 任務接口 (Task):每一個任務必須實現的接口,以供工做線程調度任務的執行,它主要規定了任務的入口,任務執行完後的收尾工做,任務的執行狀態等;
      • 任務隊列 (TaskQueue):用於存放沒有處理的任務。提供一種緩衝機制;
    • 所包含的方法
      • private ThreadPool() 建立線程池
      • public static ThreadPool getThreadPool() 得到一個默認線程個數的線程池
      • public void execute(Runnable task) 執行任務,其實只是把任務加入任務隊列,何時執行有線程池管理器決定
      • public void execute(Runnable[] task) 批量執行任務,其實只是把任務加入任務隊列,何時執行有線程池管理器決定
      • public void destroy() 銷燬線程池,該方法保證在全部任務都完成的狀況下才銷燬全部線程,不然等待任務完成才銷燬
      • public int getWorkThreadNumber() 返回工做線程的個數
      • public int getFinishedTasknumber() 返回已完成任務的個數,這裏的已完成是隻出了任務隊列的任務個數,可能該任務並無實際執行完成
      • public void addThread() 在保證線程池中全部線程正在執行,而且要執行線程的個數大於某一值時。增長線程池中線程的個數
      • public void reduceThread() 在保證線程池中有很大一部分線程處於空閒狀態,而且空閒狀態的線程在小於某一值時,減小線程池中線程的個數
  • 線程池四種建立方式

    Java 經過 Executors 提供四種線程池,分別爲:

    • new CachedThreadPool 建立一個可緩存線程池,若是線程池長度超過處理須要,可靈活回收空閒線程,若無可回收,則新建線程。
    • new FixedThreadPool 建立一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。
    • new ScheduledThreadPool 建立一個定長線程池,支持定時及週期性任務執行。
    • new SingleThreadExecutor 建立一個單線程化的線程池,它只會用惟一的工做線程來執行任務,保證全部任務按照指定順序(FIFO, LIFO, 優先級)執行。

3. 什麼是併發和並行


併發

  • 併發是指兩個任務都請求運行,而處理器只能按受一個任務,就把這兩個任務安排輪流進行,因爲時間間隔較短,令人感受兩個任務都在運行。
  • 若是用一臺電腦我先給甲發個消息,而後馬上再給乙發消息,而後再跟甲聊,再跟乙聊。這就叫併發。
  • 多個線程操做相同的資源,保證線程安全,合理使用資源

並行

  • 並行就是兩個任務同時運行,就是甲任務進行的同時,乙任務也在進行。(須要多核CPU)

  • 好比我跟兩個網友聊天,左手操做一個電腦跟甲聊,同時右手用另外一臺電腦跟乙聊天,這就叫並行。

  • 服務能同時處理不少請求,提升程序性能

參考資料:

4. 什麼是線程安全

當多個線程訪問同一個對象時,若是不用考慮這些線程在運行時環境下的調度和交替運行,也不須要進行額外的同步,或者在調用方進行任何其餘的協調操做,調用這個對象的行爲均可以獲取正確的結果,那這個對象是線程安全的。——來自《深刻理解Java虛擬機》

  • 定義

    • 某個類的行爲與其規範一致。
    • 無論多個線程是怎樣的執行順序和優先級,或是 wait , sleep , join 等控制方式,若是一個類在多線程訪問下運轉一切正常,而且訪問類不須要進行額外的同步處理或者協調,那麼咱們就認爲它是線程安全的。
  • 如何保證線程安全?(更加詳細的請轉向第一部分 11. 線程安全

    • 對變量使用 volitate
    • 對程序段進行加鎖 (synchronized , lock)
  • 注意

    • 非線程安全的集合在多線程環境下能夠使用,但並不能做爲多個線程共享的屬性,能夠做爲某個線程獨享的屬性。
    • 例如 Vector 是線程安全的,ArrayList 不是線程安全的。若是每個線程中 new 一個 ArrayList,而這個ArrayList 只是在這一個線程中使用,確定沒問題。

非線程安全!=不安全?

有人在使用過程當中有一個不正確的觀點:個人程序是多線程的,不能使用 ArrayList 要使用 Vector,這樣才安全。

非線程安全並非多線程環境下就不能使用。注意我上面有說到:多線程操做同一個對象。注意是同一個對象。好比最上面那個模擬,就是在主線程中 new 的一個 ArrayList 而後多個線程操做同一個 ArrayList 對象。

若是是每一個線程中 new 一個 ArrayList,而這個 ArrayList 只在這一個線程中使用,那麼確定是沒問題的。

線程安全十萬個爲何?

問:平時項目中使用鎖和 synchronized 比較多,而不多使用 volatile,難道就沒有保證可見性?
答:鎖和 synchronized 便可以保證原子性,也能夠保證可見性。都是經過保證同一時間只有一個線程執行目標代碼段來實現的。

問:鎖和 synchronized 爲什麼能保證可見性?
答:根據 JDK 7的Java doc 中對 concurrent 包的說明,一個線程的寫結果保證對另外線程的讀操做可見,只要該寫操做能夠由 happen-before 原則推斷出在讀操做以前發生。

The results of a write by one thread are guaranteed to be visible to a read by another thread only if the write operation happens-before the read operation. The synchronized and volatile constructs, as well as the Thread.start() and Thread.join() methods, can form happens-before relationships.

問:既然鎖和 synchronized 便可保證原子性也可保證可見性,爲什麼還須要 volatile?
答:synchronized和鎖須要經過操做系統來仲裁誰得到鎖,開銷比較高,而 volatile 開銷小不少。所以在只須要保證可見性的條件下,使用 volatile 的性能要比使用鎖和 synchronized 高得多。

問:既然鎖和 synchronized 能夠保證原子性,爲何還須要 AtomicInteger 這種的類來保證原子操做?
答:鎖和 synchronized 須要經過操做系統來仲裁誰得到鎖,開銷比較高,而 AtomicInteger 是經過CPU級的CAS操做來保證原子性,開銷比較小。因此使用 AtomicInteger 的目的仍是爲了提升性能。

問:還有沒有別的辦法保證線程安全
答:有。儘量避免引發非線程安全的條件——共享變量。若是能從設計上避免共享變量的使用,便可避免非線程安全的發生,也就無須經過鎖或者 synchronized 以及 volatile 解決原子性、可見性和順序性的問題。

問:synchronized 便可修飾非靜態方式,也可修飾靜態方法,還可修飾代碼塊,有何區別
答:synchronized 修飾非靜態同步方法時,鎖住的是當前實例;synchronized 修飾靜態同步方法時,鎖住的是該類的 Class 對象;synchronized 修飾靜態代碼塊時,鎖住的是 synchronized 關鍵字後面括號內的對象。

參考資料:

5. volatile 關鍵字的如何保證內存可見性

  • volatile 關鍵字的做用

    • 保證內存的可見性
    • 防止指令重排
    • 注意:volatile 並不保證原子性
  • 內存可見性

    • volatile 保證可見性的原理是在每次訪問變量時都會進行一次刷新,所以每次訪問都是主內存中最新的版本。因此 volatile 關鍵字的做用之一就是保證變量修改的實時可見性。
  • 當且僅當知足如下全部條件時,才應該使用 volatile 變量

    • 對變量的寫入操做不依賴變量的當前值,或者你能確保只有單個線程更新變量的值。
    • 該變量沒有包含在具備其餘變量的不變式中。
  • volatile 使用建議

    • 在兩個或者更多的線程須要訪問的成員變量上使用 volatile。當要訪問的變量已在 synchronized 代碼塊中,或者爲常量時,不必使用volatile。
    • 因爲使用 volatile 屏蔽掉了 JVM 中必要的代碼優化,因此在效率上比較低,所以必定在必要時才使用此關鍵字。
  • volatile 和 synchronized區別

    • volatile 不會進行加鎖操做:

      volatile 變量是一種稍弱的同步機制在訪問 volatile 變量時不會執行加鎖操做,所以也就不會使執行線程阻塞,所以 volatile 變量是一種比 synchronized 關鍵字更輕量級的同步機制。

    • volatile 變量做用相似於同步變量讀寫操做:

      從內存可見性的角度看,寫入 volatile 變量至關於退出同步代碼塊,而讀取 volatile 變量至關於進入同步代碼塊。

    • volatile 不如 synchronized安全:

      在代碼中若是過分依賴 volatile 變量來控制狀態的可見性,一般會比使用鎖的代碼更脆弱,也更難以理解。僅當 volatile 變量能簡化代碼的實現以及對同步策略的驗證時,才應該使用它。通常來講,用同步機制會更安全些。

    • volatile 沒法同時保證內存可見性和原則性:

      加鎖機制(即同步機制)既能夠確保可見性又能夠確保原子性,而 volatile 變量只能確保可見性,緣由是聲明爲volatile的簡單變量若是當前值與該變量之前的值相關,那麼 volatile 關鍵字不起做用,也就是說以下的表達式都不是原子操做:「count++」、「count = count+1」。

5. 什麼是線程?線程和進程有什麼區別?爲何要使用多線程

(1)線程和進程

  • 進程是操做系統分配資源的最小單位
  • 線程是CPU調度的最小單位

(2)使用線程的緣由

  • 使用多線程能夠減小程序的響應時間;
  • 與進程相比,線程的建立和切換開銷更小;
  • 多核電腦上,能夠同時執行多個線程,提升資源利用率;
  • 簡化程序的結構,使程序便於理解和維護;

6. 多線程共用一個數據變量須要注意什麼?

  • 當咱們在線程對象(Runnable)中定義了全局變量,run方法會修改該變量時,若是有多個線程同時使用該線程對象,那麼就會形成全局變量的值被同時修改,形成錯誤.
  • ThreadLocal 是JDK引入的一種機制,它用於解決線程間共享變量,使用 ThreadLocal 聲明的變量,即便在線程中屬於全局變量,針對每一個線程來說,這個變量也是獨立的。
  • volatile 變量每次被線程訪問時,都強迫線程從主內存中重讀該變量的最新值,而當該變量發生修改變化時,也會強迫線程將最新的值刷新回主內存中。這樣一來,不一樣的線程都能及時的看到該變量的最新值。

7. 內存泄漏與內存溢出

Java內存回收機制

  不論哪一種語言的內存分配方式,都須要返回所分配內存的真實地址,也就是返回一個指針到內存塊的首地址。Java中對象是採用 new、反射、clone、反序列化等方法建立的, 這些對象的建立都是在堆(Heap)中分配的,全部對象的回收都是由Java虛擬機經過垃圾回收機制完成的。GC 爲了可以正確釋放對象,會監控每一個對象的運行情況,對他們的申請、引用、被引用、賦值等情況進行監控,Java 會使用有向圖的方法進行管理內存,實時監控對象是否能夠達到,若是不可到達,則就將其回收,這樣也能夠消除引用循環的問題。

  在 Java 語言中,判斷一個內存空間是否符合垃圾收集標準有兩個:一個是給對象賦予了空值 null,如下再沒有調用過,另外一個是給對象賦予了新值,這樣從新分配了內存空間。

Java內存泄露引發緣由

  首先,什麼是內存泄露?常常聽人談起內存泄露,但要問什麼是內存泄露,沒幾個說得清楚。

  內存泄露:是指無用對象(再也不使用的對象)持續佔有內存或無用對象的內存得不到及時釋放,從而形成的內存空間的浪費稱爲內存泄露。內存泄露有時不嚴重且不易察覺,這樣開發者就不知道存在內存泄露,但有時也會很嚴重,會提示 Out of memory

  內存溢出:指程序運行過程當中沒法申請到足夠的內存而致使的一種錯誤。內存泄露是內存溢出的一種誘因,不是惟一因素
  那麼,Java 內存泄露根本緣由是什麼呢?長生命週期的對象持有短生命週期對象的引用就極可能發生內存泄露,儘管短生命週期對象已經再也不須要,可是由於長生命週期對象持有它的引用而致使不能被回收,這就是 Java 中內存泄露的發生場景。具體主要有以下幾大類

靜態集合類

  靜態集合類,使用Set、Vector、HashMap等集合類的時候須要特別注意。當這些類被定義成靜態的時候,因爲他們的生命週期跟應用程序同樣長,這時候就有可能發生內存泄漏。

// 例子 
class StaticTest 
{ 
    private static Vector v = new Vector(10); 
    public void init() 
    { 
        for (int i = 1; i < 100; i++) 
        { 
            Object object = new Object(); 
            v.add(object); 
            object = null; 
        } 
    } 
}

  在上面的代碼中,循環申請object對象,並添加到Vector中,而後設置object=null(就是清除棧中引用變量object),可是這些對象被vector引用着,必然不能被GC回收,形成內存泄露。所以要釋放這些對象,還須要將它們從vector中刪除,最簡單的方法就是將vector=null,清空集合類中的引用。

監聽器

  在 Java 編程中,咱們都須要和監聽器打交道,一般一個應用中會用到不少監聽器,咱們會調用一個控件,諸如 addXXXListener() 等方法來增長監聽器,但每每在釋放的時候卻沒有去刪除這些監聽器,從而增長了內存泄漏的機會。

各類鏈接

  好比數據庫鏈接(dataSourse.getConnection()),網絡鏈接 (socket) 和 IO 鏈接,除非其顯式的調用了其close() 方 法將其鏈接關閉,不然是不會自動被 GC 回收的。對於 Resultset 和 Statement 對象能夠不進行顯式回收,但 Connection 必定要顯式回收,由於 Connection 在任什麼時候候都沒法自動回收,而 Connection一旦回收,Resultset 和 Statement 對象就會當即爲 NULL。可是若是使用鏈接池,狀況就不同了,除了要顯式地關閉鏈接,還必須顯式地關閉 Resultset Statement 對象(關閉其中一個,另一個也會關閉),不然就會形成大量的 Statement 對象沒法釋放,從而引發內存泄漏。這種狀況下通常都會在 try 裏面去的鏈接,在 finally 裏面釋放鏈接。

內部類和外部模塊等的引用

  內部類的引用是比較容易遺忘的一種,並且一旦沒釋放可能致使一系列的後繼類對象沒有釋放。在調用外部模塊的時候,也應該注意防止內存泄漏,若是模塊A調用了外部模塊B的一個方法,如: public void register(Object o) 這個方法有可能就使得A模塊持有傳入對象的引用,這時候須要查看B模塊是否提供了出去引用的方法,這種狀況容易忽略,並且發生內存泄漏的話,還比較難察覺。

單例模式

  由於單利對象初始化後將在 JVM 的整個生命週期內存在,若是它持有一個外部對象的(生命週期比較短)引用,那麼這個外部對象就不能被回收,從而致使內存泄漏。若是這個外部對象還持有其餘對象的引用,那麼內存泄漏更嚴重。

8. 如何減小線程上下文切換

使用多線程時,不是多線程能提高程序的執行速度,使用多線程是爲了更好地利用 CPU 資源

程序在執行時,多線程是 CPU 經過給每一個線程分配 CPU 時間片來實現的,時間片是CPU分配給每一個線程執行的時間,因時間片很是短,因此CPU 經過不停地切換線程執行

線程不是越多就越好的,由於線程上下文切換是有性能損耗的,在使用多線程的同時須要考慮如何減小上下文切換

通常來講有如下幾條經驗

  • 無鎖併發編程。多線程競爭時,會引發上下文切換,因此多線程處理數據時,能夠用一些辦法來避免使用鎖,如將數據的 ID 按照Hash取模分段,不一樣的線程處理不一樣段的數據
  • CAS算法。Java 的 Atomic 包使用 CAS 算法來更新數據,而不須要加鎖
  • 控制線程數量。避免建立不須要的線程,好比任務不多,可是建立了不少線程來處理,這樣會形成大量線程都處於等待狀態
  • 協程。在單線程裏實現多任務的調度,並在單線程裏維持多個任務間的切換
  • 協程能夠當作是用戶態自管理的「線程」不會參與CPU時間調度,沒有均衡分配到時間。非搶佔式

還能夠考慮咱們的應用是IO密集型的仍是CPU密集型的。

  • 若是是IO密集型的話,線程能夠多一些。
  • 若是是CPU密集型的話,線程不宜太多。

9. 線程間通訊和進程間通訊

線程間通訊

  • synchronized 同步

    • 這種方式,本質上就是 「共享內存」 式的通訊。多個線程須要訪問同一個共享變量,誰拿到了鎖(得到了訪問權限),誰就能夠執行。
  • while 輪詢的方式

    • 在這種方式下,線程A不斷地改變條件,線程 ThreadB 不停地經過 while 語句檢測這個條件(list.size()==5) 是否成立 ,從而實現了線程間的通訊。可是這種方式會浪費 CPU 資源。之因此說它浪費資源,是由於 JVM 調度器將 CPU 交給線程B執行時,它沒作啥「有用」的工做,只是在不斷地測試某個條件是否成立。就相似於現實生活中,某我的一直看着手機屏幕是否有電話來了,而不是: 在幹別的事情,當有電話來時,響鈴通知TA電話來了。
  • wait/notify 機制

    • 當條件未知足時,線程A調用 wait() 放棄CPU,並進入阻塞狀態。(不像 while 輪詢那樣佔用 CPU)

      當條件知足時,線程B調用 notify() 通知線程A,所謂通知線程A,就是喚醒線程A,並讓它進入可運行狀態。

  • 管道通訊

    • java.io.PipedInputStream 和 java.io.PipedOutputStream 進行通訊

進程間通訊

  • 管道(Pipe) :管道可用於具備親緣關係進程間的通訊,容許一個進程和另外一個與它有共同祖先的進程之間進行通訊。
  • 命名管道(named pipe) :命名管道克服了管道沒有名字的限制,所以,除具備管道所具備的功能外,它還容許無親緣關 系 進程間的通訊。命名管道在文件系統中有對應的文件名。命名管道經過命令mkfifo或系統調用mkfifo來建立。
  • 信號(Signal) :信號是比較複雜的通訊方式,用於通知接受進程有某種事件發生,除了用於進程間通訊外,進程還能夠發送 信號給進程自己;Linux除了支持Unix早期信號語義函數sigal外,還支持語義符合Posix.1標準的信號函數sigaction(實際上,該函數是基於BSD的,BSD爲了實現可靠信號機制,又可以統一對外接口,用sigaction函數從新實現了signal函數)。
  • 消息(Message)隊列 :消息隊列是消息的連接表,包括Posix消息隊列system V消息隊列。有足夠權限的進程能夠向隊列中添加消息,被賦予讀權限的進程則能夠讀走隊列中的消息。消息隊列克服了信號承載信息量少,管道只能承載無格式字節流以及緩衝區大小受限等缺
  • 共享內存 :使得多個進程能夠訪問同一塊內存空間,是最快的可用IPC形式。是針對其餘通訊機制運行效率較低而設計的。每每與其它通訊機制,如信號量結合使用,來達到進程間的同步及互斥。
  • 內存映射(mapped memory) :內存映射容許任何多個進程間通訊,每個使用該機制的進程經過把一個共享的文件映射到本身的進程地址空間來實現它。
  • 信號量(semaphore) :主要做爲進程間以及同一進程不一樣線程之間的同步手段。
  • 套接口(Socket) :更爲通常的進程間通訊機制,可用於不一樣機器之間的進程間通訊。起初是由Unix系統的BSD分支開發出來的,但如今通常能夠移植到其它類Unix系統上:linux和System V的變種都支持套接字。

參考資料:

10. 什麼是同步和異步,阻塞和非阻塞?

同步和異步關注的是消息通訊機制 (synchronous communication/ asynchronous communication)

同步

  • 在發出一個同步調用時,在沒有獲得結果以前,該調用就不返回。
  • 例如:按下電飯鍋的煮飯按鈕,而後等待飯煮好,把飯盛出來,而後再去炒菜。

異步

  • 在發出一個異步調用後,調用者不會馬上獲得結果,該調用就返回了。
  • 例如:按下電鈕鍋的煮飯按鈕,直接去炒菜或者作別的事情,當電飯鍋「滴滴滴」響的時候,再回去把飯盛出來。顯然,異步式編程要比同步式編程高效得多。

阻塞和非阻塞關注的是程序在等待調用結果(消息,返回值)時的狀態.

阻塞

  • 調用結果返回以前,當前線程會被掛起。調用線程只有在獲得結果以後纔會返回。
  • 例子:你打電話問書店老闆有沒有《分佈式系統》這本書,你若是是阻塞式調用,你會一直把本身「掛起」,直到獲得這本書有沒有的結果

非阻塞

  • 在不能馬上獲得結果以前,該調用不會阻塞當前線程。
  • 例子:你打電話問書店老闆有沒有《分佈式系統》這本書,你無論老闆有沒有告訴你,你本身先一邊去玩了, 固然你也要偶爾過幾分鐘check一下老闆有沒有返回結果。

參考資料:

11. Java中的鎖

本小結參考:Java 中的鎖 - Java 併發性和多線程 - 極客學院Wiki

  鎖像 synchronized 同步塊同樣,是一種線程同步機制,但比 Java 中的 synchronized 同步塊更復雜。由於鎖(以及其它更高級的線程同步機制)是由 synchronized 同步塊的方式實現的,因此咱們還不能徹底擺脫 synchronized 關鍵字(譯者注:這說的是 Java 5 以前的狀況)。

  自 Java 5 開始,java.util.concurrent.locks 包中包含了一些鎖的實現,所以你不用去實現本身的鎖了。可是你仍然須要去了解怎樣使用這些鎖,且瞭解這些實現背後的理論也是頗有用處的。能夠參考我對 java.util.concurrent.locks.Lock 的介紹,以瞭解更多關於鎖的信息。

一個簡單的鎖

讓咱們從 java 中的一個同步塊開始:

public class Counter{
    private int count = 0;

    public int inc(){
        synchronized(this){
            return ++count;
        }
    }
}

能夠看到在 inc()方法中有一個 synchronized(this)代碼塊。該代碼塊能夠保證在同一時間只有一個線程能夠執行 return ++count。雖然在 synchronized 的同步塊中的代碼能夠更加複雜,可是++count 這種簡單的操做已經足以表達出線程同步的意思。

如下的 Counter 類用 Lock 代替 synchronized 達到了一樣的目的:

public class Counter{
    private Lock lock = new Lock();
    private int count = 0;

    public int inc(){
        lock.lock();
        int newCount = ++count;
        lock.unlock();
        return newCount;
    }
}

lock()方法會對 Lock 實例對象進行加鎖,所以全部對該對象調用 lock()方法的線程都會被阻塞,直到該 Lock 對象的 unlock()方法被調用。

這裏有一個 Lock 類的簡單實現:

public class Counter{
public class Lock{
    private boolean isLocked = false;

    public synchronized void lock()
        throws InterruptedException{
        while(isLocked){
            wait();
        }
        isLocked = true;
    }

    public synchronized void unlock(){
        isLocked = false;
        notify();
    }
}

注意其中的 while(isLocked) 循環,它又被叫作 「自旋鎖」。自旋鎖以及 wait() 和 notify() 方法在線程通訊這篇文章中有更加詳細的介紹。當 isLocked 爲 true 時,調用 lock() 的線程在 wait() 調用上阻塞等待。爲防止該線程沒有收到 notify() 調用也從 wait() 中返回(也稱做虛假喚醒),這個線程會從新去檢查 isLocked 條件以決定當前是否能夠安全地繼續執行仍是須要從新保持等待,而不是認爲線程被喚醒了就能夠安全地繼續執行了。若是 isLocked 爲 false,當前線程會退出 while(isLocked) 循環,並將 isLocked 設回 true,讓其它正在調用 lock() 方法的線程可以在 Lock 實例上加鎖。

當線程完成了臨界區(位於 lock()和 unlock()之間)中的代碼,就會調用 unlock()。執行 unlock()會從新將 isLocked 設置爲 false,而且通知(喚醒)其中一個(如有的話)在 lock()方法中調用了 wait()函數而處於等待狀態的線程。

鎖的可重入性

Java 中的 synchronized 同步塊是可重入的。這意味着若是一個 Java 線程進入了代碼中的 synchronized 同步塊,並所以得到了該同步塊使用的同步對象對應的管程上的鎖,那麼這個線程能夠進入由同一個管程對象所同步的另外一個 java 代碼塊。下面是一個例子:

public class Reentrant{
    public synchronized outer(){
        inner();
    }

    public synchronized inner(){
        //do something
    }
}

注意 outer()和 inner()都被聲明爲 synchronized,這在 Java 中和 synchronized(this) 塊等效。若是一個線程調用了 outer(),在 outer()裏調用 inner()就沒有什麼問題,由於這兩個方法(代碼塊)都由同一個管程對象(」this」) 所同步。若是一個線程已經擁有了一個管程對象上的鎖,那麼它就有權訪問被這個管程對象同步的全部代碼塊。這就是可重入。線程能夠進入任何一個它已經擁有的鎖所同步着的代碼塊。

前面給出的鎖實現不是可重入的。若是咱們像下面這樣重寫 Reentrant 類,當線程調用 outer() 時,會在 inner()方法的 lock.lock() 處阻塞住。

public class Reentrant2{
    Lock lock = new Lock();

    public outer(){
        lock.lock();
        inner();
        lock.unlock();
    }

    public synchronized inner(){
        lock.lock();
        //do something
        lock.unlock();
    }
}

調用 outer() 的線程首先會鎖住 Lock 實例,而後繼續調用 inner()。inner()方法中該線程將再一次嘗試鎖住 Lock 實例,結果該動做會失敗(也就是說該線程會被阻塞),由於這個 Lock 實例已經在 outer()方法中被鎖住了。

兩次 lock()之間沒有調用 unlock(),第二次調用 lock 就會阻塞,看過 lock() 實現後,會發現緣由很明顯:

public class Lock{
    boolean isLocked = false;

    public synchronized void lock()
        throws InterruptedException{
        while(isLocked){
            wait();
        }
        isLocked = true;
    }

    ...
}

一個線程是否被容許退出 lock()方法是由 while 循環(自旋鎖)中的條件決定的。當前的判斷條件是隻有當 isLocked 爲 false 時 lock 操做才被容許,而沒有考慮是哪一個線程鎖住了它。

爲了讓這個 Lock 類具備可重入性,咱們須要對它作一點小的改動:

public class Lock{
    boolean isLocked = false;
    Thread  lockedBy = null;
    int lockedCount = 0;

    public synchronized void lock()
        throws InterruptedException{
        Thread callingThread =
            Thread.currentThread();
        while(isLocked && lockedBy != callingThread){
            wait();
        }
        isLocked = true;
        lockedCount++;
        lockedBy = callingThread;
  }

    public synchronized void unlock(){
        if(Thread.curentThread() ==
            this.lockedBy){
            lockedCount--;

            if(lockedCount == 0){
                isLocked = false;
                notify();
            }
        }
    }

    ...
}

注意到如今的 while 循環(自旋鎖)也考慮到了已鎖住該 Lock 實例的線程。若是當前的鎖對象沒有被加鎖(isLocked = false),或者當前調用線程已經對該 Lock 實例加了鎖,那麼 while 循環就不會被執行,調用 lock()的線程就能夠退出該方法(譯者注:「被容許退出該方法」在當前語義下就是指不會調用 wait()而致使阻塞)。

除此以外,咱們須要記錄同一個線程重複對一個鎖對象加鎖的次數。不然,一次 unblock()調用就會解除整個鎖,即便當前鎖已經被加鎖過屢次。在 unlock()調用沒有達到對應 lock()調用的次數以前,咱們不但願鎖被解除。

如今這個 Lock 類就是可重入的了。

鎖的公平性

Java 的 synchronized 塊並不保證嘗試進入它們的線程的順序。所以,若是多個線程不斷競爭訪問相同的 synchronized 同步塊,就存在一種風險,其中一個或多個線程永遠也得不到訪問權 —— 也就是說訪問權老是分配給了其它線程。這種狀況被稱做線程飢餓。爲了不這種問題,鎖須要實現公平性。本文所展示的鎖在內部是用 synchronized 同步塊實現的,所以它們也不保證公平性。飢餓和公平中有更多關於該內容的討論。

在 finally 語句中調用 unlock()

若是用 Lock 來保護臨界區,而且臨界區有可能會拋出異常,那麼在 finally 語句中調用 unlock()就顯得很是重要了。這樣能夠保證這個鎖對象能夠被解鎖以便其它線程能繼續對其加鎖。如下是一個示例:

lock.lock();
try{
    //do critical section code,
    //which may throw exception
} finally {
    lock.unlock();
}

這個簡單的結構能夠保證當臨界區拋出異常時 Lock 對象能夠被解鎖。若是不是在 finally 語句中調用的 unlock(),當臨界區拋出異常時,Lock 對象將永遠停留在被鎖住的狀態,這會致使其它全部在該 Lock 對象上調用 lock()的線程一直阻塞。

12. 併發包(J.U.C)下面,都用過什麼

  • concurrent下面的包
    • Executor 用來建立線程池,在實現Callable接口時,添加線程。
    • FeatureTask 此 FutureTask 的 get 方法所返回的結果類型。
    • TimeUnit
    • Semaphore
    • LinkedBlockingQueue
  • 所用過的類
    • Executor

13. 從volatile說到,i++原子操做,線程安全問題

從 volatile 說到,i++原子操做,線程安全問題 - CSDN博客
https://blog.csdn.net/zbw18297786698/article/details/53420780

參考資料

相關文章
相關標籤/搜索