原做地址:https://github.com/frank-lam/2019_campus_applyjavascript
在本文將總結多線程併發編程中的常見面試題,主要核心線程生命週期、線程通訊、併發包部分。主要分紅 「併發編程」 和 「面試指南」 兩 部分,在面試指南中將討論併發相關面經。html
參考資料:java
建立後還沒有啓動。linux
可能正在運行,也可能正在等待 CPU 時間片。git
包含了操做系統線程狀態中的 運行(Running ) 和 就緒(Ready)。程序員
這個狀態下,是在多個線程有同步操做的場景,好比正在等待另外一個線程的 synchronized 塊的執行釋放,或者可重入的 synchronized 塊裏別人調用 wait() 方法,也就是線程在等待進入臨界區。github
阻塞能夠分爲:等待阻塞,同步阻塞,其餘阻塞面試
等待其它線程顯式地喚醒,不然不會被分配 CPU 時間片。算法
進入方法 | 退出方法 |
---|---|
沒有設置 Timeout 參數的 Object.wait() 方法 | Object.notify() / Object.notifyAll() |
沒有設置 Timeout 參數的 Thread.join() 方法 | 被調用的線程執行完畢 |
LockSupport.park() 方法 | - |
無需等待其它線程顯式地喚醒,在必定時間以後會被系統自動喚醒。數據庫
調用 Thread.sleep() 方法使線程進入限期等待狀態時,經常用 「使一個線程睡眠」 進行描述。
調用 Object.wait() 方法使線程進入限期等待或者無限期等待時,經常用 「掛起一個線程」 進行描述。
睡眠和掛起是用來描述行爲,而阻塞和等待用來描述狀態。
阻塞和等待的區別在於,阻塞是被動的,它是在等待獲取一個排它鎖。而等待是主動的,經過調用 Thread.sleep() 和 Object.wait() 等方法進入。
進入方法 | 退出方法 |
---|---|
Thread.sleep() 方法 | 時間結束 |
設置了 Timeout 參數的 Object.wait() 方法 | 時間結束 / Object.notify() / Object.notifyAll() |
設置了 Timeout 參數的 Thread.join() 方法 | 時間結束 / 被調用的線程執行完畢 |
LockSupport.parkNanos() 方法 | - |
LockSupport.parkUntil() 方法 | - |
有三種使用線程的方法:
實現 Runnable 和 Callable 接口的類只能當作一個能夠在線程中運行的任務,不是真正意義上的線程,所以最後還須要經過 Thread 來調用。能夠說任務是經過線程驅動從而執行的。
須要實現 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(); }
與 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()); }
一樣也是須要實現 run() 方法,由於 Thread 類也實現了 Runable 接口。
public class MyThread extends Thread { public void run() { // ... } }
public static void main(String[] args) { MyThread mt = new MyThread(); mt.start(); }
實現接口會更好一些,由於:
Executor 管理多個異步任務的執行,而無需程序員顯式地管理線程的生命週期。這裏的異步是指多個任務的執行互不干擾,不須要進行同步操做。
主要有三種 Executor:
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() 的缺點
採用線程池的優勢
Java 中有兩類線程:User Thread (用戶線程)、Daemon Thread (守護線程)
用戶線程即運行在前臺的線程,而守護線程是運行在後臺的線程。 守護線程做用是爲其餘前臺線程的運行提供便利服務,並且僅在普通、非守護線程仍然運行時才須要,好比垃圾回收線程就是一個守護線程。當 JVM 檢測僅剩一個守護線程,而用戶線程都已經退出運行時,JVM 就會退出,由於沒有若是沒有了被守護這,也就沒有繼續運行程序的必要了。若是有非守護線程仍然存活,JVM 就不會退出。
守護線程並不是只有虛擬機內部提供,用戶在編寫程序時也能夠本身設置守護線程。用戶能夠用 Thread 的 setDaemon(true) 方法設置當前線程爲守護線程。
雖然守護線程可能很是有用,但必須當心確保其餘全部非守護線程消亡時,不會因爲它的終止而產生任何危害。由於你不可能知道在全部的用戶線程退出運行前,守護線程是否已經完成了預期的服務任務。一旦全部的用戶線程退出了,虛擬機也就退出運行了。 所以,不要在守護線程中執行業務邏輯操做(好比對數據的讀寫等)。
另外有幾點須要注意:
守護線程是程序運行時在後臺提供服務的線程,不屬於程序中不可或缺的部分。
當全部非守護線程結束時,程序也就終止,同時會殺死全部守護線程。
main() 屬於非守護線程。
使用 setDaemon() 方法將一個線程設置爲守護線程。
public static void main(String[] args) { Thread thread = new Thread(new MyRunnable()); thread.setDaemon(true); }
Thread.sleep(millisec) 方法會休眠當前正在執行的線程,millisec 單位爲毫秒。
sleep() 可能會拋出 InterruptedException,由於異常不能跨線程傳播回 main() 中,所以必須在本地進行處理。線程中拋出的其它異常也一樣須要在本地進行處理。
public void run() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } }
對靜態方法 Thread.yield() 的調用聲明瞭當前線程已經完成了生命週期中最重要的部分,能夠切換給其它線程來執行。該方法只是對線程調度器的一個建議,並且也只是建議具備相同優先級的其它線程能夠運行。
public void run() { Thread.yield(); }
線程能夠阻塞於四種狀態:
注意,並不是全部的阻塞狀態都是可中斷的,以上阻塞狀態的前兩種能夠被中斷,後兩種不會對中斷作出反應
一個線程執行完畢以後會自動結束,若是在運行過程當中發生異常也會提早結束。
經過調用一個線程的 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)
若是一個線程的 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 的 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);
Java 提供了兩種鎖機制來控制多個線程對共享資源的互斥訪問,第一個是 JVM 實現的 synchronized,而另外一個是 JDK 實現的 ReentrantLock。
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. 同步一個靜態方法
public synchronized static void fun() { // ... }
做用於整個類。
重入鎖(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 對象。
1. 鎖的實現
synchronized 是 JVM 實現的,而 ReentrantLock 是 JDK 實現的。
2. 性能
新版本 Java 對 synchronized 進行了不少優化,例如自旋鎖等。目前來看它和 ReentrantLock 的性能基本持平了,所以性能因素再也不是選擇 ReentrantLock 的理由。synchronized 有更大的性能優化空間,應該優先考慮 synchronized。
3. 功能
ReentrantLock 多了一些高級功能。
4. 使用選擇
除非須要使用 ReentrantLock 的高級功能,不然優先使用 synchronized。這是由於 synchronized 是 JVM 實現的一種鎖機制,JVM 原生地支持它,而 ReentrantLock 不是全部的 JDK 版本都支持。而且使用 synchronized 不用擔憂沒有釋放鎖而致使死鎖問題,由於 JVM 會確保鎖的釋放。
蘑菇街面試,這裏簡單論述一下
在 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。
CAS 指的是現代 CPU 普遍支持的一種對內存中的共享數據進行操做的一種特殊指令。這個指令會對內存中的共享數據作原子的讀寫操做。
簡單介紹一下這個指令的操做過程:
這一系列的操做是原子的。它們雖然看似複雜,但倒是 Java 5 併發機制優於原有鎖機制的根本。簡單來講,CAS 的含義是:我認爲原有的值應該是什麼,若是是,則將原有的值更新爲新值,不然不作修改,並告訴我原來的值是多少。
簡單的來講,CAS 有 3 個操做數,內存值 V,舊的預期值 A,要修改的新值 B。當且僅當預期值 A 和內存值 V 相同時,將內存值 V 修改成 B,不然返回 V。這是一種樂觀鎖的思路,它相信在它修改以前,沒有其它線程去修改它;而 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方法佔用的是對象鎖,不存在互斥現象) */ } }
當多個線程能夠一塊兒工做去解決某個問題時,若是某些部分必須在其它部分以前完成,那麼就須要對線程進行協調。
在線程中調用另外一個線程的 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 線程,爭奪時間片的線程只有一個)來喚醒掛起的線程。
它們都屬於 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
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
AQS 是 AbstractQueuedSynchronizer 的簡稱,java.util.concurrent(J.U.C)大大提升了併發性能,AQS (AbstractQueuedSynchronizer) 被認爲是 J.U.C 的核心。它提供了一個基於 FIFO 隊列,這個隊列能夠用來構建鎖或者其餘相關的同步裝置的基礎框架。下圖是 AQS 底層的數據結構:
它底層使用的是雙向列表,是隊列的一種實現 , 所以也能夠將它當成一種隊列。
簡單的來講:
AQS其實就是一個能夠給咱們實現鎖的框架
內部實現的關鍵是:先進先出的隊列、state 狀態
定義了內部類 ConditionObject
擁有兩種線程模式
通常咱們叫 AQS 爲同步器。
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個子線程已經執行完畢 繼續執行主線程
用來控制多個線程互相等待,只有當多個線程都到達時,這些線程纔會繼續執行。
和 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 能夠控同時訪問的線程個數,經過 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釋放出機器
下面對上面說的三個輔助類進行一個總結:
在介紹 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
java.util.concurrent.BlockingQueue 接口有如下阻塞隊列的實現:
提供了阻塞的 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..
主要用於並行計算中,和 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 來執行,這樣就避免發生競爭。可是若是隊列中只有一個任務時仍是會發生競爭。
若是多個線程對同一個共享數據進行訪問而不採起同步操做的話,那麼操做的結果是不一致的。
如下代碼演示了 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
Java 內存模型試圖屏蔽各類硬件和操做系統的內存訪問差別,以實現讓 Java 程序在各類平臺下都能達到一致的內存訪問效果。
處理器上的寄存器的讀寫的速度比內存快幾個數量級,爲了解決這種速度矛盾,在它們之間加入了高速緩存。
加入高速緩存帶來了一個新的問題:緩存一致性。若是多個緩存共享同一塊主內存區域,那麼多個緩存的數據可能會不一致,須要一些協議來解決這個問題。
全部的變量都存儲在主內存中,每一個線程還有本身的工做內存,工做內存存儲在高速緩存或者寄存器中,保存了該線程使用的變量的主內存副本拷貝。
線程只能直接操做工做內存中的變量,不一樣線程之間的變量值傳遞須要經過主內存來完成。
Java內存模型和硬件關係圖
Java內存模型抽象結構圖
Java 內存模型定義了 8 個操做來完成主內存和工做內存的交互操做。
若是要把一個變量從主內存中複製到工做內存,就須要按順尋地執行 read 和 load 操做,若是把變量從工做內存中同步回主內存中,就要按順序地執行 store 和 write 操做。Java內存模型只要求上述操做必須按順序執行,而沒有保證必須是連續執行。也就是 read 和 load 之間,store 和 write 之間是能夠插入其餘指令的,如對主內存中的變量a、b進行訪問時,可能的順序是read a,read b,load b, load a。
Java內存模型還規定了在執行上述8種基本操做時,必須知足以下規則:
參考資料:
volatile關鍵字與Java內存模型(JMM) - yzwall - 博客園
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
可見性指當一個線程修改了共享變量的值,其它線程可以當即得知這個修改。Java 內存模型是經過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值來實現可見性的。
主要有有三種實現可見性的方式:
對前面的線程不安全示例中的 cnt 變量使用 volatile 修飾,不能解決線程不安全問題,由於 volatile 並不能保證操做的原子性。
有序性是指:在本線程內觀察,全部操做都是有序的。在一個線程觀察另外一個線程,全部操做都是無序的,無序是由於發生了指令重排序。
在 Java 內存模型中,容許編譯器和處理器對指令進行重排序,重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。
volatile 關鍵字經過添加內存屏障的方式來禁止指令重排,即重排序時不能把後面的指令放到內存屏障以前。
也能夠經過 synchronized 來保證有序性,它保證每一個時刻只有一個線程執行同步代碼,至關因而讓線程順序執行同步代碼。
在執行程序時爲了提升性能,編譯器和處理器經常會對指令作重排序。
指令重排序包括:編譯器重排序和處理器重排序
重排序分三種類型:
從 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 語義的意思指:無論怎麼重排序(編譯器和處理器爲了提升並行度),(單線程)程序的執行結果不能被改變。編譯器,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 關係:
這裏的第 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 是用來指定兩個操做之間的執行順序。提供跨線程的內存可見性。
在 Java 內存模型中,若是一個操做執行的結果須要對另外一個操做可見,那麼這兩個操做之間必然存在 happens-before 關係。
上面提到了能夠用 volatile 和 synchronized 來保證有序性。除此以外,JVM 還規定了先行發生原則,讓一個操做無需控制就能先於另外一個操做完成。
主要有如下這些原則:
Single Thread rule
在一個線程內,在程序前面的操做先行發生於後面的操做。
Monitor Lock Rule
對一個鎖的解鎖(unlock ),老是 happens-before 於隨後對這個鎖的加鎖(lock)
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。
一個類在能夠被多個線程安全調用時就是線程安全的。
線程安全不是一個非真即假的命題,能夠將共享數據按照安全程度的強弱順序分紅如下五類:不可變、絕對線程安全、相對線程安全、線程兼容和線程對立。
不可變(Immutable)的對象必定是線程安全的,不管是對象的方法實現仍是方法的調用者,都不須要再採起任何的線程安全保障措施,只要一個不可變的對象被正確地構建出來,那其外部的可見狀態永遠也不會改變,永遠也不會看到它在多個線程之中處於不一致的狀態。
不可變的類型:
對於集合類型,能夠使用 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(); }
多線程環境下,應當儘可能使對象成爲不可變,來知足線程安全。
無論運行時環境如何,調用者都不須要任何額外的同步措施。
相對的線程安全須要保證對這個對象單獨的操做是線程安全的,在調用的時候不須要作額外的保障措施,可是對於一些特定順序的連續調用,就可能須要在調用端使用額外的同步手段來保證調用的正確性。
在 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); } } });
線程兼容是指對象自己並非線程安全的,可是能夠經過在調用端正確地使用同步手段來保證對象在併發環境中能夠安全地使用,咱們日常說一個類不是線程安全的,絕大多數時候指的是這一種狀況。Java API 中大部分的類都是屬於線程兼容的,如與前面的 Vector 和 HashTable 相對應的集合類 ArrayList 和 HashMap 等。
線程對立是指不管調用端是否採起了同步措施,都沒法在多線程環境中併發使用的代碼。因爲 Java 語言天生就具有多線程特性,線程對立這種排斥多線程的代碼是不多出現的,並且一般都是有害的,應當儘可能避免。
synchronized 和 ReentrantLock。
互斥同步最主要的問題就是進行線程阻塞和喚醒所帶來的性能問題,所以這種同步也稱爲阻塞同步。
互斥同步屬於一種悲觀的併發策略,老是認爲只要不去作正確的同步措施,那就確定會出現問題。不管共享數據是否真的會出現競爭,它都要進行加鎖(這裏討論的是概念模型,實際上虛擬機會優化掉很大一部分沒必要要的加鎖)、用戶態核心態轉換、維護鎖計數器和檢查是否有被阻塞的線程須要喚醒等操做。
隨着硬件指令集的發展,咱們能夠使用基於衝突檢測的樂觀併發策略:先進行操做,若是沒有其它線程爭用共享數據,那操做就成功了,不然採起補償措施(不斷地重試,直到成功爲止)。這種樂觀的併發策略的許多實現都不須要把線程掛起,所以這種同步操做稱爲非阻塞同步。
樂觀鎖須要操做和衝突檢測這兩個步驟具有原子性,這裏就不能再使用互斥同步來保證了,只能靠硬件來完成。
硬件支持的原子性操做最典型的是:比較並交換(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 問題,改用傳統的互斥同步可能會比原子類更高效。
要保證線程安全,並非必定就要進行同步,二者沒有因果關係。同步只是保證共享數據爭用時的正確性的手段,若是一個方法原本就不涉及共享數據,那它天然就無須任何同步措施去保證正確性,所以會有一些代碼天生就是線程安全的。
這種代碼也叫作純代碼(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
若是一段代碼中所須要的數據必須與其餘代碼共享,那就看看這些共享數據的代碼是否能保證在同一個線程中執行。若是能保證,咱們就能夠把共享數據的可見範圍限制在同一個線程以內,這樣,無須同步也能保證線程之間不出現數據爭用的問題。
符合這種特色的應用並很多見,大部分使用消費隊列的架構模式(如「生產者-消費者」模式)都會將產品的消費過程儘可能在一個線程中消費完,其中最重要的一個應用實例就是經典 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 經典的內存泄漏甚至是形成自身業務混亂的風險。
參考資料:
這裏的鎖優化主要是指虛擬機對 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)後恢復到未鎖定狀態或者輕量級鎖狀態。
蘑菇街面試,設計一個線程池
入隊
非阻塞隊列:當隊列中滿了時候,放入數據,數據丟失
阻塞隊列:當隊列滿了的時候,進行等待,何時隊列中有出隊的數據,那麼第11個再放進去
出隊
非阻塞隊列:若是如今隊列中沒有元素,取元素,獲得的是null
阻塞隊列:等待,何時放進去,再取出來
線程池使用的是阻塞隊列
線程是稀缺資源,若是被無限制的建立,不只會消耗系統資源,還會下降系統的穩定性,合理的使用線程池對線程進行統一分配、調優和監控,有如下好處:
Java1.5 中引入的 Executor 框架把任務的提交和執行進行解耦,只須要定義好任務,而後提交給線程池,而不用關心該任務是如何執行、被哪一個線程執行,以及何時執行。
線程池中的核心線程數,當提交一個任務時,線程池建立一個新線程執行任務,直到當前線程數等於corePoolSize;若是當前線程數爲 corePoolSize,繼續提交的任務被保存到阻塞隊列中,等待被執行;若是阻塞隊列滿了,那就建立新的線程執行當前任務;直到線程池中的線程數達到 maxPoolSize,這時再有任務來,只能執行 reject() 處理該任務。
// 使用Executors靜態方法進行初始化 ExecutorService service = Executors.newSingleThreadExecutor(); // 經常使用方法 service.execute(new Thread()); service.submit(new Thread()); service.shutDown(); service.shutDownNow();
用到返回值的例子,好比說我有不少個作 validation 的 task,我但願全部的 task 執行完,而後每一個 task 告訴我它的執行結果,是成功仍是失敗,若是是失敗,緣由是什麼。而後我就能夠把全部失敗的緣由綜合起來發給調用者。
若是你在你的 task 裏會拋出 checked 或者 unchecked exception,而你又但願外面的調用者可以感知這些 exception 並作出及時的處理,那麼就須要用到 submit,經過捕獲 Future.get 拋出的異常。
當線程池調用該方法時,線程池的狀態則馬上變成 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 參數,有四種隊列可供選擇:
關於 handler 參數,線程池的飽和策略,當阻塞隊列滿了,且沒有空閒的工做線程,若是繼續提交任務,必須採起一種策略處理該任務,線程池提供了 4 種策略:
固然也能夠根據應用場景實現 RejectedExecutionHandler 接口,自定義飽和策略,如記錄日誌或持久化存儲不能處理的任務。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
其中 AtomicInteger 變量 ctl 的功能很是強大:利用低 29 位表示線程池中線程數,經過高 3 位表示線程池的運行狀態:
若是執行了線程池的 prestartAllCoreThreads() 方法,線程池會提早建立並啓動全部核心線程。
ThreadPoolExecutor 提供了動態調整線程池容量大小的方法:setCorePoolSize() 和 setMaximumPoolSize()。
通常須要根據任務的類型來配置線程池大小:
若是是 CPU 密集型任務,就須要儘可能壓榨 CPU,參考值能夠設爲 NCPU+1
若是是 IO 密集型任務,參考值能夠設置爲 2*NCPU
在這裏將總結面試中和併發編程相關的常見知識點,如在第一部分中出現的這裏將不進行詳細闡述。面試指南中,我將用最簡潔的語言描述,更可能是以一種大綱的形式列出問答點,根據本身掌握的狀況回答。
參考資料:
(1)僅靠volatile不能保證線程的安全性。(原子性)
synchronized 不只保證可見性,並且還保證原子性,由於,只有得到了鎖的線程才能進入臨界區,從而保證臨界區中的全部語句都所有執行。多個線程爭搶 synchronized 鎖對象時,會出現阻塞。
(2)線程安全性
線程安全性包括兩個方面,①可見性。②原子性。
從上面自增的例子中能夠看出:僅僅使用 volatile 並不能保證線程安全性。而 synchronized 則可實現線程的安全性。
什麼是線程池
設計一個動態大小的線程池,如何設計,應該有哪些方法
線程池四種建立方式
Java 經過 Executors 提供四種線程池,分別爲:
並行就是兩個任務同時運行,就是甲任務進行的同時,乙任務也在進行。(須要多核CPU)
好比我跟兩個網友聊天,左手操做一個電腦跟甲聊,同時右手用另外一臺電腦跟乙聊天,這就叫並行。
服務能同時處理不少請求,提升程序性能
參考資料:
當多個線程訪問同一個對象時,若是不用考慮這些線程在運行時環境下的調度和交替運行,也不須要進行額外的同步,或者在調用方進行任何其餘的協調操做,調用這個對象的行爲均可以獲取正確的結果,那這個對象是線程安全的。——來自《深刻理解Java虛擬機》
定義
如何保證線程安全?(更加詳細的請轉向第一部分 11. 線程安全
)
注意
有人在使用過程當中有一個不正確的觀點:個人程序是多線程的,不能使用 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 關鍵字後面括號內的對象。
參考資料:
volatile 關鍵字的做用
內存可見性
當且僅當知足如下全部條件時,才應該使用 volatile 變量
volatile 使用建議
volatile 和 synchronized區別
volatile 變量是一種稍弱的同步機制在訪問 volatile 變量時不會執行加鎖操做,所以也就不會使執行線程阻塞,所以 volatile 變量是一種比 synchronized 關鍵字更輕量級的同步機制。
從內存可見性的角度看,寫入 volatile 變量至關於退出同步代碼塊,而讀取 volatile 變量至關於進入同步代碼塊。
在代碼中若是過分依賴 volatile 變量來控制狀態的可見性,一般會比使用鎖的代碼更脆弱,也更難以理解。僅當 volatile 變量能簡化代碼的實現以及對同步策略的驗證時,才應該使用它。通常來講,用同步機制會更安全些。
加鎖機制(即同步機制)既能夠確保可見性又能夠確保原子性,而 volatile 變量只能確保可見性,緣由是聲明爲volatile的簡單變量若是當前值與該變量之前的值相關,那麼 volatile 關鍵字不起做用,也就是說以下的表達式都不是原子操做:「count++」、「count = count+1」。
(1)線程和進程
(2)使用線程的緣由
不論哪一種語言的內存分配方式,都須要返回所分配內存的真實地址,也就是返回一個指針到內存塊的首地址。Java中對象是採用 new、反射、clone、反序列化等方法建立的, 這些對象的建立都是在堆(Heap)中分配的,全部對象的回收都是由Java虛擬機經過垃圾回收機制完成的。GC 爲了可以正確釋放對象,會監控每一個對象的運行情況,對他們的申請、引用、被引用、賦值等情況進行監控,Java 會使用有向圖的方法進行管理內存,實時監控對象是否能夠達到,若是不可到達,則就將其回收,這樣也能夠消除引用循環的問題。
在 Java 語言中,判斷一個內存空間是否符合垃圾收集標準有兩個:一個是給對象賦予了空值 null,如下再沒有調用過,另外一個是給對象賦予了新值,這樣從新分配了內存空間。
首先,什麼是內存泄露?常常聽人談起內存泄露,但要問什麼是內存泄露,沒幾個說得清楚。
內存泄露:是指無用對象(再也不使用的對象)持續佔有內存或無用對象的內存得不到及時釋放,從而形成的內存空間的浪費稱爲內存泄露。內存泄露有時不嚴重且不易察覺,這樣開發者就不知道存在內存泄露,但有時也會很嚴重,會提示 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 的整個生命週期內存在,若是它持有一個外部對象的(生命週期比較短)引用,那麼這個外部對象就不能被回收,從而致使內存泄漏。若是這個外部對象還持有其餘對象的引用,那麼內存泄漏更嚴重。
使用多線程時,不是多線程能提高程序的執行速度,使用多線程是爲了更好地利用 CPU 資源!
程序在執行時,多線程是 CPU 經過給每一個線程分配 CPU 時間片來實現的,時間片是CPU分配給每一個線程執行的時間,因時間片很是短,因此CPU 經過不停地切換線程執行。
線程不是越多就越好的,由於線程上下文切換是有性能損耗的,在使用多線程的同時須要考慮如何減小上下文切換
通常來講有如下幾條經驗
還能夠考慮咱們的應用是IO密集型的仍是CPU密集型的。
synchronized 同步
while 輪詢的方式
(list.size()==5)
是否成立 ,從而實現了線程間的通訊。可是這種方式會浪費 CPU 資源。之因此說它浪費資源,是由於 JVM 調度器將 CPU 交給線程B執行時,它沒作啥「有用」的工做,只是在不斷地測試某個條件是否成立。就相似於現實生活中,某我的一直看着手機屏幕是否有電話來了,而不是: 在幹別的事情,當有電話來時,響鈴通知TA電話來了。wait/notify 機制
當條件未知足時,線程A調用 wait() 放棄CPU,並進入阻塞狀態。(不像 while 輪詢那樣佔用 CPU)
當條件知足時,線程B調用 notify() 通知線程A,所謂通知線程A,就是喚醒線程A,並讓它進入可運行狀態。
管道通訊
參考資料:
同步和異步關注的是消息通訊機制 (synchronous communication/ asynchronous communication)
阻塞和非阻塞關注的是程序在等待調用結果(消息,返回值)時的狀態.
參考資料:
本小結參考: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 同步塊實現的,所以它們也不保證公平性。飢餓和公平中有更多關於該內容的討論。
若是用 Lock 來保護臨界區,而且臨界區有可能會拋出異常,那麼在 finally 語句中調用 unlock()就顯得很是重要了。這樣能夠保證這個鎖對象能夠被解鎖以便其它線程能繼續對其加鎖。如下是一個示例:
lock.lock(); try{ //do critical section code, //which may throw exception } finally { lock.unlock(); }
這個簡單的結構能夠保證當臨界區拋出異常時 Lock 對象能夠被解鎖。若是不是在 finally 語句中調用的 unlock(),當臨界區拋出異常時,Lock 對象將永遠停留在被鎖住的狀態,這會致使其它全部在該 Lock 對象上調用 lock()的線程一直阻塞。
從 volatile 說到,i++原子操做,線程安全問題 - CSDN博客
https://blog.csdn.net/zbw18297786698/article/details/53420780