Synchronized關鍵字一直是工作和面試中的重點。這篇文章準備徹徹底底的從基礎使用到原理缺陷等各個方面來一個分析,這篇文章由于篇幅比較長,但是如果你有時間和耐心,相信會有一個比較大的收獲,是以,學習請慢慢來。這篇文章主要從以下幾個方面進行分析講解.
1、Synchronized關鍵字的簡介,主要是為什麼要使用Synchronized關鍵字,極其作用地位。
2、Synchronized關鍵字的使用,主要是從對象鎖和類鎖兩個角度。
3、Synchronized關鍵字的使用注意事項。分析了6種常見的使用情況。
4、Synchronized關鍵字的兩個性質,主要是可重入性和不可中斷性。
5、Synchronized關鍵字的底層原理。
6、Synchronized關鍵字的常見缺陷。
以上我們主要是從這7個角度來分析Synchronized關鍵字,每一個角度說實話都能單獨拿出來作為一篇文章來分析。但是由于考慮到文章的連貫性,是以綜合在了一起,循序漸進。下面我們就帶着這些問題開始今天的文章。
Synchronized一句話來解釋其作用就是:能夠保證同一時刻最多隻有一個線程執行該段代碼,以達到并發安全的效果。也就是說Synchronized就好比是一把鎖,某個線程把資源鎖住了之後,别人就不能使用了,隻有當這個線程用完了别人才能用。
對于Synchronized關鍵字來說,它是并發程式設計中一個元老級角色,也就是說你隻要學習并發程式設計,就必須要學習Synchronized關鍵字。由此可見其地位。
說了這麼多,好像我們還沒體驗過它的威力。我們就直接舉個例子,來分析一下。
上面代碼要完成的功能就是,thread1對a進行增加,一直到1000,thread2再對a進行增加,一直到2000。不過如果我們運作過之後我們就會發現,最後的輸出值總是小于2000,這是為什麼呢?
這是因為我們在執行a++的時候其實包含了以下三個操作:
(1)線程1讀取a
(2)線程1将a加1
(3)将a的值寫入記憶體
出錯原因的關鍵就在于第二操作和第三個操作之間,此時線程1還沒來得及把a的值寫入記憶體,線程2就把舊值讀走了,這也就造成了a加了兩次,但是記憶體中的a的值隻增加了1。這也就是不同步現象。
但是如果說我們使用了Synchronized關鍵字之後呢?
現在我們使用synchronized關鍵字把這一塊代碼鎖住,不管你怎麼輸出都是2000了,鎖住之後,同一時刻隻有一個線程進入。也就不會發生上面a寫操作不同步的現象了。
現在相信你開始覺得synchronized關鍵字的确很實用,可以解決多線程中的很多問題。上面這個小例子隻是帶我們去簡單的認識一下,下面我們就來看看其詳細的使用。
對于synchronized關鍵字來說,一共可以分為兩類:對象鎖和類鎖。
我們一個一個來看如何使用。
對于對象鎖來說,又可以分為兩個,一個是方法鎖,一個是同步代碼塊鎖。
(1)同步代碼塊鎖
同步代碼塊鎖主要是對代碼塊進行加鎖,其實已經示範過了,就是上面的那個案例。不過為了保持一緻我們再舉一個例子。
在這個例子中,我們使用了synchronized鎖住了run方法中的代碼塊。表示同一時刻隻有一個線程能夠進入代碼塊。就好比是去醫院挂号,前面一個人辦完了業務,下一個人才開始。
在這裡面我們看到,線程1和線程2使用的是同一個鎖,也就是我們new的Object。如果我們讓線程1和線程2每一個人擁有一個鎖對象呢?
現線上程1和線程2每個人擁有一把鎖,去通路不同的方法資源。這時候會出現什麼情況呢?
我們同樣用一張圖看一下其原理。
也就是說,相當于兩個業務有倆視窗都可以辦理,但是兩個任務都需要排隊辦理。
同步代碼塊鎖總結:
同步代碼塊鎖主要是對代碼塊進行加鎖,此時同一時刻隻能有一個線程擷取到該資源,要注意每一把鎖隻負責目前的代碼塊,其他的代碼塊不管。
以上就是同步代碼快的使用方法。下面我們看對象鎖的另外一種形式,那就是方法鎖。這裡的方法鎖指代的是普通方法。
(2)方法鎖
方法鎖相比較同步代碼塊鎖就簡單很多了,就是在普通方法上添加synchronized關鍵字修飾即可。
在這個例子中我們使用兩個線程對同一個普通方法進行通路,結果可想而知,也就是同一時刻隻能有一個線程進入到此方法。我們運作一下,看一下結果。
跟我們預想的一樣,很簡單。不過我們想過一個問題沒有,此時我們synchronized關鍵字加了一把鎖,這個鎖指代是誰呢?像同步代碼塊鎖synchronized (object),這裡面都有object,但是方法鎖是誰呢?
答案就是this對象,也就是說我們在方法鎖裡面synchronized其實鎖的就是目前this對象。我們如何去驗證this鎖的存在呢?不如我們再舉一個例子:
上面這個例子中,我們定義了兩個synchronized關鍵字修飾的方法method1和method2,然後讓兩個線程同時運作,我們測試一下看看會出現什麼結果:
從結果來看,會發現不管是method1還是method2,同一個時刻兩個方法隻能有一個線程在運作。這也就是this鎖導緻的。我們再給一張圖描述一下其原理。
現在應該明白了吧,這也就驗證了方法鎖的存在。也驗證了方法鎖的原理。下面我們繼續。讨論一下類鎖。
上面的鎖都是對象鎖,下面我們看看類鎖。類鎖其實也有兩種形式,一種是static方法鎖,一種是class鎖。
(1)static方法鎖
在java中,java的類對象可能有無數個,但是類卻隻有一個。首先我們看第一種形式。
在這個例子中我們定義了兩個不同的對象instance1和instance2。分别去執行了method1。會出現什麼結果呢?
如果我們把static關鍵字去掉,很明顯現在就是普通方法了,如果我們再去運作,由于instance1和instance2是兩個不同的對象,那麼也就是兩個不同的this鎖,這時候就能随便進入了。我們去掉static關鍵字之後運作一下:
現在看到了,由于是兩個不同的this鎖,是以都能進入,就好比是一個門有兩把鑰匙,每一把都能打開門。
(2)class鎖
這種用法我們直接看例子再來分析一下:
在這個例子中我們使用了同步代碼塊,不過synchronized關鍵字包裝的可不是object了,而是SynTest5.class。我們還定義了兩個不同的對象執行個體instance1和instance2。運作一下我們會發現,線程1和線程2依然會依次執行。
以上就是synchronized關鍵字的幾種常見的用法,到這裡我們來一個總結:
對于同步不同步,關鍵點在于鎖,兩個線程執行的是同一把鎖,那麼就依次排隊等候,兩個線程執行的不是同一把鎖,那就各幹各的事。
基本的使用我們也講完了,下面我們進入下一個專題,那就是我們需要注意的事項。這是面試常考的一個問題,不管是機試還是面試。
我們先給出這6種常見的情況,然後一個一個分析。
1、兩個線程同時通路一個對象的同步方法。
2、兩個線程通路的是兩個對象的同步方法。
3、兩個線程通路的是synchronized的靜态方法。
4、兩個線程同時通路同步方法與非同步方法。
5、一個線程通路一個類的兩個普通同步方法。
6、同時通路靜态同步方法和非靜态同步方法。
為了對這6種情況做到心中有數,不至于搞混了,我們畫一張圖,對每一種情況進行分析。
上面是架構圖,下面我們基于開始來分析:
這種情況對應于以下這張圖:
這種情況很簡單,我們在上面也示範過,結果就是同一個時刻隻能有一個方法進入。這裡就不再示範了。
這種情況對應于下面這種:
也就是一個方法有兩把鎖,線程1和線程2互不幹擾的通路。鎖是不起作用的。
這種情況對應于下面這種情況:
我們對這種情況來測試一下吧。
在這個例子中我們執行個體化了兩個對象instance1和instance2,并且存放在了兩個不同的線程中,我們測試一下通路同一個static同步方法你會發現。即使是執行個體不同,鎖也會生效,也就是同一時刻隻能有一個線程進去。
這種情況對應于下面這張圖:
我們對這種情況使用代碼進行示範一遍:
在上面的代碼中,我們定義一個對象,但是使用了兩個線程去分别同時通路同步和非同步方法。我們看結果:
也就是說,同步方法依然會同步執行,非同步方法不會受到任何影響。
我們代碼來測試一下:
上面這個例子我們建立了一個對象instance1,然後使用一個線程分别去通路同步方法1和同步方法2。結果呢可想而知,所一定會失效。因為在一開始我們已經驗證了,此時同步方法1和同步方法2中synchronized鎖的就是this對象,是以是同一把鎖。當然會生效。
我們使用代碼來測試一波:
在上面的代碼中,我們建立了一個instance執行個體,使用兩個線程同時通路普通同步方法和靜态同步方法。下面運作一下,看看輸出結果:
上面輸出結果表明普通同步方法和靜态同步方法是沒有關聯的,這是為什麼呢?這是因為普通同步方法的鎖是對象,但是靜态同步方法的鎖是類,是以這是兩把鎖。鎖自然也就是失效了。
讀到這裡,不知道你是不是已經很疲憊了,反正我寫的是很難受,不過剩下的這些部分才是精華,也是面試或者是工作中提升你zhuangbility的一個點。希望你一定要注意。認真讀下去。
對于synchronized關鍵字主要有兩個性質:可重入性質和不可中斷性質。我們分别來看。
什麼是可重入呢?指的是同一線程的外層函數獲得鎖之後,内層函數可以直接再次擷取該鎖。我們舉一個例子來說明,一句話吃着碗裡的看着鍋裡的。嘴裡面還沒吃完就繼續再去拿吃的。這就是可重入。不可重入的意思正好相反,你吃完了這碗飯才能盛下一碗。
可重入的程度可以細分為三種情況,我們分别測試一下:
(1)同一個方法中是不是可重入的。就好比是遞歸調用同步方法。
(2)不同的方法是不是可重入的。就好比是一個同步方法調用另外一個同步方法。
(3)不同的類方法是不是可重入的。
下面我們就是用代碼來測試一遍:
(1)同一個方法是不是可重入的
代碼很簡單,也就是我們定義了一個變量a,隻要a不等于3,就一直遞歸調用方法method1。我們可以看一下運作結果。
也就是說在同一個方法中是可重入的。下面我們接着測試。
(2)不同的方法是不是可重入的
我們在同步方法1中調用了同步方法2。我們同樣測試一下。
method1和method2可以依次輸出,說明了在不同的方法中也是可重入的。
(3)、不同的類方法是不是可重入的
既然是不同的類,那麼我們就在這裡定義兩個類,一個是Father,一個是Son。我們讓son調用father中的方法。
在這裡son類中使用super.father()調用了父類中的synchronized方法,我們測試一下看看輸出結果:
不可中斷的意思你可以這樣了解,别人正在打遊戲,你也想玩,你必須要等别人不想玩了你才能去。在java中表示一旦這個鎖被别人搶走了,你必須等待。等别的線程釋放了鎖,你才可以拿到。否則就一直等下去。
這一點看起來是個有點但其實在某些場景下弊端超級大,因為假如拿到鎖得線程永遠的不釋放,那你就要永遠的等下去。
對于原理,最好的方式就是深入到JVM中去。我們可以編譯看看其位元組碼檔案,再來分析,是以在這裡舉一個最簡單的例子。
分析的步驟很簡單,我們通過反編譯位元組碼檔案。記住我們的類名是SynTest11。
先編譯生成位元組碼檔案。
然後,我們再反編譯位元組碼檔案。
以上我們知道其是就是設定了一個監控器monitor。線程進來那就是monitorenter,線程離開是monitorexit。這就是synchronized關鍵字最基本的原理。
在上面我們曾提到可重入的性質,那麼synchronized關鍵字是如何保證的呢?其是工作是由我們的jvm來完成的,線程第一次給對象加鎖的時候,計數為1,以後這個線程再次擷取鎖的時候,計數會依次增加。同理,任務離開的時候,相應的計數器也會減少。
java記憶體模型不是真正存在的,但是我們可以給出一個記憶體模型。synchronized關鍵字,會對同步的代碼會先寫到工作記憶體,等synchronized修飾的代碼塊一結束,就會寫入到主記憶體,這樣保證了同步。
synchronized關鍵字既有優點也有缺點,而且缺點賊多,是以後來出現了比他更好的鎖。下面我們就來分析一下,這也是面試常問問題。
我們之前曾經分析過synchronized關鍵字是不可中斷的,這也就意味着一個等待的線程如果不能擷取到鎖将會一直等待,而不能再去做其他的事了。
這裡也說明了對synchronized關鍵字的一個改進措施,那就是設定逾時時間,如果一個線程長時間拿不到鎖,就可以去做其他事情了。
加鎖和解鎖的時候,每個鎖隻能有一個對象處理,這對于目前分布式等思想格格不入。
也就是我們的鎖如果擷取到了,我們無法得知。既然無法得知我們也就很不容易進行改進。
既然synchronized有這麼多缺陷。是以才出現了各種各樣的鎖。
終于寫完了,synchronized涉及到的知識點,以及能夠引出來的知識點超級多,不過隻有了解synchronized關鍵字,我們才可以更加深入的學習。本篇文章不可能面面俱到,隻能說列出來一些常見的知識點。更加深入的了解我也會在後續的文章中指出。感謝大家的支援。