摘要:前言同步代碼塊是中最基礎的實現線程間的同步與通信的機制之一,本篇我們將對同步代碼塊以及監視器鎖的概念進行討論。離開同步代碼塊后,所獲得的鎖會被自動釋放。
前言
同步代碼塊(Synchronized Block) 是java中最基礎的實現線程間的同步與通信的機制之一,本篇我們將對同步代碼塊以及監視器鎖的概念進行討論。
系列文章目錄
什么是同步代碼塊(Synchronized Block)同步代碼塊簡單來說就是將一段代碼用一把鎖給鎖起來, 只有獲得了這把鎖的線程才訪問, 并且同一時刻, 只有一個線程能持有這把鎖, 這樣就保證了同一時刻只有一個線程能執行被鎖住的代碼.
這里有兩個關鍵字需要注意: 一段代碼和鎖.
一段代碼一般來說, 由 synchronized 鎖住的代碼都是拿{}括起來的代碼塊:
synchronized(this) { //由鎖保護的代碼 }
但值得注意的是, synchronized 也可以用來修飾一個方法, 則對應的被鎖保護的一段代碼很自然就是整個方法體.
public class Foo { public synchronized void doSomething() { // 由鎖保護的代碼 } }鎖
其實鎖這個東西說起來很抽象, 你可以就把它想象成現實中的鎖, 所以它只不過是一塊令牌, 一把尚方寶劍, 它是木頭做的還是金屬做的并不重要, 你可以拿任何東西當作鎖, 重要的是它代表的含義: 誰持有它, 誰就有獨立訪問臨界區(即上面所說的一段代碼)的權利.
在java中, 我們可以拿一個對象當作鎖.
這里引用<
每個java對象都可以用做一個實現同步的鎖, 這些鎖被稱為內置鎖(Intrinsic Lock)或者監視器鎖(Monitor Lock). 線程在進入同步代碼塊之前會自動獲得鎖, 并且在退出同步代碼塊時自動釋放鎖.獲得內置鎖的唯一途徑就是進入由這個鎖保護的同步代碼塊或方法.
所以, synchronized 同步代碼塊的標準寫法應該是:
synchronized(reference-to-lock) { //臨界區 }
其中, 括號里面的reference-to-lock就是鎖的引用, 它只要指向一個Java對象就行, 你可以自己隨便new一個不相關的對象, 將它作為鎖放進去, 也可以像之前的例子一樣, 直接使用this, 代表使用當前對象作為鎖.
有的同學就要問了, 我們前面說可以用synchronized修飾一個方法, 并且也知道對應的由鎖保護的代碼塊就是整個方法體, 但是, 它的鎖是什么呢?
要回答這個問題,首先要區分synchronized 所修飾的方法是否是靜態方法:
如果synchronized所修飾的是靜態方法, 則其所用的鎖為Class對象
如果synchronized所修飾的是非靜態方法, 則其所用的鎖為方法調用所在的對象
當使用synchronized 修飾非靜態方法時, 以下兩種寫法是等價的:
//寫法1 public synchronized void doSomething() { // 由鎖保護的代碼 } //寫法2 public void doSomething() { synchronized(this) { // 由鎖保護的代碼 } }到底拿什么鎖住了同步代碼塊
同步代碼塊中最難理解的部分就是拿什么作為了鎖, 上面我們已經提到了三個 this, Class對象, 方法調用所在的對象, 并且我們也說明了可以拿任何java對象作為鎖.
this 和 方法調用所在的對象這兩個其實是一個意思, 我們需要特別注意的是, 一個Class可以有多個實例(Instance), 每一個Instance都可以作為鎖, 不同Instance就是不同的鎖, 同一個Instance就是同一個鎖, this 和 方法調用所在的對象 指代的都是調用這個同步代碼塊的對象.
這么說可能比較抽象, 我們直接上例子: (以下例子轉載自博客Java中Synchronized的用法)
class SyncThread implements Runnable { private static int count; public SyncThread() { count = 0; } public void run() { synchronized(this) { for (int i = 0; i < 5; i++) { try { System.out.println(Thread.currentThread().getName() + ":" + (count++)); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) { SyncThread syncThread = new SyncThread(); //線程1和線程2使用了SyncThread類的同一個對象實例 //因此, 這兩個線程中的synchronized(this), 持有的是同一把鎖 Thread thread1 = new Thread(syncThread, "SyncThread1"); Thread thread2 = new Thread(syncThread, "SyncThread2"); thread1.start(); thread2.start(); } }
運行結果:
SyncThread1:0
SyncThread1:1
SyncThread1:2
SyncThread1:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread2:7
SyncThread2:8
SyncThread2:9
這里兩個線程SyncThread1 和 SyncThread2 持有同一個對象syncThread的鎖, 因此同一時刻, 只有一個線程能訪問同步代碼塊, 線程SyncThread2 只有等 SyncThread1 執行完同步代碼塊后, SyncThread1線程自動釋放了鎖, 隨后 SyncThread2才能獲取同一把鎖, 進入同步代碼塊.
我們也可以修改一下main函數, 讓兩個線程持有同一Class對象的不同實例的鎖:
public static void main(String[] args) { Thread thread1 = new Thread(new SyncThread(), "SyncThread1"); Thread thread2 = new Thread(new SyncThread(), "SyncThread2"); thread1.start(); thread2.start(); }
上面這段等價于:
public static void main(String[] args) { SyncThread syncThread1 = new SyncThread(); SyncThread syncThread2 = new SyncThread(); Thread thread1 = new Thread(syncThread1, "SyncThread1"); Thread thread2 = new Thread(syncThread2, "SyncThread2"); thread1.start(); thread2.start(); }
運行結果:
SyncThread1:0
SyncThread2:1
SyncThread1:2
SyncThread2:3
SyncThread1:4
SyncThread2:5
SyncThread1:6
SyncThread2:7
SyncThread1:8
SyncThread2:9
可見, 兩個線程這次都能訪問同步代碼塊, 這是因為線程1執行的是syncThread1對象的同步代碼塊, 線程2執行的是syncThread2的同步代碼塊, 雖然這兩個同步代碼塊一樣, 但是他們在不同的對象實例里面, 即雖然它們都用this作為鎖, 但是this指代的對象在這兩個線程中不是同一個對象, 兩個線程各自都能獲得鎖, 因此各自都能執行這一段同步代碼塊.
這告訴我們, 當一段代碼用同步代碼塊包起來的時候, 并不絕對意味著這段代碼同一時刻只能由一個線程訪問, 這種情況只發生在多個線程訪問的是同一個Instance, 也就是說, 多個線程請求的是同一把鎖.
再回顧我們上面兩個例子, 第一個例子中, 兩個線程使用的是同一個對象實例, 他們需要同一把對象鎖 syncThread,
第二個例子中, 兩個線程分別使用了一個對象實例, 他們分別請求的是自己訪問的對象實例的鎖syncThread1, syncThread2, 因此都能訪問同步代碼塊.
導致不同線程可以同時訪問同步代碼塊的最根本原因就是我們使用的是當前實例對象鎖(this), 因為類的實例可以有多個, 這導致了同步代碼塊散布在類的多個實例中, 雖然同一個實例中的同步代碼塊只能由持有鎖的單個線程訪問(this對象鎖保護), 但是我們可以每個線程訪問自己的對象實例, 而每一個對象實例的同步代碼塊都是一致的, 這就間接導致了多個線程同時訪問了"同一個"同步代碼塊.
上面這種情況在某些條件下是沒有問題的, 例如同步代碼塊中不存在對靜態變量(共享的狀態量)的修改.
但是, 對于上面的例子, 這樣的情況明顯違背了我們加同步代碼塊的初衷.
要解決上面的情況, 一種可行的辦法就是像第一個例子一樣, 多個線程使用同一個對象實例, 例如在單例模式下, 本身就只有一個對象實例, 所以多個線程必將請求同一把鎖, 從而實現同步訪問.
另一種方法就是我們下面要講的: 使用Class鎖.
使用Class級別鎖前面我們提到:
如果synchronized所修飾的是靜態方法, 則其所用的鎖為Class對象
這是因為靜態方法是屬于類的而不屬于對象的, 因此synchronized修飾的靜態方法鎖定的是這個類的所有對象。我們來看下面一個例子(以下例子同樣轉載自博客Java中Synchronized的用法):
class SyncThread implements Runnable { private static int count; public SyncThread() { count = 0; } // synchronized 關鍵字加在一個靜態方法上 public synchronized static void staticMethod() { for (int i = 0; i < 5; i ++) { try { System.out.println(Thread.currentThread().getName() + ":" + (count++)); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } public void run() { staticMethod(); } public static void main(String[] args) { SyncThread syncThread1 = new SyncThread(); SyncThread syncThread2 = new SyncThread(); Thread thread1 = new Thread(syncThread1, "SyncThread1"); Thread thread2 = new Thread(syncThread2, "SyncThread2"); thread1.start(); thread2.start(); } }
運行結果:
SyncThread1:0
SyncThread1:1
SyncThread1:2
SyncThread1:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread2:7
SyncThread2:8
SyncThread2:9
可見, 靜態方法鎖定了類的所有對象, 用我們之前的話來說, 如果說"因為類的實例可以有多個, 這導致了同步代碼塊散布在類的多個實例中", 那么類的靜態方法就是阻止同步代碼塊散布在類的實例中, 因為類的靜態方法只屬于類本身.
其實, 上面的例子的本質就是拿Class對象作為鎖, 我們前面也提到了, 可以拿任何對象作為鎖, 如果我們直接拿類的Class對象作為鎖, 同樣可以保證所以線程請求的都是同一把鎖, 因為Class對象只有一個.
類鎖實際上是通過對象鎖實現的,即類的 Class 對象鎖。每個類只有一個 Class 對象,所以每個類只有一個類鎖。
class SyncThread implements Runnable { private static int count; public SyncThread() { count = 0; } public void run() { // 這里直接拿Class對象作為鎖 synchronized(SyncThread.class) { for (int i = 0; i < 5; i++) { try { System.out.println(Thread.currentThread().getName() + ":" + (count++)); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) { SyncThread syncThread1 = new SyncThread(); SyncThread syncThread2 = new SyncThread(); Thread thread1 = new Thread(syncThread1, "SyncThread1"); Thread thread2 = new Thread(syncThread2, "SyncThread2"); thread1.start(); thread2.start(); } }
這樣所得到的結果與上面的類的靜態方法加鎖是一致的。
幾點補充其實到這里, 重要的部分已經講完了, 下面補充說明幾點:
(1) 當一個線程訪問對象的一個synchronized(this)同步代碼塊時,另一個線程仍然可以訪問該對象中的非synchronized(this)同步代碼塊。
這個結論是顯而易見的, 在沒有加鎖的情況下, 所有的線程都可以自由地訪問對象中的代碼, 而synchronized關鍵字只是限制了線程對于已經加鎖的同步代碼塊的訪問, 并不會對其他代碼做限制.
這里也提示我們:
同步代碼塊應該越短小越好
(2) 當一個線程訪問object的一個synchronized(this)同步代碼塊時,其他線程對object中所有其它synchronized(this)同步代碼塊的訪問將被阻塞。
這個結論也是顯而易見的, 因為synchronized(this)拿的都是當前對象的鎖, 如果一個線程已經進入了一個同步代碼塊, 說明它已經拿到了鎖, 而訪問同一個object中的其他同步代碼塊同樣需要當前對象的鎖, 所以它們會被阻塞.
(3) synchronized關鍵字不能繼承。
對于父類中用synchronized 修飾的方法,子類在覆蓋該方法時,默認情況下不是同步的,必須顯式的使用 synchronized 關鍵字修飾才行, 當然子類也可以直接調用父類的方法, 這樣就間接實現了同步.
(4) 在定義接口方法時不能使用synchronized關鍵字。
(5) 構造方法不能使用synchronized關鍵字,但可以使用synchronized代碼塊來進行同步。
(6) 離開同步代碼塊后,所獲得的鎖會被自動釋放。
synchronized關鍵字通過一把鎖鎖住一段代碼, 使得線程只有在持有鎖的時候才能訪問這段代碼
任何java對象都可以作為這把鎖
可以在synchronized后面用()顯式的指定鎖. 也可以直接作用在方法上
作用于普通方法時, 相當于以this對象作為鎖, 此時同步代碼塊散布于類的所有實例中, 每一個實例的同步代碼塊的鎖 為該實例對象自身。
作用于靜態方法時, 相當于以Class對象作為鎖, 此時對象的所有實例只能爭搶同一把鎖。
內置鎖的一個重要的特性是當離開同步代碼塊之后, 會自動釋放鎖,而其他的高級鎖(如ReentrantLock)需要顯式釋放鎖。
思考題前面我們說明了synchronized 的使用方法,但對一些底層的細節并不了解,如:
前面說“獲得內置鎖的唯一途徑就是進入由這個鎖保護的同步代碼塊或方法.”, 這句話看上去很有道理,其實是廢話,同步代碼塊究竟是怎么獲得鎖的?
我們說,JAVA中任何對象都可以作為鎖,那么鎖信息是怎么被記錄和存儲的?
為什么代碼離開了同步代碼塊鎖就被釋放了,誰釋放了鎖,怎樣叫釋放了鎖?
這些問題,我們后續的文章再研究。
(完)
查看更多系列文章:系列文章目錄
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/76699.html
摘要:由此可見,自旋鎖和各有優劣,他們分別適用于競爭不多和競爭激烈的場景中。每一個試圖進入同步代碼塊的線程都會被封裝成對象,它們或在對象的中,或在中,等待成為對象的成為的對象即獲取了監視器鎖。 前言 系列文章目錄 前面兩篇文章我們介紹了synchronized同步代碼塊以及wait和notify機制,大致知道了這些關鍵字和方法是干什么的,以及怎么用。 但是,知其然,并不知其所以然。 例如...
摘要:本文對多線程基礎知識進行梳理,主要包括多線程的基本使用,對象及變量的并發訪問,線程間通信,的使用,定時器,單例模式,以及線程狀態與線程組。源碼采用構建,多線程這部分源碼位于模塊中。通知可能等待該對象的對象鎖的其他線程。 本文對多線程基礎知識進行梳理,主要包括多線程的基本使用,對象及變量的并發訪問,線程間通信,lock的使用,定時器,單例模式,以及線程狀態與線程組。 寫在前面 花了一周時...
摘要:在從返回前,線程與其他線程競爭重新獲得鎖。就緒隊列存儲了將要獲得鎖的線程,阻塞隊列存儲了被阻塞的線程。當線程呈狀態,調用線程對象的方法會出現異常。在執行同步代碼塊過程中,遇到異常而導致線程終止,鎖也會被釋放。 方法wait()的作用是使當前執行代碼的線程進行等待,wait()方法是Object類的方法,該方法用來將當前線程置入預執行隊列中,并且在wait()所在的代碼行處停止執行,直...
摘要:為了避免一篇文章的篇幅過長,于是一些比較大的主題就都分成幾篇來講了,這篇文章是筆者所有文章的目錄,將會持續更新,以給大家一個查看系列文章的入口。 前言 大家好,筆者是今年才開始寫博客的,寫作的初衷主要是想記錄和分享自己的學習經歷。因為寫作的時候發現,為了弄懂一個知識,不得不先去了解另外一些知識,這樣以來,為了說明一個問題,就要把一系列知識都了解一遍,寫出來的文章就特別長。 為了避免一篇...
閱讀 2089·2021-11-23 09:51
閱讀 3697·2021-10-20 13:49
閱讀 1706·2021-09-06 15:13
閱讀 1815·2021-09-06 15:02
閱讀 3154·2021-09-02 15:11
閱讀 889·2019-08-29 15:37
閱讀 1731·2019-08-29 13:24
閱讀 2273·2019-08-29 11:28