上篇文章中介紹了Java線程的帶來的問題與內存模型中介紹了線程可能會引起的問題以及對應Java的內存模型,順帶介紹了Volatile和Sychronized關鍵字。今天對Java中涉及到的常見的關鍵類和關鍵字進行一個總結。html
與鎖相比,Volatile提供了一種更加輕量級的同步機制,使用Volatile的變量在多線程中是不會發生上下文切換或者線程調度等操做的。當一個變量定義成爲一個Volatile的時候,這個變量具有了兩種特性:java
Volatile變量不會緩存在工做內存(對應物理寄存器)當中,在線程A中修改了一個共享變量的值,修改後當即從A的工做內存中同步給了主內存更新值,同時其餘線程每次使用該共享變量值時,保證從主內存中獲取。不過Volatile也有必定的侷限性,雖然提供了類似的可見性保證,但不能用於構建原子的複合操做,所以當一個變量依賴其餘變量,或者當前變量依賴與舊值時候,就不能使用Volatile變量,由於Volatile不保證代碼的原子性。最多見的就是自增操做的問題。編程
因爲Java中的運算並不是原子操做,因此在多線程的狀況下進行運算同樣是不安全的。示例Demo以下:緩存
class ThreadTest { private volatile int count = 0; public void update() { for (int i = 0; i < 50; i++) { Thread thread = new Thread(() -> { for (int k = 0; k < 100; k++) { count++; } }); thread.start(); } try { Thread.sleep(5000); System.out.println(count); } catch (InterruptedException e) { e.printStackTrace(); } } }
上面代碼獲取到的值基本上都是小於5000的,由於count++在執行過程當中分三步進行,首先從主存中複製count到工做內存中,工做內存中將count+1,而後在再刷新回主存。因此存在的問題是當一個進行前兩步的時候,其餘的線程已經刷新最新值回主存了,那麼當前線程再刷新回主存的時候形成了值變小的問題。安全
Volatile最多見的場景就是在線程中充當flag變量的標誌,如提供一個方法進行終止線程:多線程
class ThreadTest extends Thread { private volatile boolean isCancle; public void setCancle(boolean isCancle) { this.isCancle = isCancle; } @Override public void run() { super.run(); while (!isCancle) { } System.out.println("over"); } }
當調用setCancle(...)的時候可以立馬結束while循環,從而打印出over。併發
第二個,使用Volatile可以禁止指令重排序的優化。在Java線程的帶來的問題與內存模型(JMM)中咱們解釋了指令重排序的概念,那麼在Java中能夠經過Volatile關鍵字添加內存屏障,從而實現禁止指令重排序的優化,關於Volatile禁止指令重排序的一個在經典的案例就是DCL中的使用:app
public class DoubleCheckedLocking { private static Instance instance; public static Instance getInstance() { if (instance == null) { synchronized (DoubleCheckedLocking.class) { if (instance == null) instance = new Instance(); } } return instance; } }
在DCL沒添加Volatile的版本中,在new Instance()
該句中會出現問題,因爲new Instance()
不是一個原子操做,其操做分爲以下過程:ide
因爲重排序的存在,編譯器能夠將2,3順序進行重排序優化:性能
當線程A再進行new Instance()
時候,此時正好執行到第2個步驟,這時候線程B進行判斷instance是否爲null,發現instance引用不爲空,那麼就直接返回了,然而線程A還沒初始化Instance對象,這就形成了線程B引用了一個未初始化的引用,那麼天然會有問題。解決方案就是爲instance變量添加volatile關鍵字,保證禁止指令的重排序,程序就正確了。
關於DCL更詳細的內容能夠閱讀如下這篇文章。
最後總結一下Volatile使用的場景:
Java中最多見到的同步機制就是Synchronized關鍵字了,通常狀況下,若是對性能的要求不是那麼的苛刻,經過Sychronized關鍵字基本上可以解決全部的線程同步問題。通常使用Synchronized方式有以下幾種:
在靜態方法中添加Synchronized的方式和對Class添加Synchronized的本質上是同樣的,都是是持有對應的class的鎖,示例以下:
public class Test{ private static int num=2; public static void main(String[] args){ } public static synchronized void increaseNum(){ num++; System.out.println("調用increaseNum,當前值爲:"+num); } public void increseNum2(){ synchronized(Test.class){ num++; System.out.println("調用increseNum2,當前值爲:"+num); } } }
在實例方法中添加Synchronized本質上是持有了當前對象實例的鎖,示例代碼以下:
public synchronized void increseNum3(){ num++; System.out.println("調用increseNum3,當前值爲:"+num); }
對某個對象添加Synchronized本質上是對持有了當前對象的鎖,示例代碼以下:
public void increseNum4(){ synchronized (object) { num++; System.out.println("調用increseNum4,當前值爲:"+num); } }
上面代碼中持有了object對象的鎖。
Synchronized稱之爲互斥鎖,使用Synchronized可以保證代碼段的可見性和原子性,多線程操做中在某一個線程A得到互斥鎖的時候,其餘線程只能等待而阻塞等待A的執行完畢後再競爭鎖資源。除此以外,使用Synchronized時候具有了可重入性,即一個線程獲取了互斥鎖以後,該線程其餘的聲明瞭Synchronized的,若是被調用了,而且是同一個鎖的代碼段,則是不須要阻塞,可以一併執行的。示例代碼以下:
public void increseNum4(){ synchronized (object) { num++; increseNum5(); System.out.println("調用increseNum4,當前值爲:"+num); } } public void increseNum5(){ synchronized (object) { num++; System.out.println("調用increseNum5,當前值爲:"+num); } }
能夠看到,在increseNum4()
方法中咱們是有了object對象的鎖,其內部中調用了increseNum5()
方法,因爲increseNum5()
中持有相同的object對象鎖,因此方法能夠等同理解爲:
public void increseNum4(){ synchronized (object) { num++; increseNum5(); System.out.println("調用increseNum4,當前值爲:"+num); } } public void increseNum5(){ num++; System.out.println("調用increseNum5,當前值爲:"+num); }
若是咱們修改increseNum5()
中的Synchronized的修飾,改爲以下:
public void increseNum4(){ synchronized (object) { num++; increseNum5(); System.out.println("調用increseNum4,當前值爲:"+num); } } public synchronized void increseNum5(){ num++; System.out.println("調用increseNum5,當前值爲:"+num); }
那麼因爲上述兩個方法持有不一樣的鎖,若是increseNum5()
不被其餘線程使用鎖定,那麼可以正常執行;反之,increseNum4()
方法必須等到increseNum5()
的線程執行完畢後釋放對應的鎖後纔可以繼續執行代碼段。
上篇文章Java併發編程學習二中講述了底層中JVM針對工做內存與主存的8種交互操做時講述了一個規則:**一個變量在同一時刻只容許一條線程進行lock操做,但lock操做能夠被同一線程重複執行屢次,屢次執行lock後,只有執行相同次數的unlock操做,變量纔會被解鎖。**lock跟unlock操做咱們沒法直接操做,取而代之的是關鍵字monitorenter和monitorexit,這個也在上篇文章中舉例說過了,這裏也不過多敘述。
Java中的同步實現跟操做系統中的管程(監視器,monitor)有關,管程是操做系統實現同步的重要基礎概念。關於對應的介紹能夠看下這個維基百科的[介紹](https://zh.wikipedia.org/wiki/%E7%9B%A3%E8%A6%96%E5%99%A8_(%E7%A8%8B%E5%BA%8F%E5%90%8C%E6%AD%A5%E5%8C%96)。關於更加深刻的知識點,能夠仔細閱讀這篇文章,這裏對底層Synchronized實現作個總結:
在Java5.0以前只有Synchronized和Volatile使用,在5.0以後增長了Lock接口,可以實現Synchronized的全部工做,而且除此以外擁有Synchronized不具備的以下特性:
總而言之,Lock接口比Synchronized更加靈活的控制空間,當Synchronized不能知足咱們的需求的時候,能夠嘗試的考慮使用該接口的實現類,最多見的實現類就是ReentrantLock了,下面就以ReentrantLock做爲Demo例子學習。這裏首先先介紹一下Lock接口:
public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }
lock()方法的使用跟Synchronized關鍵字一致,若是當前monitor沒有被佔用,則得到monitor,其餘線程會一直阻塞,直到調用lock()的線程調用unlock()方法,,示例代碼以下:
class ThreadTest { private static int num = 1; private Lock mLock = new ReentrantLock(); public void increaseNum() { try { mLock.lock(); num++; System.out.println(timeStamp2Date() + " 調用increaseNum,當前值爲:" + num); Thread.sleep(4000); } catch (Exception e) { } finally { mLock.unlock(); } } public static String timeStamp2Date() { String format = "yyyy-MM-dd HH:mm:ss"; SimpleDateFormat sdf = new SimpleDateFormat(format); return sdf.format(new Date(System.currentTimeMillis())); } public void increaseNum2() { try { mLock.lock(); num++; System.out.println(timeStamp2Date() + " 調用increaseNum2,當前值爲:" + num); } catch (Exception e) { } finally { mLock.unlock(); } } } //------------------------------------------------------- fun main(args: Array<String>) { val threadTest = ThreadTest() val thread1 = Thread(Runnable { threadTest.increaseNum() }) val thread2 = Thread(Runnable { threadTest.increaseNum2() }) thread1.start() thread2.start() } //------------------------------------------------ //輸出結果 2018-11-19 16:06:10 調用increaseNum,當前值爲:2 2018-11-19 16:06:14 調用increseNum2,當前值爲:3
increaseNum()
中模擬了4秒的耗時操做,能夠看到在結果中increaseNum2()
確實等待了4秒左右的時間才進行了調用,調用的方式跟Synchronized一模一樣,只不過增長了手動釋放的代碼。
接下來看看tryLock方法:
接下來仍是代碼測試,首先測試一下傳遞無參的:
public void increaseNum() { if (mLock.tryLock()) { try { num++; System.out.println(timeStamp2Date() + " 調用increaseNum,當前值爲:" + num); Thread.sleep(4000); } catch (Exception e) { } finally { mLock.unlock(); } } else { System.out.println(timeStamp2Date() + " increaseNum 獲取鎖失敗"); } } public static String timeStamp2Date() { String format = "yyyy-MM-dd HH:mm:ss"; SimpleDateFormat sdf = new SimpleDateFormat(format); return sdf.format(new Date(System.currentTimeMillis())); } public void increaseNum2() { if (mLock.tryLock()) { try { num++; System.out.println(timeStamp2Date() + " 調用increaseNum2,當前值爲:" + num); } catch (Exception e) { } finally { mLock.unlock(); } } else { System.out.println(timeStamp2Date() + " increaseNum2 獲取鎖失敗"); } } ------------------------------------------------ fun main(args: Array<String>) { val threadTest = ThreadTest() val thread1 = Thread(Runnable { threadTest.increaseNum() }) var thread2 = Thread(Runnable { threadTest.increaseNum2() }) thread2.start() thread1.start() Thread.sleep(5000) thread2 = Thread(Runnable { threadTest.increaseNum2() }) thread2.start() } //輸出結果 2018-11-19 16:37:09 increaseNum 獲取鎖失敗 2018-11-19 16:37:09 調用increaseNum2,當前值爲:2 2018-11-19 16:37:14 調用increaseNum2,當前值爲:3
接着測試有形參的:
public void increaseNum2(int time) { try { if (mLock.tryLock(time, TimeUnit.SECONDS)) { try { num++; System.out.println(timeStamp2Date() + " 調用increaseNum2,當前值爲:" + num); } catch (Exception e) { } finally { mLock.unlock(); } } else { System.out.println(timeStamp2Date() + " increaseNum2 獲取鎖失敗"); } } catch (InterruptedException e) { e.printStackTrace(); } } ------------------------------------------------------ fun main(args: Array<String>) { val threadTest = ThreadTest() val thread1 = Thread(Runnable { threadTest.increaseNum() }) var thread2 = Thread(Runnable { threadTest.increaseNum2(2) threadTest.increaseNum2(4) }) thread1.start() thread2.start() } //輸出結果 2018-11-19 16:43:46 調用increaseNum,當前值爲:2 2018-11-19 16:43:48 increaseNum2 獲取鎖失敗 2018-11-19 16:43:50 調用increaseNum2,當前值爲:3
第一次調用increaseNum2()
的時候因爲在2秒的時間內increaseNum()
尚未釋放掉鎖,因此獲取鎖失敗;接着第二次調用increaseNum2()
的時候,鎖已經釋放了,因此正常獲取到。
除此以外,經過調用 tryLock(long time, TimeUnit unit)
方法,可以拋出InterruptedException異常,因此可以正常響應中斷操做,即thread.interrupt()
,這是Synchronized沒法作到的。
與上面方法相同的是lockInterruptibly()
也可以正常響應中斷操做,方法的描述以下(摘抄來自該篇文章):
關於這個方法的用法和理解就比較複雜了,lockInterruptibly()
自己拋出InterruptedException異常,能夠類比Thread.sleep()方法,這樣就比較好理解了。下面簡單給個Demo測試一下:
public void increaseNum3() { boolean flag = false; try { mLock.lockInterruptibly(); flag = true; } catch (InterruptedException e) { System.out.println("中斷髮生"); } finally { if (flag) { mLock.unlock(); } } } public void increaseNum() { if (mLock.tryLock()) { try { num++; System.out.println(timeStamp2Date() + " 調用increaseNum,當前值爲:" + num); Thread.sleep(4000); } catch (Exception e) { } finally { mLock.unlock(); } } else { System.out.println(timeStamp2Date() + " increaseNum 獲取鎖失敗"); } } ---------------------------------------------------------------- fun main(args: Array<String>) { val threadTest = ThreadTest() val thread2 = Thread(Runnable { threadTest.increaseNum() }) thread2.start() val thread1 = Thread(Runnable { threadTest.increaseNum3() }) thread1.start() Thread.sleep(2000) thread1.interrupt() } //結果 2018-11-19 17:25:39 調用increaseNum,當前值爲:2 中斷髮生
上述代碼thread2在increaseNum()
方法中獲取到了mLock的鎖,因此在thread1調用increaseNum3()
時候阻塞了,過了兩秒後因爲在主線程調用了thread1.interrupt()
,因此increaseNum3()
中拋出了異常,打印出了中斷髮生的log。這裏只是簡單驗證了一下一種狀況,更多種能夠自主測試一下。
最後一個就是wait/notify機制了,wai()方法介紹以下:
wait方法是一個本地方法,其底層也是經過monitor對象來完成的,因此咱們使用wait/notify機制時候必須跟Synchronized一塊兒使用。除了這個,在線程的概念以及使用文章中還說過:
這裏須要區分sleep和wait的區別,wait和notify方法跟sychronized關鍵字一塊兒配套使用,wait()方法在進入等待狀態的時候,這個時候會讓度出cpu資源讓其餘線程使用,與sleep()不一樣的是,這個時候wait()方法是不佔有對應的鎖的。
在使用wait方法時候,最好使用以下模板:
synchronized (obj) { while (<condition does not hold>) obj.wait(timeout); ... // Perform action appropriate to condition }
關於wait/notify的例子,這裏就貼一個單生產者-單消費者模型的Demo吧:
private static final int MAX_NUM = 10; private static final Object lock = new Object(); static ArrayList<String> list = new ArrayList<>(); public static class ProductThread extends Thread { @Override public void run() { super.run(); while (true) { synchronized (lock) { while (list.size() > MAX_NUM) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } list.add("h"); System.out.println(getName() + ": 生產者生產一個元素"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } lock.notify(); } } } } public static class ConsumerThread extends Thread { @Override public void run() { super.run(); while (true) { synchronized (lock) { while (list.size() == 0) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } list.remove(0); System.out.println(getName() + ": 消費者消費一個元素"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } lock.notify(); } } } }