備戰-Java 併發

濤姐濤哥發表於2021-07-20

備戰-Java 併發

 

      誰念西風獨自涼,蕭蕭黃葉閉疏窗

 

簡介:備戰-Java 併發。

一、執行緒的使用

有三種使用執行緒的方法:

  • 實現 Runnable 介面;
  • 實現 Callable 介面;
  • 繼承 Thread 類。

實現 Runnable 和 Callable 介面的類只能當做一個可以線上程中執行的任務,不是真正意義上的執行緒,因此最後還需要通過 Thread 來呼叫。可以理解為任務是通過執行緒驅動從而執行的。

1、實現 Runnable 介面

需要實現介面中的 run() 方法。

1 public class MyRunnable implements Runnable {
2     @Override
3     public void run() {
4         // do your own business
5     }
6 }

使用 Runnable 例項再建立一個 Thread 例項,然後呼叫 Thread 例項的 start() 方法來啟動執行緒。

備戰-Java 併發
1 public static void main(String[] args) {
2     MyRunnable instance = new MyRunnable();
3     Thread thread = new Thread(instance);
4     thread.start();
5 }
View Code

2、實現 Callable 介面

與 Runnable 相比,Callable 可以有返回值,返回值通過 FutureTask 進行封裝。

備戰-Java 併發
public class MyCallable implements Callable<Integer> {
    public Integer call() {
        return 666;
    }
}
View Code
備戰-Java 併發
1 public static void main(String[] args) throws ExecutionException, InterruptedException {
2     MyCallable mc = new MyCallable();
3     FutureTask<Integer> ft = new FutureTask<>(mc);
4     Thread thread = new Thread(ft);
5     thread.start();
6     System.out.println(ft.get());  // 666
7 }
View Code

3、繼承 Thread 類

同樣也是需要實現 run() 方法,因為 Thread 類也實現了 Runable 介面。

當呼叫 start() 方法啟動一個執行緒時,虛擬機器會將該執行緒放入就緒佇列中等待被排程,當一個執行緒被排程時會執行該執行緒的 run() 方法。

1 public class MyThread extends Thread {
2     public void run() {
3         // do what you want to do
4     }
5 }
1 public static void main(String[] args) {
2     MyThread mt = new MyThread();
3     mt.start();
4 }

4、實現Runnable/Callable 介面 VS 繼承 Thread

實現介面會更好一些,因為:

  • Java 不支援多重繼承,因此繼承了 Thread 類就無法繼承其它類,但是可以實現多個介面;
  • 類可能只要求可執行就行,繼承整個 Thread 類開銷過大。

二、基礎執行緒機制

1、Executor

Executor 管理多個非同步任務的執行,而無需程式設計師顯式地管理執行緒的生命週期。這裡的非同步是指多個任務的執行互不干擾,不需要進行同步操作。

主要有三種 Executor:

  • CachedThreadPool:一個任務建立一個執行緒;
  • FixedThreadPool:所有任務只能使用固定大小的執行緒;
  • SingleThreadExecutor:相當於大小為 1 的 FixedThreadPool。
備戰-Java 併發
1 public static void main(String[] args) {
2     ExecutorService executorService = Executors.newCachedThreadPool();
3     for (int i = 0; i < 5; i++) {
4         executorService.execute(new MyRunnable());
5     }
6     executorService.shutdown();
7 }
View Code

2、Daemon

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

當所有非守護執行緒結束時,程式也就終止,同時會殺死所有守護執行緒。

main() 屬於非守護執行緒。

線上程啟動之前使用 setDaemon() 方法可以將一個執行緒設定為守護執行緒。

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

守護執行緒應用場景:

  • QQ、飛訊等等聊天軟體,主程式是非守護執行緒,而所有的聊天視窗是守護執行緒 ,當在聊天的過程中,直接關閉聊天應用程式時,聊天視窗也會隨之關閉,但不是立即關閉,而是需要緩衝,等待接收到關閉命令後才會執行視窗關閉操作。
  • JVM 中gc 執行緒是守護執行緒,作用就是當所有使用者自定義線以及主執行緒執行完畢後,gc執行緒才停止。

3、sleep()

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

sleep() 可能會丟擲 InterruptedException,因為異常不能跨執行緒傳播回 main() 中,因此必須在本地進行處理。執行緒中丟擲的其它異常也同樣需要在本地進行處理。

備戰-Java 併發
1 public void run() {
2     try {
3         Thread.sleep(3000);
4     } catch (InterruptedException e) {
5         e.printStackTrace();
6     }
7 }
View Code

4、yield()

對靜態方法 Thread.yield() 的呼叫宣告瞭當前執行緒已經完成了生命週期中最重要的部分,可以切換給其它執行緒來執行。該方法只是對執行緒排程器的一個建議,而且也只是建議具有相同優先順序的其它執行緒可以執行。

1 public void run() {
2     Thread.yield();
3 }

三、中斷

一個執行緒執行完畢之後會自動結束,如果在執行過程中發生異常也會提前結束。

1、InterruptedException

通過呼叫一個執行緒的 interrupt() 來中斷該執行緒,如果該執行緒處於阻塞、限期等待或者無限期等待狀態,那麼就會丟擲 InterruptedException,從而提前結束該執行緒。但是不能中斷 I/O 阻塞和 synchronized 鎖阻塞。

對於以下程式碼,在 main() 中啟動一個執行緒之後再中斷它,由於執行緒中呼叫了 Thread.sleep() 方法,因此會丟擲一個 InterruptedException,從而提前結束執行緒,不執行之後的語句。

備戰-Java 併發
 1 public class InterruptExample {
 2 
 3     private static class MyThread1 extends Thread {
 4         @Override
 5         public void run() {
 6             try {
 7                 Thread.sleep(2000);
 8                 System.out.println("Thread run");
 9             } catch (InterruptedException e) {
10                 e.printStackTrace();
11             }
12         }
13     }
14 }
View Code
備戰-Java 併發
1 public static void main(String[] args) throws InterruptedException {
2     Thread thread1 = new MyThread1();
3     thread1.start();
4     thread1.interrupt();
5     System.out.println("Main run");
6 }
View Code
備戰-Java 併發
1 Main run
2 java.lang.InterruptedException: sleep interrupted
3     at java.lang.Thread.sleep(Native Method)
4     at InterruptExample.lambda$main$0(InterruptExample.java:5)
5     at InterruptExample$$Lambda$1/713338599.run(Unknown Source)
6     at java.lang.Thread.run(Thread.java:745)
View Code

