線程安全是併發編程中的相當重要的,形成線程安全問題的主要緣由:java
而Java關鍵字synchronized,爲多線程場景下防止臨界資源訪問衝突提供支持, 能夠保證在同一時刻,只有一個線程能夠執行某個方法或某個代碼塊操做共享數據。編程
即當要執行代碼使用synchronized關鍵字時,它將檢查鎖是否可用,而後獲取鎖,執行代碼,最後再釋放鎖。而synchronized有三種使用方式:安全
首先看一下沒有使用synchronized關鍵字,以下:多線程
public class ThreadNoSynchronizedTest { public void method1(){ try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("method1"); } public void method2() { System.out.println("method2"); } public static void main(String[] args) { ThreadNoSynchronizedTest tnst= new ThreadNoSynchronizedTest(); Thread t1 = new Thread(new Runnable() { @Override public void run() { tnst.method1(); } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { tnst.method2(); } }); t1.start(); t2.start(); } }
在上述的代碼中,method1比method2多了2s的延時,所以在t1和t2線程同時執行的狀況下,執行結果:併發
method2
method1
當method1和method2使用了synchronized關鍵字後,代碼以下:app
public synchronized void method1(){ try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("method1"); } public synchronized void method2() { System.out.println("method2"); }
此時,因爲method1佔用了鎖,所以method2必需要等待method1執行完以後才能執行,執行結果:ide
method1
method2
所以synchronized鎖定是當前的對象,當前對象的synchronized方法在同一時間只能執行其中的一個,另外的synchronized方法需掛起等待,但不影響非synchronized方法的執行。下面的synchronized方法和synchronized代碼塊(把整個方法synchronized(this)包圍起來)等價的。性能
public synchronized void method1(){ } public void method2() { synchronized(this){ } }
synchronized靜態方法是做用在整個類上面的方法,至關於把類的class做爲鎖,示例代碼以下:優化
public class TreadSynchronizedTest { public static synchronized void method1(){ try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("method1"); } public static void method2() { synchronized(TreadTest.class){ System.out.println("method2"); } } public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { TreadSynchronizedTest.method1(); } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { TreadSynchronizedTest.method2(); } }); t1.start(); t2.start(); } }
因爲將class做爲鎖,所以method1和method2存在着競爭關係,method2中synchronized(ThreadTest.class)等同於在method2的聲明時void前面直接加上synchronized。上述代碼的執行結果仍然是先打印出method1的結果:this
method1
method2
synchronized代碼塊應用於處理臨界資源的代碼塊中,不須要訪問臨界資源的代碼能夠不用去競爭資源,減小了資源間的競爭,提升代碼性能。示例代碼以下:
public class TreadSynchronizedTest { private Object obj = new Object(); public void method1(){ System.out.println("method1 start"); synchronized(obj){ try { Thread.sleep(2000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("method1 end"); } } public void method2() { System.out.println("method2 start"); // 延時10ms,讓method1線獲取到鎖obj try { Thread.sleep(10); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } synchronized(obj){ System.out.println("method2 end"); } } public static void main(String[] args) { TreadSynchronizedTest tst = new TreadSynchronizedTest(); Thread t1 = new Thread(new Runnable() { @Override public void run() { tst.method1(); } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { tst.method2(); } }); t1.start(); t2.start(); } }
執行結果以下:
method1 start
method2 start
method1 end
method2 end
上述代碼中,執行method2方法,先打印出 method2 start, 以後執行同步塊,因爲此時obj被method1獲取到,method2只能等到method1執行完成後再執行,所以先打印method1 end,而後在打印method2 end。
synchronized 是JVM實現的一種鎖,其中鎖的獲取和釋放分別是monitorenter 和 monitorexit 指令。
加了 synchronized 關鍵字的代碼段,生成的字節碼文件會多出 monitorenter 和 monitorexit 兩條指令,而且會多一個 ACC_SYNCHRONIZED 標誌位,
當方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,若是設置了,執行線程將先獲取monitor,獲取成功以後才能執行方法體,方法執行完後再釋放monitor。
在方法執行期間,其餘任何線程都沒法再得到同一個monitor對象。其實本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需經過字節碼來完成。
在Java1.6以後,sychronized在實現上分爲了偏向鎖、輕量級鎖和重量級鎖,其中偏向鎖在 java1.6 是默認開啓的,輕量級鎖在多線程競爭的狀況下會膨脹成重量級鎖,有關鎖的數據都保存在對象頭中。
從輕量級鎖獲取的流程中咱們知道,當線程在獲取輕量級鎖的過程當中執行CAS操做失敗時,是要經過自旋來獲取重量級鎖的。問題在於,自旋是須要消耗CPU的,若是一直獲取不到鎖的話,那該線程就一直處在自旋狀態,白白浪費CPU資源。
其中解決這個問題最簡單的辦法就是指定自旋的次數,例如讓其循環10次,若是還沒獲取到鎖就進入阻塞狀態。可是JDK採用了更聰明的方式——適應性自旋,簡單來講就是線程若是自旋成功了,則下次自旋的次數會更多,若是自旋失敗了,則自旋的次數就會減小。
鎖粗化的概念應該比較好理解,就是將屢次鏈接在一塊兒的加鎖、解鎖操做合併爲一次,將多個連續的鎖擴展成一個範圍更大的鎖。舉個例子:
public class StringBufferTest { StringBuffer stringBuffer = new StringBuffer(); public void append(){ stringBuffer.append("a"); stringBuffer.append("b"); stringBuffer.append("c"); } }
這裏每次調用stringBuffer.append方法都須要加鎖和解鎖,若是虛擬機檢測到有一系列連串的對同一個對象加鎖和解鎖操做,就會將其合併成一次範圍更大的加鎖和解鎖操做,即在第一次append方法時進行加鎖,最後一次append方法結束後進行解鎖。
鎖消除即刪除沒必要要的加鎖操做。根據代碼逃逸技術,若是判斷到一段代碼中,堆上的數據不會逃逸出當前線程,那麼能夠認爲這段代碼是線程安全的,沒必要要加鎖。看下面這段程序:
public class SynchronizedTest02 { public static void main(String[] args) { SynchronizedTest02 test02 = new SynchronizedTest02(); for (int i = 0; i < 10000; i++) { i++; } long start = System.currentTimeMillis(); for (int i = 0; i < 100000000; i++) { test02.append("abc", "def"); } System.out.println("Time=" + (System.currentTimeMillis() - start)); } public void append(String str1, String str2) { StringBuffer sb = new StringBuffer(); sb.append(str1).append(str2); } }
雖然StringBuffer的append是一個同步方法,可是這段程序中的StringBuffer屬於一個局部變量,而且不會從該方法中逃逸出去,因此其實這過程是線程安全的,能夠將鎖消除。
Sychronized會讓沒有獲得鎖的資源進入Block狀態,爭奪到資源以後又轉爲Running狀態,這個過程涉及到操做系統用戶模式和內核模式的切換,代價比較高。
Java1.6爲 synchronized 作了優化,增長了從偏向鎖到輕量級鎖再到重量級鎖的過分,可是在最終轉變爲重量級鎖以後,性能仍然較低。