以前的一篇文章有過Syncronized和Volatile的使用,是單例中用到的,原文如下
同步(synchronized)簡單說可以理解為共享的意思,如果資源不是共享的,就沒必要進行同步。設置共享資源為同步的話,可以避免一些髒讀情況。
synchronized的4種修飾位置
public synchronized void synMethod() { //方法體
}
2.對某一代碼塊使用,synchronized後跟括號,括號裡是變量,這樣,一次只有一個線程進入該代碼塊,如:
public int synMethod(int a1){ 代碼塊
synchronized(a1) {
//一次只能有一個線程進入
}
}
3.synchronized後面括號裡是一對象,此時,線程獲得的是對象鎖.例如:
public class MyThread implements Runnable {
public static void main(String args[]) {
MyThread mt = new MyThread();
Thread t1 = new Thread(mt, "t1");
Thread t2 = new Thread(mt, "t2");
t1.start();
t2.start();
}
public void run() {
synchronized (this) {
System.out.println(Thread.currentThread().getName());
}
}
}
4.synchronized後面括號裡是類.例如:
public static class Single2 { private static Single2 instance; private Single2() {} public static Single2 getInstance() { synchronized(Single2.class) { if (instance == null) { instance = new Single2(); } return instance; } }}
鎖類型
- 可重入鎖:在執行對象中所有同步方法不用再次獲得鎖
- 可中斷鎖:在等待獲取鎖過程中可中斷
- 公平鎖: 按等待獲取鎖的線程的等待時間進行獲取,等待時間長的具有優先獲取鎖權利
- 讀寫鎖:對資源讀取和寫入的時候拆分為2部分處理,讀的時候可以多線程一起讀,寫的時候必須同步地寫
java中另一個使用鎖的基本工具 Lock
synchronized與Lock的區別

看一下Lock的部分源碼:
public interface Lock {
/**
* Acquires the lock.
*/
void lock();
/**
* Acquires the lock unless the current thread is
* {@linkplain Thread#interrupt interrupted}.
*/
void lockInterruptibly() throws InterruptedException;
/**
* Acquires the lock only if it is free at the time of invocation.
*/
boolean tryLock();
/**
* Acquires the lock if it is free within the given waiting time and the
* current thread has not been {@linkplain Thread#interrupt interrupted}.
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
* Releases the lock.
*/
void unlock();
}
從Lock接口中我們可以看到主要有個方法,這些方法的功能從註釋中可以看出:
- lock():獲取鎖,如果鎖被暫用則一直等待
- unlock():釋放鎖
- tryLock(): 注意返回類型是boolean,如果獲取鎖的時候鎖被佔用就返回false,否則返回true
- tryLock(long time, TimeUnit unit):比起tryLock()就是給了一個時間期限,保證等待參數時間
- lockInterruptibly():用該鎖的獲得方式,如果線程在獲取鎖的階段進入了等待,那麼可以中斷此線程,先去做別的事
Lock實現和synchronized不一樣,後者是一種悲觀鎖,它膽子很小,它很怕有人和它搶吃的,所以它每次吃東西前都把自己關起來。而Lock呢底層其實是CAS樂觀鎖的體現,它無所謂,別人搶了它吃的,它重新去拿吃的就好啦,所以它很樂觀。具體底層怎麼實現,後期對current包下面的機制好好和大家說說,如果面試問起,你就說底層主要靠volatile和CAS操作實現。
使用synchronized 代碼塊相比方法有兩點優勢:
1、可以只對需要同步的使用 2、與wait()/notify()/nitifyAll()一起使用時,比較方便 ,前面 有過應用
volatile
作用:volatile關鍵字的作用是:使變量在多個線程間可見(具有可見性),但是僅靠volatile是不能保證線程的安全性,volatile關鍵字不具備synchronized關鍵字的原子性。
volatile關鍵字的兩層語義
一旦一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾之後,那麼就具備了兩層語義:
- 保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。
- 禁止進行指令重排序。
- volatile不保證原子性