2、interrupted()

如果一個執行緒的 run() 方法執行一個無限迴圈,並且沒有執行 sleep() 等會丟擲 InterruptedException 的操作,那麼呼叫執行緒的 interrupt() 方法就無法使執行緒提前結束。

但是呼叫 interrupt() 方法會設定執行緒的中斷標記,此時呼叫 interrupted() 方法會返回 true。因此可以在迴圈體中使用 interrupted() 方法來判斷執行緒是否處於中斷狀態,從而提前結束執行緒。

備戰-Java 併發
 1 public class InterruptExample {
 2 
 3     private static class MyThread2 extends Thread {
 4         @Override
 5         public void run() {
 6             while (!interrupted()) {
 7                 // ..
 8             }
 9             System.out.println("Thread end");
10         }
11     }
12 }
View Code
備戰-Java 併發
1 public static void main(String[] args) throws InterruptedException {
2     Thread thread2 = new MyThread2();
3     thread2.start();
4     thread2.interrupt();
5     // thread end
6 }
View Code

3、Executor 的中斷操作

呼叫 Executor 的 shutdown() 方法會等待執行緒都執行完畢之後再關閉,但是如果呼叫的是 shutdownNow() 方法,則相當於呼叫每個執行緒的 interrupt() 方法。

以下使用 Lambda 建立執行緒,相當於建立了一個匿名內部執行緒。

備戰-Java 併發
 1 public static void main(String[] args) {
 2     ExecutorService executorService = Executors.newCachedThreadPool();
 3     executorService.execute(() -> {
 4         try {
 5             Thread.sleep(2000);
 6             System.out.println("Thread run");
 7         } catch (InterruptedException e) {
 8             e.printStackTrace();
 9         }
10     });
11     executorService.shutdownNow();
12     System.out.println("Main run");
13 }
View Code
備戰-Java 併發
1 Main run
2 java.lang.InterruptedException: sleep interrupted
3     at java.lang.Thread.sleep(Native Method)
4     at ExecutorInterruptExample.lambda$main$0(ExecutorInterruptExample.java:9)
5     at ExecutorInterruptExample$$Lambda$1/1160460865.run(Unknown Source)
6     at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
7     at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
8     at java.lang.Thread.run(Thread.java:745)
View Code

如果只想中斷 Executor 中的一個執行緒,可以通過使用 submit() 方法來提交一個執行緒,它會返回一個 Future<?> 物件,通過呼叫該物件的 cancel(true) 方法就可以中斷執行緒。

1 Future<?> future = executorService.submit(() -> {
2     // do your own business
3 });
4 future.cancel(true);

四、互斥同步

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

1、synchronized

同步一個程式碼塊

1 public void func() {
2     synchronized (this) {
3         // do your own business
4     }
5 }

它只作用於同一個物件,如果呼叫兩個物件上的同步程式碼塊,就不會進行同步。

對於以下程式碼,使用 ExecutorService 執行了兩個執行緒,由於呼叫的是同一個物件的同步程式碼塊,因此這兩個執行緒會進行同步,當一個執行緒進入同步語句塊時,另一個執行緒就必須等待。

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

對於以下程式碼,兩個執行緒呼叫了不同物件的同步程式碼塊,因此這兩個執行緒就不需要同步。從輸出結果可以看出,兩個執行緒交叉執行。

備戰-Java 併發
1 // e1 和 e2 兩個不同的物件
2 public static void main(String[] args) {
3     SynchronizedExample e1 = new SynchronizedExample();
4     SynchronizedExample e2 = new SynchronizedExample();
5     ExecutorService executorService = Executors.newCachedThreadPool();
6     executorService.execute(() -> e1.func1());
7     executorService.execute(() -> e2.func1());
8 }
View Code
列印:0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9

同步一個方法

1 public synchronized void func () {
2     // do what you want to do
3 }

它和同步程式碼塊一樣,作用於同一個物件。

同步一個類

1 public void func() {
2     synchronized (SynchronizedExample.class) {
3         // do what you want to do
4     }
5 }

作用於整個類,也就是說兩個執行緒呼叫同一個類的不同物件上的這種同步語句,也會進行同步。

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

同步一個靜態方法

1 public synchronized static void fun() {
2     // synchronized 同步靜態方法作用於整個類
3 }

2、ReentrantLock

ReentrantLock 是 java.util.concurrent(J.U.C)包中的鎖。

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

3、比較

鎖的實現

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

效能

新版本 Java 對 synchronized 進行了很多優化,例如自旋鎖等,synchronized 與 ReentrantLock 大致相同。

等待可中斷

當持有鎖的執行緒長期不釋放鎖的時候,正在等待的執行緒可以選擇放棄等待,改為處理其他事情。

ReentrantLock 可中斷,而 synchronized 不行。

公平鎖

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

synchronized 中的鎖是非公平的,ReentrantLock 預設情況下也是非公平的,但是也可以是公平的。

鎖繫結多個條件

一個 ReentrantLock 可以同時繫結多個 Condition 物件。

4、使用選擇

除非需要使用 ReentrantLock 的高階功能,否則優先使用 synchronized。這是因為 synchronized 是 JVM 實現的一種鎖機制,JVM 原生地支援它,而 ReentrantLock 不是所有的 JDK 版本都支援。並且使用 synchronized 不用擔心沒有釋放鎖而導致死鎖問題,因為 JVM 會確保鎖的釋放。

五、執行緒之間的協作

當多個執行緒可以一起工作去解決某個問題時,如果某些部分必須在其它部分之前完成,那麼就需要對執行緒進行協調。

1、join()

線上程中呼叫另一個執行緒的 join() 方法,會將當前執行緒掛起,而不是忙等待,直到目標執行緒結束。

對於以下程式碼,雖然 b 執行緒先啟動,但是因為在 b 執行緒中呼叫了 a 執行緒的 join() 方法,b 執行緒會等待 a 執行緒結束才繼續執行,因此最後能夠保證 a 執行緒的輸出先於 b 執行緒的輸出。

