Java synchronized與CAS使用方式詳解
引言
上一篇文章中我們說過,volatile通過lock指令保證了可見性、有序性以及“部分”原子性。但在大部分并發(fā)問題中,都需要保證操作的原子性,volatile并不具有該功能,這時就需要通過其他手段來達(dá)到線程安全的目的,在Java編程中,我們可以通過鎖、synchronized關(guān)鍵字,以及CAS操作來達(dá)到線程安全的目的。
synchronized
在Java的并發(fā)編程中,保證線程同步最為程序員所熟悉的就是synchronized關(guān)鍵字,synchronized關(guān)鍵字最為方便的地方是他不需要顯示的管理鎖的釋放,極大減少了編程出錯的概率。
在Java1.5及以前的版本中,synchronized并不是同步最好的選擇,由于并發(fā)時頻繁的阻塞和喚醒線程,會浪費許多資源在線程狀態(tài)的切換上,導(dǎo)致了synchronized的并發(fā)效率在某些情況下不如ReentrantLock。在Java1.6的版本中,對synchronized進(jìn)行了許多優(yōu)化,極大的提高了synchronized的性能。只要synchronized能滿足使用環(huán)境,建議使用synchronized而不使用ReentrantLock。
synchronized的三種使用方式
1.修飾實例方法,為當(dāng)前實例加鎖,進(jìn)入同步方法前要獲得當(dāng)前實例的鎖。
2.修飾靜態(tài)方法,為當(dāng)前類對象加鎖,進(jìn)入同步方法前要獲得當(dāng)前類對象的鎖。
3.修飾代碼塊,指定加鎖對象,對給定對象加鎖,進(jìn)入同步代碼塊前要獲得給定對象的鎖。
這三種使用方式大家應(yīng)該都很熟悉,有一個要注意的地方是對靜態(tài)方法的修飾可以和實例方法的修飾同時使用,不會阻塞,因為一個是修飾的Class類,一個是修飾的實例對象。下面的例子可以說明這一點:
public class SynchronizedTest { public static synchronized void StaticSyncTest() { for (int i = 0; i < 3; i++) { System.out.println("StaticSyncTest"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } public synchronized void NonStaticSyncTest() { for (int i = 0; i < 3; i++) { System.out.println("NonStaticSyncTest"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } public static void main(String[] args) throws InterruptedException {SynchronizedTest synchronizedTest = new SynchronizedTest();new Thread(new Runnable() { @Override public void run() { SynchronizedTest.StaticSyncTest(); } }).start();new Thread(new Runnable() { @Override public void run() { synchronizedTest.NonStaticSyncTest(); } }).start(); } //StaticSyncTest //NonStaticSyncTest //StaticSyncTest //NonStaticSyncTest //StaticSyncTest //NonStaticSyncTest
代碼中我們開啟了兩個線程分別鎖定靜態(tài)方法和實例方法,從打印的輸出結(jié)果中我們可以看到,這兩個線程鎖定的是不同對象,可以并發(fā)執(zhí)行。
synchronized的底層原理
我們看一段synchronized關(guān)鍵字經(jīng)過編譯后的字節(jié)碼:
if (null == instance) { synchronized (DoubleCheck.class) { if (null == instance) { instance = new DoubleCheck(); } } }
可以看到synchronized關(guān)鍵字在同步代碼塊前后加入了monitorenter和monitorexit這兩個指令。monitorenter指令會獲取鎖對象,如果獲取到了鎖對象,就將鎖計數(shù)器加1,未獲取到則會阻塞當(dāng)前線程。monitorexit指令會釋放鎖對象,同時將鎖計數(shù)器減1。
JDK1.6對synchronized的優(yōu)化
JDK1.6對對synchronized的優(yōu)化主要體現(xiàn)在引入了“偏向鎖”和“輕量級鎖”的概念,同時synchronized的鎖只可升級,不可降級:
這里我不打算詳細(xì)講解每種鎖的實現(xiàn),想了解的可以參照《深入理解Java虛擬機》,只簡單說下自己的理解。
偏向鎖的思想是指如果一個線程獲得了鎖,那么就從無鎖模式進(jìn)入偏向模式,這一步是通過CAS操作來做的,進(jìn)入偏向模式的線程每一次訪問這個鎖的同步代碼塊時都不需要再進(jìn)行同步操作,除非有其他線程訪問這個鎖。
偏向鎖提高的是那些帶同步但無競爭的代碼的性能,也就是說如果你的同步代碼塊很長時間都是同一個線程訪問,偏向鎖就會提高效率,因為他減少了重復(fù)獲取鎖和釋放鎖產(chǎn)生的性能消耗。如果你的同步代碼塊會頻繁的在多個線程之間訪問,可以使用參數(shù)-XX:-UseBiasedLocking來禁止偏向鎖產(chǎn)生,避免在多個鎖狀態(tài)之間切換。
偏向鎖優(yōu)化了只有一個線程進(jìn)入同步代碼塊的情況,當(dāng)多個線程訪問鎖時偏向鎖就升級為了輕量級鎖。
輕量級鎖的思想是當(dāng)多個線程進(jìn)入同步代碼塊后,多個線程未發(fā)生競爭時一直保持輕量級鎖,通過CAS來獲取鎖。如果發(fā)生競爭,首先會采用CAS自旋操作來獲取鎖,自旋在極短時間內(nèi)發(fā)生,有固定的自旋次數(shù),一旦自旋獲取失敗,則升級為重量級鎖。
輕量級鎖優(yōu)化了多個線程進(jìn)入同步代碼塊的情況,多個線程未發(fā)生競爭時,可以通過CAS獲取鎖,減少鎖狀態(tài)切換。當(dāng)多個線程發(fā)生競爭時,不是直接阻塞線程,而是通過CAS自旋來嘗試獲取鎖,減少了阻塞線程的概率,這樣就提高了synchronized鎖的性能。
synchronized的等待喚醒機制
synchronized的等待喚醒是通過notify/notifyAll和wait三個方法來實現(xiàn)的,這三個方法的執(zhí)行都必須在同步代碼塊或同步方法中進(jìn)行,否則將會報錯。
wait方法的作用是使當(dāng)前執(zhí)行代碼的線程進(jìn)行等待,notify/notifyAll相同,都是通知等待的代碼繼續(xù)執(zhí)行,notify只通知任一個正在等待的線程,notifyAll通知所有正在等待的線程。wait方法跟sleep不一樣,他會釋放當(dāng)前同步代碼塊的鎖,notify在通知任一等待的線程時不會釋放鎖,只有在當(dāng)前同步代碼塊執(zhí)行完成之后才會釋放鎖。下面的代碼可以說明這一點:
public static void main(String[] args) throws InterruptedException {waitThread();notifyThread(); } private static Object lockObject = new Object(); private static void waitThread() {Thread watiThread = new Thread(new Runnable() {@Overridepublic void run() {synchronized (lockObject) {System.out.println(Thread.currentThread().getName() + "wait-before");try {TimeUnit.SECONDS.sleep(2);lockObject.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "after-wait");}}},"waitthread");watiThread.start(); } private static void notifyThread() {Thread watiThread = new Thread(new Runnable() {@Overridepublic void run() {synchronized (lockObject) {System.out.println(Thread.currentThread().getName() + "notify-before");lockObject.notify();try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();} System.out.println(Thread.currentThread().getName() + "after-notify");}}},"notifythread");watiThread.start(); } //waitthreadwait-before //notifythreadnotify-before //notifythreadafter-notify //waitthreadafter-wait
代碼中notify線程通知之后wait線程并沒有馬上啟動,還需要notity線程執(zhí)行完同步代碼塊釋放鎖之后wait線程才開始執(zhí)行。
CAS
在synchronized的優(yōu)化過程中我們看到大量使用了CAS操作,CAS全稱Compare And Set(或Compare And Swap),CAS包含三個操作數(shù):內(nèi)存位置(V)、原值(A)、新值(B)。簡單來說CAS操作就是一個虛擬機實現(xiàn)的原子操作,這個原子操作的功能就是將舊值(A)替換為新值(B),如果舊值(A)未被改變,則替換成功,如果舊值(A)已經(jīng)被改變則替換失敗。
可以通過AtomicInteger類的自增代碼來說明這個問題,當(dāng)不使用同步時下面這段代碼很多時候不能得到預(yù)期值10000,因為noncasi[0]++不是原子操作。
private static void IntegerTest() throws InterruptedException {final Integer[] noncasi = new Integer[]{ 0 };for (int i = 0; i < 10; i++) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {for (int j = 0; j < 1000; j++) {noncasi[0]++;}}});thread.start();}while (Thread.activeCount() > 2) {Thread.sleep(10);}System.out.println(noncasi[0]); } //7889
當(dāng)使用AtomicInteger的getAndIncrement方法來實現(xiàn)自增之后相當(dāng)于將casi.getAndIncrement()操作變成了原子操作:
private static void AtomicIntegerTest() throws InterruptedException {AtomicInteger casi = new AtomicInteger();casi.set(0);for (int i = 0; i < 10; i++) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {for (int j = 0; j < 1000; j++) {casi.getAndIncrement();}}});thread.start();}while (Thread.activeCount() > 2) {Thread.sleep(10);}System.out.println(casi.get()); } //10000
當(dāng)然也可以通過synchronized關(guān)鍵字來達(dá)到目的,但CAS操作不需要加鎖解鎖以及切換線程狀態(tài),效率更高。
再來看看casi.getAndIncrement()具體做了什么,在JDK1.8之前getAndIncrement是這樣實現(xiàn)的(類似incrementAndGet):
private volatile int value; public final int incrementAndGet() {for (;;) {int current = get();int next = current + 1;if (compareAndSet(current, next))return next;} }
通過compareAndSet將變量自增,如果自增成功則完成操作,如果自增不成功,則自旋進(jìn)行下一次自增,由于value變量是volatile修飾的,通過volatile的可見性,每次get()都能獲取到最新值,這樣就保證了自增操作每次自旋一定次數(shù)之后一定會成功。
JDK1.8中則直接將getAndAddInt方法直接封裝成了原子性的操作,更加方便使用。
public final int getAndIncrement() {return unsafe.getAndAddInt(this, valueOffset, 1); }
CAS操作是實現(xiàn)Java并發(fā)包的基石,他理解起來比較簡單但同時也非常重要。Java并發(fā)包就是在CAS操作和volatile基礎(chǔ)上建立的,下圖中列舉了J.U.C包中的部分類支撐圖:
到此這篇關(guān)于Java synchronized與CAS使用方式詳解的文章就介紹到這了,更多相關(guān)Java synchronized與CAS內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Java同步鎖Synchronized底層源碼和原理剖析(推薦)
- java同步鎖的正確使用方法(必看篇)
- 95%的Java程序員人都用不好Synchronized詳解
- Java?synchronized同步關(guān)鍵字工作原理
- Java synchronized偏向鎖的概念與使用
- Java?synchronized輕量級鎖實現(xiàn)過程淺析
- Java synchronized重量級鎖實現(xiàn)過程淺析
- Java @Transactional與synchronized使用的問題
- Java?synchronized與死鎖深入探究
- 淺析Java關(guān)鍵詞synchronized的使用
- synchronized及JUC顯式locks?使用原理解析
- java鎖synchronized面試常問總結(jié)
- Java?HashTable與Collections.synchronizedMap源碼深入解析
- Java?Synchronized鎖的使用詳解
- AQS加鎖機制Synchronized相似點詳解
- Java必會的Synchronized底層原理剖析
- 一個例子帶你看懂Java中synchronized關(guān)鍵字到底怎么用
- 詳解Java?Synchronized的實現(xiàn)原理
- Synchronized?和?ReentrantLock?的實現(xiàn)原理及區(qū)別
- Java同步鎖synchronized用法的最全總結(jié)
相關(guān)文章
Java畢業(yè)設(shè)計實戰(zhàn)之仿小米電子產(chǎn)品售賣商城系統(tǒng)的實現(xiàn)
這是一個使用了java+SpringBoot+Vue+MySQL+Redis+ElementUI開發(fā)的仿小米商城系統(tǒng),是一個畢業(yè)設(shè)計的實戰(zhàn)練習(xí),具有小米商城該有的所有基礎(chǔ)功能,感興趣的朋友快來看看吧2022-01-01SpringMVC中利用@InitBinder來對頁面數(shù)據(jù)進(jìn)行解析綁定的方法
本篇文章主要介紹了SpringMVC中利用@InitBinder來對頁面數(shù)據(jù)進(jìn)行解析綁定的方法,非常具有實用價值,需要的朋友可以參考下2018-03-03Java Morris遍歷算法及其在二叉樹中的應(yīng)用
Morris遍歷是一種基于線索二叉樹的遍歷算法,可以在不使用?;蜻f歸的情況下,實現(xiàn)二叉樹的前序、中序和后序遍歷。該算法利用二叉樹中的空指針或線索指針,將遍歷序列嵌入到原二叉樹中,實現(xiàn)了常數(shù)級別的空間復(fù)雜度,適用于對空間要求較高的場景2023-04-04