Java并發基礎 - synchronized篇
在Java并發編程中,synchronized和volatile是兩個非常重要的關鍵字,它們可以用來控制并發中的互斥性與可見性,本文我們先來看看在并發環境下,synchronized應該如何使用,以及它能夠如何保證互斥性與可見性。
在正式開始之前,我們首先來看一下互斥性和可見性的概念:
互斥性:即在同一時間只允許一個線程持有某個對象鎖,通過這種特性來實現多線程中的協調機制,這樣在同一時間只有一個線程對需同步的代碼塊(復合操作)進行訪問。互斥性我們也往往稱為操作的原子性。
可見性:必須確保在鎖被釋放之前,對共享變量所做的修改,對于隨后獲得該鎖的另一個線程是可見的(即在獲得鎖時應獲得最新共享變量的值),否則另一個線程可能是在本地緩存的某個副本上繼續操作從而引起不一致。
我們知道synchronized關鍵字是用來控制線程同步的,在多線程的環境下,使用synchronized能夠控制代碼不被多個線程同時執行,來看看它的具體使用。
1、同步非靜態方法
被修飾的方法稱為同步方法,這時的鎖是當前類的實例對象。
a、多個線程訪問相同對象的相同synchronized方法:
public class SynchronizedDemo1 { public synchronized void access() { try { System.out.println(Thread.currentThread().getName()+" start"); TimeUnit.SECONDS.sleep(2); System.out.println(Thread.currentThread().getName()+" end"); }catch (Exception e){ e.printStackTrace(); } } public static void main(String[] args) { SynchronizedDemo1 demo01=new SynchronizedDemo1(); for(int i=0;i<5;i++){ new Thread(demo01::access).start(); } } }
運行結果:
可以看出,當多個線程對同一個對象的同步方法進行操作時,只有一個線程能夠搶到鎖。在一個線程獲取了該對象的鎖后,其他的線程無法獲取該對象的鎖,需要等待線程先把這個鎖釋放掉才能訪問同步方法。
b、 多個線程訪問相同對象的不同synchronized方法:
public class SynchronizedDemo2 { public synchronized void access1() { try { System.out.println(Thread.currentThread().getName()+" in access1 start"); TimeUnit.SECONDS.sleep(5); System.out.println(Thread.currentThread().getName()+" in access1 end"); } catch (Exception e) { e.printStackTrace(); } } public synchronized void access2() { try { System.out.println(Thread.currentThread().getName()+" in access1 start"); TimeUnit.SECONDS.sleep(5); System.out.println(Thread.currentThread().getName()+" in access1 end"); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { SynchronizedDemo2 test = new SynchronizedDemo2(); new Thread(test::access1).start(); new Thread(test::access2).start(); } }
運行結果:
由此可以確認,當線程訪問synchronized修飾的任意方法時,如果當前對象被其他線程加鎖,都需要等待其他線程先把當前的對象鎖釋放掉。
c、 多個不同對象的線程訪問synchronized方法:
public class SynchronizedDemo3 { public synchronized void access1() { try { System.out.println(Thread.currentThread().getName()+" start"); TimeUnit.SECONDS.sleep(5); System.out.println(Thread.currentThread().getName()+" end"); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { final SynchronizedDemo3 test1 = new SynchronizedDemo3(); final SynchronizedDemo3 test2 = new SynchronizedDemo3(); new Thread(test1::access1).start(); new Thread(test2::access1).start(); } }
運行結果:
可以看出兩個線程同時開始執行,這時因為兩個線程屬于不同的對象,而鎖住的是類產生的實例對象,兩個線程就獲得了不同的鎖。因此,不同對象產生的線程可以同時訪問synchronized方法。
2、同步靜態方法
靜態方法是屬于類的而不屬于對象的 ,所以同樣的, synchronized修飾的靜態方法鎖定的是這個類的class對象 。
public class SynchronizedDemo4 { public synchronized static void access() { try { System.out.println(Thread.currentThread().getName()+" start"); TimeUnit.SECONDS.sleep(2); System.out.println(Thread.currentThread().getName()+" end"); }catch (Exception e){ e.printStackTrace(); } } public static void main(String[] args) { for(int i=0;i<5;i++){ new Thread(SynchronizedDemo4::access).start(); } } }
運行結果:
分析可知,當synchronized修飾靜態方法時,線程之間也發生了互斥,當一個線程訪問同步方法時,其他線程必須等待。因為當synchronized修飾靜態方法時,鎖是class對象,而不是類的實例對象。
3、同步代碼塊
被修飾的代碼塊稱為同步代碼塊,其作用的范圍是大括號括起來的代碼,這時鎖是括號中的對象。
那么為什么要使用同步代碼塊呢?在方法比較長,而需要同步的代碼只有一小部分時,如果對整段方法進行同步操作,可能會造成等待時間過長。這時我們可以使用同步代碼塊對需要同步的代碼進行包圍,而無需對整個方法進行同步。
根據鎖的對象不同,又可以分為以下兩類:
a、以對象作為鎖:
使用實例對象作為鎖,即線程需要進入被synchronized的代碼塊時,必須持有該對象鎖,而后來的線程則必須等待該對象的釋放。
//以this為例 public void accessResources() { synchronized (this) { try { TimeUnit.SECONDS.sleep(2); System.out.println(Thread.currentThread().getName() + " is running"); } catch (Exception e) { e.printStackTrace(); } } }
此處,因為this指的是當前對象,所以不能用在static方法上。
b、使用類的class對象作為鎖:
public void accessResources() { synchronized (SynchroDemo5.class) { try { TimeUnit.SECONDS.sleep(2); System.out.println(Thread.currentThread().getName() + " is running"); } catch (Exception e) { e.printStackTrace(); } } } public static void main(String[] args) { final SynchroDemo5 demo5 = new SynchroDemo5(); for (int i = 0; i < 5; i++) { new Thread(demo5::accessResources).start(); } }
此時,有該class對象的所有的對象都共同使用這一個鎖。
在當沒有明確的對象作為鎖時,只是想讓一段代碼同步時,則可以創建一個特殊的對象來充當鎖,例如創建一個Object對象。
private final Object MUTEX =new Object(); public void methodName(){ Synchronized(MUTEX ){ //TODO } }
看完了實現,那么synchronized底層的實現原理是怎樣的呢?我們分同步代碼塊與同步方法來。
原理
反編譯使用同步代碼塊的類生成的class文件:
這里使用了monitorenter和monitorexit對進入同步代碼進行了控制。
每個對象有一個監視器鎖(monitor)。當monitor被占用時就會處于鎖定狀態,線程執行monitorenter指令時嘗試獲取monitor的所有權,過程如下:
如果monitor的進入數為0,則該線程進入monitor,然后將進入數設置為1,該線程即為monitor的所有
如果線程已經占有該monitor,只是重新進入,則進入monitor的進入數加1
如果其他線程已經占用了monitor,則該線程進入阻塞狀態,直到monitor的進入數為0,再重新嘗試獲取monitor的所有權。
執行monitorexit的線程必須是monitor的持有者。指令執行時,monitor的進入數減1,如果減1后進入數為0,那線程退出monitor,不再是這個monitor的所有者,其他被這個monitor阻塞的線程可以嘗試去獲取這個monitor的所有權。
反編譯使用同步方法的類生成的class文件:
方法的同步并沒有通過指令monitorenter和monitorexit來完成,相對于普通方法,其常量池中多了ACC_SYNCHRONIZED標識符。JVM就是根據該標示符來實現方法的同步的:當方法調用時,調用指令將會檢查方法的ACC_SYNCHRONIZED訪問標志是否被設置,如果設置了,執行線程將先獲取monitor,獲取成功之后才能執行方法體,方法執行完后再釋放monitor。在方法執行期間,其他任何線程都無法再獲得同一個monitor對象。其實本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需通過字節碼來完成。
好了,這篇文章就先到這里,下一篇我們再看看volatile。
最后
覺得對您有所幫助,小伙伴們可以點個贊啊,非常感謝~
公眾號『碼農參上』,一個熱愛分享的公眾號,有趣、深入、直接,與你聊聊技術。歡迎來加Hydra好友 (- DrHydra9),圍觀朋友圈,做個之交啊。
Java 多線程
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。