備戰-Java 併發
 1 public class JoinExample {
 2 
 3     private class A extends Thread {
 4         @Override
 5         public void run() {
 6             System.out.println("A");
 7         }
 8     }
 9 
10     private class B extends Thread {
11 
12         private A a;
13 
14         B(A a) {
15             this.a = a;
16         }
17 
18         @Override
19         public void run() {
20             try {
21                 a.join();
22             } catch (InterruptedException e) {
23                 e.printStackTrace();
24             }
25             System.out.println("B");
26         }
27     }
28 
29     public void test() {
30         A a = new A();
31         B b = new B(a);
32         b.start();
33         a.start();
34     }
35 }
View Code
1 public static void main(String[] args) {
2     JoinExample example = new JoinExample();
3     example.test();
4 }
輸出:
A
B

2、wait()、 notify()、 notifyAll()

呼叫 wait() 使得執行緒等待某個條件滿足,執行緒在等待時會被掛起,當其他執行緒的執行使得這個條件滿足時,其它執行緒會呼叫 notify() 或者 notifyAll() 來喚醒掛起的執行緒。

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

只能用在同步方法或者同步控制塊中使用,否則會在執行時丟擲 IllegalMonitorStateException。

使用 wait() 掛起期間,執行緒會釋放鎖。這是因為,如果沒有釋放鎖,那麼其它執行緒就無法進入物件的同步方法或者同步控制塊中,那麼就無法執行 notify() 或者 notifyAll() 來喚醒掛起的執行緒,造成死鎖。

備戰-Java 併發
 1 public class WaitNotifyExample {
 2 
 3     public synchronized void before() {
 4         System.out.println("before");
 5         notifyAll();
 6     }
 7 
 8     public synchronized void after() {
 9         try {
10             wait();
11         } catch (InterruptedException e) {
12             e.printStackTrace();
13         }
14         System.out.println("after");
15     }
16 }
View Code
1 public static void main(String[] args) {
2     ExecutorService executorService = Executors.newCachedThreadPool();
3     WaitNotifyExample example = new WaitNotifyExample();
4     executorService.execute(() -> example.after());
5     executorService.execute(() -> example.before());
6 }
輸出:
before
after

wait() 和 sleep() 的區別

  • wait() 是 Object 的方法,而 sleep() 是 Thread 的靜態方法;
  • wait() 會釋放鎖,sleep() 不會。

3、await() 、signal()、 signalAll()

java.util.concurrent 類庫中提供了 Condition 類來實現執行緒之間的協調,可以在 Condition 上呼叫 await() 方法使執行緒等待,其它執行緒呼叫 signal() 或 signalAll() 方法喚醒等待的執行緒。

相比於 wait() 這種等待方式,await() 可以指定等待的條件,因此更加靈活。

使用 Lock 來獲取一個 Condition 物件。

備戰-Java 併發
 1 public class AwaitSignalExample {
 2 
 3     private Lock lock = new ReentrantLock();
 4     private Condition condition = lock.newCondition();
 5 
 6     public void before() {
 7         lock.lock();
 8         try {
 9             System.out.println("before");
10             condition.signalAll();
11         } finally {
12             lock.unlock();
13         }
14     }
15 
16     public void after() {
17         lock.lock();
18         try {
19             condition.await();
20             System.out.println("after");
21         } catch (InterruptedException e) {
22             e.printStackTrace();
23         } finally {
24             lock.unlock();
25         }
26     }
27 }
View Code
1 public static void main(String[] args) {
2     ExecutorService executorService = Executors.newCachedThreadPool();
3     AwaitSignalExample example = new AwaitSignalExample();
4     executorService.execute(() -> example.after());
5     executorService.execute(() -> example.before());
6 }
1 輸出:
2 before
3 after

六、執行緒狀態

一個執行緒只能處於一種狀態(六大狀態:新建、可執行、阻塞、無限期等待、限期等待、死亡),並且這裡的執行緒狀態特指 Java 虛擬機器的執行緒狀態,不能反映執行緒在特定作業系統下的狀態。

1、新建(NEW)

建立後尚未啟動。

2、可執行(RUNABLE)

正在 Java 虛擬機器中執行。但是在作業系統層面,它可能處於執行狀態,也可能等待資源排程(例如處理器資源),資源排程完成就進入執行狀態。所以該狀態的可執行是指可以被執行,具體有沒有執行要看底層作業系統的資源排程。

3、阻塞(BLOCKED)

請求獲取 monitor lock 從而進入 synchronized 函式或者程式碼塊,但是其它執行緒已經佔用了該 monitor lock,所以出於阻塞狀態。要結束該狀態進入從而 RUNABLE 需要其他執行緒釋放 monitor lock。

4、無限期等待(WAITING)

等待其它執行緒顯式地喚醒。

阻塞和等待的區別在於,阻塞是被動的,它是在等待獲取 monitor lock。而等待是主動的,通過呼叫 Object.wait() 等方法進入。

進入方法退出方法
沒有設定 Timeout 引數的 Object.wait() 方法 Object.notify() / Object.notifyAll()
沒有設定 Timeout 引數的 Thread.join() 方法 被呼叫的執行緒執行完畢
LockSupport.park() 方法 LockSupport.unpark(Thread)

5、限期等待(TIMED_WAITING)

無需等待其它執行緒顯式地喚醒,在一定時間之後會被系統自動喚醒。

進入方法退出方法
Thread.sleep() 方法 時間結束
設定了 Timeout 引數的 Object.wait() 方法 時間結束 / Object.notify() / Object.notifyAll()
設定了 Timeout 引數的 Thread.join() 方法 時間結束 / 被呼叫的執行緒執行完畢
LockSupport.parkNanos() 方法 LockSupport.unpark(Thread)
LockSupport.parkUntil() 方法 LockSupport.unpark(Thread)

呼叫 Thread.sleep() 方法使執行緒進入限期等待狀態時,常常用“使一個執行緒睡眠”進行描述。呼叫 Object.wait() 方法使執行緒進入限期等待或者無限期等待時,常常用“掛起一個執行緒”進行描述。睡眠和掛起是用來描述行為,而阻塞和等待用來描述狀態。

6、死亡(TERMINATED)

可以是執行緒結束任務之後自己結束,或者產生了異常而結束。

七、J.U.C - AQS

java.util.concurrent(J.U.C)大大提高了併發效能,AQS 被認為是 J.U.C 的核心。

1、CountDownLatch