大家想一下這段程序的輸出結果是多少?也許有些朋友認為是10000。但是事實上運行它會發現每次運行結果都不一致,都是一個小於10000的數字。
可能有的朋友就會有疑問,不對啊,上面是對變量inc進行自增操作,由於volatile保證了可見性,那麼在每個線程中對inc自增完之後,在其他線程中都能看到修改後的值啊,所以有10個線程分別進行了1000次操作,那麼最終inc的值應該是1000*10=10000。
這裡面就有一個誤區了,volatile關鍵字能保證可見性沒有錯,但是上面的程序錯在沒能保證原子性。可見性只能保證每次讀取的是最新的值,但是volatile沒辦法保證對變量的操作的原子性。
自增操作是不具備原子性的,它包括讀取變量的原始值、進行加1操作、寫入工作內存。那麼就是說自增操作的三個子操作可能會分割開執行,就有可能導致下面這種情況出現:
假如某個時刻變量inc的值為10,
線程1對變量進行自增操作,線程1先讀取了變量inc的原始值,然後線程1被阻塞了;
然後線程2對變量進行自增操作,線程2也去讀取變量inc的原始值,由於線程1只是對變量inc進行讀取操作,而沒有對變量進行修改操作,所以不會導致線程2的工作內存中緩存變量inc的緩存行無效,所以線程2會直接去主存讀取inc的值,發現inc的值時10,然後進行加1操作,並把11寫入工作內存,最後寫入主存。
然後線程1接著進行加1操作,由於已經讀取了inc的值,注意此時在線程1的工作內存中inc的值仍然為10,所以線程1對inc進行加1操作後inc的值為11,然後將11寫入工作內存,最後寫入主存。
那麼兩個線程分別進行了一次自增操作後,inc只增加了1。
解釋到這裡,可能有朋友會有疑問,不對啊,前面不是保證一個變量在修改volatile變量時,會讓緩存行無效嗎?然後其他線程去讀就會讀到新的值,對,這個沒錯。這個就是上面的happens-before規則中的volatile變量規則,但是要注意,線程1對變量進行讀取操作之後,被阻塞了的話,並沒有對inc值進行修改。然後雖然volatile能保證線程2對變量inc的值讀取是從內存中讀取的,但是線程1沒有進行修改,所以線程2根本就不會看到修改的值。
根源就在這裡,自增操作不是原子性操作,而且volatile也無法保證對變量的任何操作都是原子性的。
可以通過synchronized,Lock或者AtomicInteger結合進行修飾保證原子性。
volatile能保證有序性嗎?
在前面提到volatile關鍵字能禁止指令重排序,所以volatile能在一定程度上保證有序性。
volatile關鍵字禁止指令重排序有兩層意思:
1)當程序執行到volatile變量的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見;在其後面的操作肯定還沒有進行;
2)在進行指令優化時,不能將在對volatile變量訪問的語句放在其後面執行,也不能把volatile變量後面的語句放到其前面執行。
volatile的原理和實現機制
前面講述了源於volatile關鍵字的一些使用,下面我們來探討一下volatile到底如何保證可見性和禁止指令重排序的。
下面這段話摘自《深入理解Java虛擬機》:
“觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令”
lock前綴指令實際上相當於一個內存屏障(也成內存柵欄),內存屏障會提供3個功能:
1)它確保指令重排序時不會把其後面的指令排到內存屏障之前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操作已經全部完成;
2)它會強制將對緩存的修改操作立即寫入主存;
3)如果是寫操作,它會導致其他CPU中對應的緩存行無效。
volatile 與 synchronized 的比較:
①volatile輕量級,只能修飾變量。synchronized重量級,還可修飾方法
②volatile只能保證數據的可見性,不能用來同步,因為多個線程併發訪問volatile修飾的變量不會阻塞。
synchronized不僅保證可見性,而且還保證原子性,因為,只有獲得了鎖的線程才能進入臨界區,從而保證臨界區中的所有語句都全部執行。多個線程爭搶synchronized鎖對象時,會出現阻塞。
線程安全性包括 原子性問題,可見性問題,有序性問題。
CAS
無鎖的非堵塞算法採用一種比較交換技術CAS(compare and swap)來鑑別線程衝突,一旦檢測到衝突,就充實當前操作指導沒有衝突為止。CAS基於硬件實現,不需要進入內核,不需要切換線程,因此可以獲得更高的性能。但對於資源競爭嚴重的情況,CAS自旋的概率會比較大,從而浪費更多的CPU資源。
CAS是項樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。
CAS(比較並交換)是CPU指令級的操作,只有一步原子操作,CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改為B,否則什麼都不做。CAS語義是“我認為V的值應該是A,如果是,那麼就將V的值更新成B, 否則不更新,並告訴V的實際是是多少”。
偽代碼可以這樣表示:
do{ 備份舊數據; 基於舊數據構造新數據; }while(!CAS( 內存地址,備份的舊數據,新數據 ))
就是指當兩者進行比較時,如果相等,則證明共享數據沒有被修改,替換成新值,然後繼續往下運行;如果不相等,說明共享數據已經被修改,放棄已經所做的操作,然後重新執行剛才的操作。容易看出 CAS 操作是基於共享數據不會被修改的假設,採用了類似於數據庫的 commit-retry 的模式。當同步衝突出現的機會很少時,這種假設能帶來較大的性能提升。
JVM中的CAS
堆中對象的分配
我們都知道java調用new object()會創建一個對象,這個對象會被分配到JVM的堆中。那麼這個對象到底是怎麼在堆中保存的呢?
首先,new object()執行的時候,這個對象需要多大的空間,其實是已經確定的,因為java中的各種數據類型,佔用多大的空間都是固定的(對其原理不清楚的請自行Google)。那麼接下來的工作就是在堆中找出那麼一塊空間用於存放這個對象。 在單線程的情況下,一般有兩種分配策略:
指針碰撞
這種一般適用於內存是規整的(內存是否規整取決於內存回收策略),分配空間的工作只是將指針像空閒內存一側移動對象大小的距離即可。
空閒列表
這種適用於內存非規整的情況,這種情況下JVM會維護一個內存列表,記錄那些內存區域是空閒的,大小是多少哦啊。給對象分配空間的時候去空閒列表裡查詢到合適的區域然後進行分配即可
但是JVM不可能一直在單線程狀態下運行,那樣效率太差了。由於再給一個對象分配內存的時候不是原子性的操作,至少需要以下幾步:查找空閒列表、分配內存、修改空閒列表等等,這是不安全的。解決併發時的安全問題也有兩種策略
CAS缺點:
雖然CAS有效的解決了原子操作的問題,但是其仍然有三個劣勢:
1、ABA問題:因為CAS需要在操作前檢查下值有沒有發生變化,如果沒有則更新。但是如果一個值開始的時候是A,變成了B,又變成了A,那麼使用CAS進行檢查的時候會發現它的值沒有發生變化,但是事實卻不是如此。
ABA問題的解決思路是使用版本號,如A-B-A變成1A-2B-3A
2、循環時間長開銷大:自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。
3、只能保證一個共享變量的原子操作:對一個共享變量可以使用CAS進行原子操作,但是多個共享變量的原子操作就無法使用CAS,這個時候只能使用鎖。
synchronized在jdk1.6之後,已經改進優化。synchronized的底層實現主要依靠Lock-Free的隊列,基本思路是自旋後阻塞,競爭切換後繼續競爭鎖,稍微犧牲了公平性,但獲得了高吞吐量。在線程衝突較少的情況下,可以獲得和CAS類似的性能;而線程衝突嚴重的情況下,性能遠高於CAS。
如有錯誤或者補充歡迎留言指正,謝謝
閱讀更多 全棧取經之路 的文章