用來控制一個或者多個執行緒等待多個執行緒。

維護了一個計數器 cnt,每次呼叫 countDown() 方法會讓計數器的值減 1,減到 0 的時候,那些因為呼叫 await() 方法而在等待的執行緒就會被喚醒。

備戰-Java 併發
 1 public class CountdownLatchExample {
 2 
 3     public static void main(String[] args) throws InterruptedException {
 4         final int totalThread = 10;
 5         CountDownLatch countDownLatch = new CountDownLatch(totalThread);
 6         ExecutorService executorService = Executors.newCachedThreadPool();
 7         for (int i = 0; i < totalThread; i++) {
 8             executorService.execute(() -> {
 9                 System.out.print("run..");
10                 countDownLatch.countDown();
11             });
12         }
13         countDownLatch.await();
14         System.out.println("end");
15         executorService.shutdown();
16     }
17 }
View Code
輸出:run..run..run..run..run..run..run..run..run..run..end

2、CyclicBarrier

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

和 CountdownLatch 相似,都是通過維護計數器來實現的。執行緒執行 await() 方法之後計數器會減 1,並進行等待,直到計數器為 0,所有呼叫 await() 方法而在等待的執行緒才能繼續執行。

CyclicBarrier 和 CountdownLatch 的一個區別是,CyclicBarrier 的計數器通過呼叫 reset() 方法可以迴圈使用,所以它才叫做迴圈屏障。

CyclicBarrier 有兩個建構函式,其中 parties 指示計數器的初始值,barrierAction 在所有執行緒都到達屏障的時候會執行一次。

備戰-Java 併發
 1 public CyclicBarrier(int parties, Runnable barrierAction) {
 2     if (parties <= 0) throw new IllegalArgumentException();
 3     this.parties = parties;
 4     this.count = parties;
 5     this.barrierCommand = barrierAction;
 6 }
 7 
 8 public CyclicBarrier(int parties) {
 9     this(parties, null);
10 }
View Code

備戰-Java 併發
 1 public class CyclicBarrierExample {
 2 
 3     public static void main(String[] args) {
 4         final int totalThread = 10;
 5         CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
 6         ExecutorService executorService = Executors.newCachedThreadPool();
 7         for (int i = 0; i < totalThread; i++) {
 8             executorService.execute(() -> {
 9                 System.out.print("before..");
10                 try {
11                     cyclicBarrier.await();
12                 } catch (InterruptedException | BrokenBarrierException e) {
13                     e.printStackTrace();
14                 }
15                 System.out.print("after..");
16             });
17         }
18         executorService.shutdown();
19     }
20 }
View Code
輸出:before..before..before..before..before..before..before..before..before..before..after..after..after..after..after..after..after..after..after..after..

3、Semaphore

Semaphore 類似於作業系統中的訊號量,可以控制對互斥資源的訪問執行緒數。

以下程式碼模擬了對某個服務的併發請求,每次只能有 3 個客戶端同時訪問,請求總數為 10。

備戰-Java 併發
 1 public class SemaphoreExample {
 2 
 3     public static void main(String[] args) {
 4         final int clientCount = 3;
 5         final int totalRequestCount = 10;
 6         Semaphore semaphore = new Semaphore(clientCount);
 7         ExecutorService executorService = Executors.newCachedThreadPool();
 8         for (int i = 0; i < totalRequestCount; i++) {
 9             executorService.execute(()->{
10                 try {
11                     semaphore.acquire();
12                     System.out.print(semaphore.availablePermits() + " ");
13                 } catch (InterruptedException e) {
14                     e.printStackTrace();
15                 } finally {
16                     semaphore.release();
17                 }
18             });
19         }
20         executorService.shutdown();
21     }
22 }
View Code
輸出:2 1 2 2 2 2 2 1 2 2

CountDownLatch、CyclicBarrier、Semaphore 簡單問連線:https://www.cnblogs.com/taojietaoge/archive/2019/08/01/11188118.html

八、J.U.C - 其它元件

1、FutureTask

在介紹 Callable 時我們知道它可以有返回值,返回值通過 Future<V> 進行封裝。FutureTask 實現了 RunnableFuture 介面,該介面繼承自 Runnable 和 Future<V> 介面,這使得 FutureTask 既可以當做一個任務執行,也可以有返回值。

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

FutureTask 可用於非同步獲取執行結果或取消執行任務的場景。當一個計算任務需要執行很長時間,那麼就可以用 FutureTask 來封裝這個任務,主執行緒在完成自己的任務之後再去獲取結果。

備戰-Java 併發
 1 public class FutureTaskExample {
 2 
 3     public static void main(String[] args) throws ExecutionException, InterruptedException {
 4         FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
 5             @Override
 6             public Integer call() throws Exception {
 7                 int result = 0;
 8                 for (int i = 0; i < 100; i++) {
 9                     Thread.sleep(10);
10                     result += i;
11                 }
12                 return result;
13             }
14         });
15 
16         Thread computeThread = new Thread(futureTask);
17         computeThread.start();
18 
19         Thread otherThread = new Thread(() -> {
20             System.out.println("other task is running...");
21             try {
22                 Thread.sleep(1000);
23             } catch (InterruptedException e) {
24                 e.printStackTrace();
25             }
26         });
27         otherThread.start();
28         System.out.println(futureTask.get());
29     }
30 }
View Code
1 輸出:
2 other task is running...
3 4950

2、BlockingQueue

java.util.concurrent.BlockingQueue 介面有以下阻塞佇列的實現:

  • FIFO 佇列 :LinkedBlockingQueue、ArrayBlockingQueue(固定長度)
  • 優先順序佇列 :PriorityBlockingQueue

提供了阻塞的 take() 和 put() 方法:如果佇列為空 take() 將阻塞,直到佇列中有內容;如果佇列為滿 put() 將阻塞,直到佇列有空閒位置。

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

備戰-Java 併發
 1 public class ProducerConsumer {
 2 
 3     private static BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
 4 
 5     private static class Producer extends Thread {
 6         @Override
 7         public void run() {
 8             try {
 9                 queue.put("product");  // 若佇列為滿 put() 將阻塞
10             } catch (InterruptedException e) {
11                 e.printStackTrace();
12             }
13             System.out.print("produce..");
14         }
15     }
16 
17     private static class Consumer extends Thread {
18 
19         @Override
20         public void run() {
21             try {
22                 String product = queue.take();  // 若佇列為空 take() 將阻塞
23             } catch (InterruptedException e) {
24                 e.printStackTrace();
25             }
26             System.out.print("consume..");
27         }
28     }
29 }
View Code
備戰-Java 併發
 1 public static void main(String[] args) {
 2     for (int i = 0; i < 2; i++) {
 3         Producer producer = new Producer();
 4         producer.start();
 5     }
 6     for (int i = 0; i < 5; i++) {
 7         Consumer consumer = new Consumer();
 8         consumer.start();
 9     }
10     for (int i = 0; i < 3; i++) {
11         Producer producer = new Producer();
12         producer.start();
13     }
14 }
View Code
輸出:produce..produce..consume..consume..produce..consume..produce..consume..produce..consume..

3、ForkJoin

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

備戰-Java 併發
 1 public class ForkJoinExample extends RecursiveTask<Integer> {
 2 
 3     private final int threshold = 5;
 4     private int first;
 5     private int last;
 6 
 7     public ForkJoinExample(int first, int last) {
 8         this.first = first;
 9         this.last = last;
10     }
11 
12     @Override
13     protected Integer compute() {
14         int result = 0;
15         if (last - first <= threshold) {
16             // 任務足夠小則直接計算
17             for (int i = first; i <= last; i++) {
18                 result += i;
19             }
20         } else {
21             // 拆分成小任務
22             int middle = first + (last - first) / 2;
23             ForkJoinExample leftTask = new ForkJoinExample(first, middle);
24             ForkJoinExample rightTask = new ForkJoinExample(middle + 1, last);
25             leftTask.fork();
26             rightTask.fork();
27             result = leftTask.join() + rightTask.join();
28         }
29         return result;
30     }
31 }
View Code
備戰-Java 併發
1 public static void main(String[] args) throws ExecutionException, InterruptedException {
2     ForkJoinExample example = new ForkJoinExample(1, 10000);
3     ForkJoinPool forkJoinPool = new ForkJoinPool();
4     Future result = forkJoinPool.submit(example);
5     System.out.println(result.get());
6 }
View Code

ForkJoin 使用 ForkJoinPool 來啟動,它是一個特殊的執行緒池,執行緒數量取決於 CPU 核數。

public class ForkJoinPool extends AbstractExecutorService

ForkJoinPool 實現了工作竊取演算法來提高 CPU 的利用率。每個執行緒都維護了一個雙端佇列,用來儲存需要執行的任務。工作竊取演算法允許空閒的執行緒從其它執行緒的雙端佇列中竊取一個任務來執行。竊取的任務必須是最晚的任務,避免和佇列所屬執行緒發生競爭。例如下圖中,Thread2 從 Thread1 的佇列中拿出最晚的 Task1 任務,Thread1 會拿出 Task2 來執行,這樣就避免發生競爭。但是如果佇列中只有一個任務時還是會發生競爭。

九、執行緒不安全示例

如果多個執行緒對同一個共享資料進行訪問而不採取同步操作的話,那麼操作的結果是不一致的。

以下程式碼演示了 1000 個執行緒同時對 cnt 執行自增操作,操作結束之後它的值有可能小於 1000。

備戰-Java 併發
 1 public class ThreadUnsafeExample {
 2 
 3     private int cnt = 0;
 4 
 5     public void add() {
 6         cnt++;
 7     }
 8 
 9     public int get() {
10         return cnt;
11     }
12 }
View Code
備戰-Java 併發
 1 public static void main(String[] args) throws InterruptedException {
 2     final int threadSize = 1000;
 3     ThreadUnsafeExample example = new ThreadUnsafeExample();
 4     final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
 5     ExecutorService executorService = Executors.newCachedThreadPool();
 6     for (int i = 0; i < threadSize; i++) {
 7         executorService.execute(() -> {
 8             example.add();
 9             countDownLatch.countDown();
10         });
11     }
12     countDownLatch.await();
13     executorService.shutdown();
14     System.out.println(example.get());  // 997
15 }
View Code

十、Java 記憶體模型

Java 記憶體模型試圖遮蔽各種硬體和作業系統的記憶體訪問差異,以實現讓 Java 程式在各種平臺下都能達到一致的記憶體訪問效果。

Java 執行時記憶體參考連結:https://www.cnblogs.com/taojietaoge/p/10264416.html

1、主記憶體與工作記憶體

處理器上的暫存器的讀寫的速度比記憶體快幾個數量級,為了解決這種速度矛盾,在它們之間加入了快取記憶體。

加入快取記憶體帶來了一個新的問題:快取一致性。如果多個快取共享同一塊主記憶體區域,那麼多個快取的資料可能會不一致,需要一些協議來解決這個問題。

所有的變數都儲存在主記憶體中,每個執行緒還有自己的工作記憶體,工作記憶體儲存在快取記憶體或者暫存器中,儲存了該執行緒使用的變數的主記憶體副本拷貝。

執行緒只能直接操作工作記憶體中的變數,不同執行緒之間的變數值傳遞需要通過主記憶體來完成。

2、記憶體間互動操作

Java 記憶體模型定義了 8 個操作來完成主記憶體和工作記憶體的互動操作。

  • read:把一個變數的值從主記憶體傳輸到工作記憶體中
  • load:在 read 之後執行,把 read 得到的值放入工作記憶體的變數副本中
  • use:把工作記憶體中一個變數的值傳遞給執行引擎
  • assign:把一個從執行引擎接收到的值賦給工作記憶體的變數
  • store:把工作記憶體的一個變數的值傳送到主記憶體中
  • write:在 store 之後執行,把 store 得到的值放入主記憶體的變數中
  • lock:作用於主記憶體的變數
  • unlock

3、記憶體模型三大特性

原子性

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 重寫之前執行緒不安全的程式碼之後得到以下執行緒安全實現:

備戰-Java 併發
 1 public class AtomicExample {
 2     private AtomicInteger cnt = new AtomicInteger();
 3 
 4     public void add() {
 5         cnt.incrementAndGet();
 6     }
 7 
 8     public int get() {
 9         return cnt.get();
10     }
11 }
View Code
備戰-Java 併發
 1 public static void main(String[] args) throws InterruptedException {
 2     final int threadSize = 1000;
 3     AtomicExample example = new AtomicExample(); // 只修改這條語句
 4     final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
 5     ExecutorService executorService = Executors.newCachedThreadPool();
 6     for (int i = 0; i < threadSize; i++) {
 7         executorService.execute(() -> {
 8             example.add();
 9             countDownLatch.countDown();
10         });
11     }
12     countDownLatch.await();
13     executorService.shutdown();
14     System.out.println(example.get());  // 1000
15 }
View Code

除了使用原子類之外,也可以使用 synchronized 互斥鎖來保證操作的原子性。它對應的記憶體間互動操作為:lock 和 unlock,在虛擬機器實現上對應的位元組碼指令為 monitorenter 和 monitorexit。

備戰-Java 併發
 1 public class AtomicSynchronizedExample {
 2     private int cnt = 0;
 3 
 4     public synchronized void add() {
 5         cnt++;
 6     }
 7 
 8     public synchronized int get() {
 9         return cnt;
10     }
11 }
View Code
備戰-Java 併發
 1 public static void main(String[] args) throws InterruptedException {
 2     final int threadSize = 1000;
 3     AtomicSynchronizedExample example = new AtomicSynchronizedExample();
 4     final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
 5     ExecutorService executorService = Executors.newCachedThreadPool();
 6     for (int i = 0; i < threadSize; i++) {
 7         executorService.execute(() -> {
 8             example.add();
 9             countDownLatch.countDown();
10         });
11     }
12     countDownLatch.await();
13     executorService.shutdown();
14     System.out.println(example.get());  // 1000
15 }
View Code

可見性

可見性指當一個執行緒修改了共享變數的值,其它執行緒能夠立即得知這個修改。Java 記憶體模型是通過在變數修改後將新值同步回主記憶體,在變數讀取前從主記憶體重新整理變數值來實現可見性的。

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

  • volatile(Java高階語法volatile 連結:https://www.cnblogs.com/taojietaoge/p/10260888.html
  • synchronized,對一個變數執行 unlock 操作之前,必須把變數值同步回主記憶體。
  • final,被 final 關鍵字修飾的欄位在構造器中一旦初始化完成,並且沒有發生 this 逃逸(其它執行緒通過 this 引用訪問到初始化了一半的物件),那麼其它執行緒就能看見 final 欄位的值。

對前面的執行緒不安全示例中的 cnt 變數使用 volatile 修飾,不能解決執行緒不安全問題,因為 volatile 並不能保證操作的原子性。

有序性

有序性是指:在本執行緒內觀察,所有操作都是有序的。在一個執行緒觀察另一個執行緒,所有操作都是無序的,無序是因為發生了指令重排序。在 Java 記憶體模型中,允許編譯器和處理器對指令進行重排序,重排序過程不會影響到單執行緒程式的執行,卻會影響到多執行緒併發執行的正確性。

volatile 關鍵字通過新增記憶體屏障的方式來禁止指令重排,即重排序時不能把後面的指令放到記憶體屏障之前。

也可以通過 synchronized 來保證有序性,它保證每個時刻只有一個執行緒執行同步程式碼,相當於是讓執行緒順序執行同步程式碼。

4、先行發生原則

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

單一執行緒原則

 

Single Thread rule,在一個執行緒內,在程式前面的操作先行發生於後面的操作。

管程鎖定規則

 

Monitor Lock Rule,一個 unlock 操作先行發生於後面對同一個鎖的 lock 操作。

volatile 變數規則

 

Volatile Variable Rule,對一個 volatile 變數的寫操作先行發生於後面對這個變數的讀操作。

執行緒啟動規則

Thread Start Rule,Thread 物件的 start() 方法呼叫先行發生於此執行緒的每一個動作。

執行緒加入規則

Thread Join Rule,Thread 物件的結束先行發生於 join() 方法返回。

執行緒中斷規則

Thread Interruption Rule,對執行緒 interrupt() 方法的呼叫先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生,可以通過 interrupted() 方法檢測到是否有中斷髮生。

物件終結規則

Finalizer Rule,一個物件的初始化完成(建構函式執行結束)先行發生於它的 finalize() 方法的開始。

傳遞性

Transitivity,如果操作 A 先行發生於操作 B,操作 B 先行發生於操作 C,那麼操作 A 先行發生於操作 C。

十一、執行緒安全

多個執行緒不管以何種方式訪問某個類,並且在主調程式碼中不需要進行同步,都能表現正確的行為。

執行緒安全有以下幾種實現方式:

1、不可變

不可變(Immutable)的物件一定是執行緒安全的,不需要再採取任何的執行緒安全保障措施。只要一個不可變的物件被正確地構建出來,永遠也不會看到它在多個執行緒之中處於不一致的狀態。多執行緒環境下,應當儘量使物件成為不可變,來滿足執行緒安全。

不可變的型別:

  • final 關鍵字修飾的基本資料型別
  • String
  • 列舉型別
  • Number 部分子類,如 Long 和 Double 等數值包裝型別,BigInteger 和 BigDecimal 等大資料型別。但同為 Number 的原子類 AtomicInteger 和 AtomicLong 則是可變的。

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

1 public class ImmutableExample {
2     public static void main(String[] args) {
3         Map<String, Integer> map = new HashMap<>();
4         map.put("a", 1);
5         Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(map);
6        // unmodifiableMap.put("a", 2);  // 對不可變集合修改報錯:java.lang.UnsupportedOperationException
7     }
8 }

2、互斥同步

synchronized 和 ReentrantLock 兩種悲觀併發策略。

java.util.concurrent.ReentrantLock,這個是JDK1.5新增的一種顆粒度更小的鎖,它完全可以替代synchronized關鍵字來實現它的所有功能,而且ReentrantLock鎖的靈活度要遠遠大於synchronized關鍵字。上文第四點有詳細介紹兩種鎖機制。

3、非阻塞同步

互斥同步最主要的問題就是執行緒阻塞和喚醒所帶來的效能問題,因此這種同步也稱為阻塞同步。

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

隨著硬體指令集的發展,我們可以使用基於衝突檢測的樂觀併發策略:先進行操作,如果沒有其它執行緒爭用共享資料,那操作就成功了,否則採取補償措施(不斷地重試,直到成功為止)。這種樂觀的併發策略的許多實現都不需要將執行緒阻塞,因此這種同步操作稱為非阻塞同步。

CAS

樂觀鎖需要操作和衝突檢測這兩個步驟具備原子性,這裡就不能再使用互斥同步來保證了,只能靠硬體來完成。硬體支援的原子性操作最典型的是:比較並交換(Compare-and-Swap,CAS)。CAS 指令需要有 3 個運算元,分別是記憶體地址 V、舊的預期值 A 和新值 B。當執行操作時,只有當 V 的值等於 A,才將 V 的值更新為 B。

AtomicInteger

J.U.C 包裡面的整數原子類 AtomicInteger 的方法呼叫了 Unsafe 類的 CAS 操作。

以下程式碼使用了 AtomicInteger 執行了自增的操作。

1 private AtomicInteger cnt = new AtomicInteger();
2 
3 public void add() {
4     cnt.incrementAndGet();
5 }

以下程式碼是 incrementAndGet() 的原始碼,AtomicInteger.incrementAndGet() 方法具備原子性,它呼叫了 Unsafe 的 getAndAddInt() 。

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

以下程式碼是 getAndAddInt() 原始碼,var1 指示物件記憶體地址,var2 指示該欄位相對物件記憶體地址的偏移,var4 指示操作需要加的數值,這裡為 1。通過 getIntVolatile(var1, var2) 得到舊的預期值,通過呼叫 compareAndSwapInt() 來進行 CAS 比較,如果該欄位記憶體地址中的值等於 var5,那麼就更新記憶體地址為 var1+var2 的變數為 var5+var4。

可以看到 getAndAddInt() 在一個迴圈中進行,發生衝突的做法是不斷的進行重試。

1 public final int getAndAddInt(Object var1, long var2, int var4) {
2     int var5;
3     do {
4         var5 = this.getIntVolatile(var1, var2);
5     } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
6 
7     return var5;
8 }

ABA

如果一個變數初次讀取的時候是 A 值,它的值被改成了 B,後來又被改回為 A,那 CAS 操作就會誤認為它從來沒有被改變過。

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

4、無同步方案

要保證執行緒安全,並不是一定就要進行同步。如果一個方法本來就不涉及共享資料,那它自然就無須任何同步措施去保證正確性。

棧封閉

多個執行緒訪問同一個方法的區域性變數時,不會出現執行緒安全問題,因為區域性變數儲存在虛擬機器棧中,屬於執行緒私有的。

備戰-Java 併發
1 public class StackClosedExample {
2     public void add100() {
3         int cnt = 0;  // 區域性變數
4         for (int i = 0; i < 100; i++) {
5             cnt++;
6         }
7         System.out.println(cnt);
8     }
9 }
View Code
備戰-Java 併發
1 public static void main(String[] args) {
2     StackClosedExample example = new StackClosedExample();
3     ExecutorService executorService = Executors.newCachedThreadPool();
4     executorService.execute(() -> example.add100());
5     executorService.execute(() -> example.add100());
6     executorService.shutdown();
7 }
View Code
1 輸出:
2 100
3 100

執行緒本地儲存(Thread Local Storage)

如果一段程式碼中所需要的資料必須與其他程式碼共享,那就看看這些共享資料的程式碼是否能保證在同一個執行緒中執行。如果能保證,我們就可以把共享資料的可見範圍限制在同一個執行緒之內,這樣,無須同步也能保證執行緒之間不出現資料爭用的問題。

符合這種特點的應用並不少見,大部分使用消費佇列的架構模式(如“生產者-消費者”模式)都會將產品的消費過程儘量在一個執行緒中消費完。其中最重要的一個應用例項就是經典 Web 互動模型中的“一個請求對應一個伺服器執行緒”(Thread-per-Request)的處理方式,這種處理方式的廣泛應用使得很多 Web 服務端應用都可以使用執行緒本地儲存來解決執行緒安全問題。

可以使用 java.lang.ThreadLocal 類來實現執行緒本地儲存功能。

對於以下程式碼,thread1 中設定 threadLocal 為 1,而 thread2 設定 threadLocal 為 2。過了一段時間之後,thread1 讀取 threadLocal 依然是 1,不受 thread2 的影響。

備戰-Java 併發
 1 public class ThreadLocalExample {
 2     public static void main(String[] args) {
 3         ThreadLocal threadLocal = new ThreadLocal();
 4         Thread thread1 = new Thread(() -> {
 5             threadLocal.set(1);
 6             try {
 7                 Thread.sleep(1000);
 8             } catch (InterruptedException e) {
 9                 e.printStackTrace();
10             }
11             System.out.println(threadLocal.get());
12             threadLocal.remove();
13         });
14         Thread thread2 = new Thread(() -> {
15             threadLocal.set(2);
16             threadLocal.remove();
17         });
18         thread1.start();
19         thread2.start();
20     }
21 }
View Code
輸出:1

為了理解 ThreadLocal,先看以下程式碼:

備戰-Java 併發
 1 public class ThreadLocalExample1 {
 2     public static void main(String[] args) {
 3         ThreadLocal threadLocal1 = new ThreadLocal();
 4         ThreadLocal threadLocal2 = new ThreadLocal();
 5         Thread thread1 = new Thread(() -> {
 6             threadLocal1.set(1);
 7             threadLocal2.set(1);
 8         });
 9         Thread thread2 = new Thread(() -> {
10             threadLocal1.set(2);
11             threadLocal2.set(2);
12         });
13         thread1.start();
14         thread2.start();
15     }
16 }
View Code

它所對應的底層結構圖為:

 

 

每個 Thread 都有一個 ThreadLocal.ThreadLocalMap 物件,這個物件可以是單個元素,也可以是List集合。

1 /* ThreadLocal values pertaining to this thread. This map is maintained
2  * by the ThreadLocal class. */
3 ThreadLocal.ThreadLocalMap threadLocals = null;

當呼叫一個 ThreadLocal 的 set(T value) 方法時,先得到當前執行緒的 ThreadLocalMap 物件,然後將 ThreadLocal->value 鍵值對插入到該 Map 中。

備戰-Java 併發
1 public void set(T value) {
2     Thread t = Thread.currentThread();
3     ThreadLocalMap map = getMap(t);
4     if (map != null)
5         map.set(this, value);
6     else
7         createMap(t, value);
8 }
View Code

get() 方法類似。

備戰-Java 併發
 1 public T get() {
 2     Thread t = Thread.currentThread();
 3     ThreadLocalMap map = getMap(t);
 4     if (map != null) {
 5         ThreadLocalMap.Entry e = map.getEntry(this);
 6         if (e != null) {
 7             @SuppressWarnings("unchecked")
 8             T result = (T)e.value;
 9             return result;
10         }
11     }
12     return setInitialValue();
13 }
View Code

ThreadLocal 從理論上講並不是用來解決多執行緒併發問題的,因為根本不存在多執行緒競爭。

在一些場景 (尤其是使用執行緒池) 下,由於 ThreadLocal.ThreadLocalMap 的底層資料結構導致 ThreadLocal 有記憶體洩漏的情況,應該儘可能在每次使用 ThreadLocal 後手動呼叫 remove(),以避免出現 ThreadLocal 經典的記憶體洩漏甚至是造成自身業務混亂的風險。

可重入程式碼(Reentrant Code)

這種程式碼也叫做純程式碼(Pure Code),可以在程式碼執行的任何時刻中斷它,轉而去執行另外一段程式碼(包括遞迴呼叫它本身),而在控制權返回後,原來的程式不會出現任何錯誤。可重入性是函式程式語言的關鍵特性之一。

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

例:可重入程式碼指可被多個函式或程式呼叫的一段程式碼(通常是一個函式),而且它保證在被任何一個函式呼叫時都以同樣的方式執行,如:

1 void test() {
2         int i;
3         i = 2;
4         System.out.println(i);
5         i++;
6         System.out.println(i);
7     }
無論誰呼叫它結果都一樣,得到:
2
3

或者,對不同的呼叫結果不一樣:

1  static int i =2;
2     void test() {
3         System.out.println(i);
4         i++;
5         System.out.println(i);
6     }
備戰-Java 併發
第一次:
2
3
第二次
3
4
第三次
4
5
View Code

十二、鎖優化

這裡的鎖優化主要是指 JVM 對 synchronized 的優化。

1、自旋鎖

互斥同步進入阻塞狀態的開銷都很大,應該儘量避免。在許多應用中,共享資料的鎖定狀態只會持續很短的一段時間。自旋鎖的思想是讓一個執行緒在請求一個共享資料的鎖時執行忙迴圈(自旋)一段時間,如果在這段時間內能獲得鎖,就可以避免進入阻塞狀態。

自旋鎖雖然能避免進入阻塞狀態從而減少開銷,但是它需要進行忙迴圈操作佔用 CPU 時間,它只適用於共享資料的鎖定狀態很短的場景。

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

2、鎖消除

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

鎖消除主要是通過逃逸分析來支援,如果堆上的共享資料不可能逃逸出去被其它執行緒訪問到,那麼就可以把它們當成私有資料對待,也就可以將它們的鎖進行消除。

對於一些看起來沒有加鎖的程式碼,其實隱式的加了很多鎖。例如下面的字串拼接程式碼就隱式加了鎖:

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

String 是一個不可變的類,編譯器會對 String 的拼接自動優化。在 JDK 1.5 之前,會轉化為 StringBuffer 物件的連續 append() 操作:

1 public static String concatString(String s1, String s2, String s3) {
2     StringBuffer sb = new StringBuffer();
3     sb.append(s1);
4     sb.append(s2);
5     sb.append(s3);
6     return sb.toString();
7 }

每個 append() 方法中都有一個同步塊。虛擬機器觀察變數 sb,很快就會發現它的動態作用域被限制在 concatString() 方法內部。也就是說,sb 的所有引用永遠不會逃逸到 concatString() 方法之外,其他執行緒無法訪問到它,因此可以進行消除。

3、鎖粗化

如果一系列的連續操作都對同一個物件反覆加鎖和解鎖,頻繁的加鎖操作就會導致效能損耗。

上面的示例程式碼中連續的 append() 方法就屬於這類情況。如果虛擬機器探測到由這樣的一串零碎的操作都對同一個物件加鎖,將會把加鎖的範圍擴充套件(粗化)到整個操作序列的外部。對於上面append 的示例程式碼就是擴充套件到第一個 append() 操作之前直至最後一個 append() 操作之後,這樣只需要加鎖一次就可以了。

4、輕量級鎖

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

以下是 HotSpot 虛擬機器物件頭的記憶體佈局,這些資料被稱為 Mark Word。其中 tag bits 對應了五個狀態,這些狀態在右側的 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 是否指向當前執行緒的虛擬機器棧,如果是的話說明當前執行緒已經擁有了這個鎖物件,那就可以直接進入同步塊繼續執行,否則說明這個鎖物件已經被其他執行緒執行緒搶佔了。如果有兩條以上的執行緒爭用同一個鎖,那輕量級鎖就不再有效,要膨脹為重量級鎖。 

5、偏向鎖

偏向鎖的思想是偏向於讓第一個獲取鎖物件的執行緒,這個執行緒在之後獲取該鎖就不再需要進行同步操作,甚至連 CAS 操作也不再需要。

當鎖物件第一次被執行緒獲得的時候,進入偏向狀態,標記為 1 01。同時使用 CAS 操作將執行緒 ID 記錄到 Mark Word 中,如果 CAS 操作成功,這個執行緒以後每次進入這個鎖相關的同步塊就不需要再進行任何同步操作。

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

十三、使用多執行緒程式設計的建議

  • 給執行緒起個有意義的名字,這樣可以方便找 Bug。

  • 縮小同步範圍,從而減少鎖爭用。例如對於 synchronized,應該儘量使用同步塊而不是同步方法。

  • 多用同步工具少用 wait() 和 notify()。首先,CountDownLatch, CyclicBarrier, Semaphore 和 Exchanger 這些同步類簡化了編碼操作,而用 wait() 和 notify() 很難實現複雜控制流;其次,這些同步類是由最好的企業編寫和維護,在後續的 JDK 中還會不斷優化和完善。

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

  • 多用併發集合少用同步集合,例如應該使用 ConcurrentHashMap 而不是 Hashtable。

  • 使用本地變數和不可變類來保證執行緒安全。

  • 使用執行緒池而不是直接建立執行緒,這是因為建立執行緒代價很高,執行緒池可以有效地利用有限的執行緒來啟動任務

 

 

 

 

誰念西風獨自涼

蕭蕭黃葉閉疏窗

 

 

 

相關文章