【从入门到放弃-Java】并发编程-锁-synchronized
簡介
上篇【從入門到放棄-Java】并發編程-線程安全中,我們了解到,可以通過加鎖機制來保護共享對象,來實現線程安全。
synchronized是java提供的一種內置的鎖機制。通過synchronized關鍵字同步代碼塊。線程在進入同步代碼塊之前會自動獲得鎖,并在退出同步代碼塊時自動釋放鎖。內置鎖是一種互斥鎖。
本文來深入學習下synchronized。
使用
同步方法
同步非靜態方法
public class Synchronized {private static int count;private synchronized void add1() {count++;System.out.println(count);}public static void main(String[] args) throws InterruptedException {Synchronized sync = new Synchronized();Thread thread1 = new Thread(() -> {for (int i = 0; i< 10000; i++) {sync.add1();}});Thread thread2 = new Thread(() -> {for (int i = 0; i< 10000; i++) {sync.add1();}});thread1.start();thread2.start();Thread.sleep(1000);System.out.println(count);} }結果符合預期:synchronized作用于非靜態方法,鎖定的是實例對象,如上所示鎖的是sync對象,因此線程能夠正確的運行,count的結果總會是20000。
public class Synchronized {private static int count;private synchronized void add1() {count++;System.out.println(count);}public static void main(String[] args) throws InterruptedException {Synchronized sync = new Synchronized();Synchronized sync1 = new Synchronized();Thread thread1 = new Thread(() -> {for (int i = 0; i< 10000; i++) {sync.add1();}});Thread thread2 = new Thread(() -> {for (int i = 0; i< 10000; i++) {sync1.add1();}});thread1.start();thread2.start();Thread.sleep(1000);System.out.println(count);} }結果不符合預期:如上所示,作用于非靜態方法,鎖的是實例化對象,因此當sync和sync1同時運行時,還是會出現線程安全問題,因為鎖的是兩個不同的實例化對象。
同步靜態方法
public class Synchronized {private static int count;private static synchronized void add1() {count++;System.out.println(count);}private static synchronized void add11() {count++;System.out.println(count);}public static void main(String[] args) throws InterruptedException {Synchronized sync = new Synchronized();Synchronized sync1 = new Synchronized();Thread thread1 = new Thread(() -> {for (int i = 0; i< 10000; i++) {Synchronized.add1();}});Thread thread2 = new Thread(() -> {for (int i = 0; i< 10000; i++) {Synchronized.add11();}});thread1.start();thread2.start();Thread.sleep(1000);System.out.println(count);} }結果符合預期:鎖靜態方法時,鎖的是類對象。因此在不同的線程中調用add1和add11依然會得到正確的結果。
同步代碼塊
鎖當前實例對象
public class Synchronized {private static int count;private void add1() {synchronized (this) {count++;System.out.println(count);}}private static synchronized void add11() {count++;System.out.println(count);}public static void main(String[] args) throws InterruptedException {Synchronized sync = new Synchronized();Synchronized sync1 = new Synchronized();Thread thread1 = new Thread(() -> {for (int i = 0; i< 10000; i++) {sync.add1();}});Thread thread2 = new Thread(() -> {for (int i = 0; i< 10000; i++) {sync1.add1();}});thread1.start();thread2.start();Thread.sleep(1000);System.out.println(count);} }結果不符合預期:當synchronized同步方法塊時,鎖的是實例對象時,如上示例在不同的實例中調用此方法還是會出現線程安全問題。
鎖其它實例對象
public class Synchronized {private static int count;public String lock = new String();private void add1() {synchronized (lock) {count++;System.out.println(count);}}private static synchronized void add11() {count++;System.out.println(count);}public static void main(String[] args) throws InterruptedException {Synchronized sync = new Synchronized();Synchronized sync1 = new Synchronized();Thread thread1 = new Thread(() -> {for (int i = 0; i< 10000; i++) {sync.add1();}});Thread thread2 = new Thread(() -> {for (int i = 0; i< 10000; i++) {sync1.add1();}});thread1.start();thread2.start();Thread.sleep(1000);System.out.println(count);System.out.println(sync.lock == sync1.lock);} }結果不符合預期:當synchronized同步方法塊時,鎖的是其它實例對象時,如上示例在不同的實例中調用此方法還是會出現線程安全問題。
public class Synchronized {private static int count;public String lock = "";private void add1() {synchronized (lock) {count++;System.out.println(count);}}private static synchronized void add11() {count++;System.out.println(count);}public static void main(String[] args) throws InterruptedException {Synchronized sync = new Synchronized();Synchronized sync1 = new Synchronized();Thread thread1 = new Thread(() -> {for (int i = 0; i< 10000; i++) {sync.add1();}});Thread thread2 = new Thread(() -> {for (int i = 0; i< 10000; i++) {sync1.add1();}});thread1.start();thread2.start();Thread.sleep(1000);System.out.println(count);System.out.println(sync.lock == sync1.lock);} }結果符合預期:當synchronized同步方法塊時,鎖的雖然是其它實例對象時,但已上實例中,因為String = "" 是存放在常量池中的,實際上鎖的還是相同的對象,因此是線程安全的
鎖類對象
public class Synchronized {private static int count;private void add1() {synchronized (Synchronized.class) {count++;System.out.println(count);}}private static synchronized void add11() {count++;System.out.println(count);}public static void main(String[] args) throws InterruptedException {Synchronized sync = new Synchronized();Synchronized sync1 = new Synchronized();Thread thread1 = new Thread(() -> {for (int i = 0; i< 10000; i++) {sync.add1();}});Thread thread2 = new Thread(() -> {for (int i = 0; i< 10000; i++) {sync1.add1();}});thread1.start();thread2.start();Thread.sleep(1000);System.out.println(count);} }結果符合預期:當synchronized同步方法塊時,鎖的是類對象時,如上示例在不同的實例中調用此方法是線程安全的。
鎖機制
public class Synchronized {private static int count;public static void main(String[] args) throws InterruptedException {synchronized (Synchronized.class) {count++;}} }使用javap -v Synchronized.class反編譯class文件。
?
可以看到synchronized實際上是通過monitorenter和monitorexit來實現鎖機制的。同一時刻,只能有一個線程進入監視區。從而保證線程的同步。
正常情況下在指令4進入監視區,指令14退出監視區然后指令15直接跳到指令23 return
但是在異常情況下異常都會跳轉到指令18,依次執行到指令20monitorexit釋放鎖,防止出現異常時未釋放的情況。
這其實也是synchronized的優點:無論代碼執行情況如何,都不會忘記主動釋放鎖。
想了解Monitors更多的原理可以點擊查看
鎖升級
因為monitor依賴操作系統的Mutex lock實現,是一個比較重的操作,需要切換系統至內核態,開銷非常大。因此在jdk1.6引入了偏向鎖和輕量級鎖。
synchronized有四種狀態:無鎖 -> 偏向鎖 -> 輕量級鎖 -> 重量級鎖。
無鎖
沒有對資源進行鎖定,所有線程都能訪問和修改。但同時只有一個線程能修改成功
偏向鎖
在鎖競爭不強烈的情況下,通常一個線程會多次獲取同一個鎖,為了減少獲取鎖的代價 引入了偏向鎖,會在java對象頭中記錄獲取鎖的線程的threadID。
- 當線程發現對象頭的threadID存在時。判斷與當前線程是否是同一線程。
- 如果是則不需要再次加、解鎖。
- 如果不是,則判斷threadID是否存活。不存活:設置為無鎖狀態,其他線程競爭設置偏向鎖。存活:查找threadID堆棧信息判斷是否需要繼續持有鎖。需要持有則升級threadID線程的鎖為輕量級鎖。不需要持有則撤銷鎖,設置為無鎖狀態等待其它線程競爭。
因為偏向鎖的撤銷操作還是比較重的,導致進入安全點,因此在競爭比較激烈時,會影響性能,可以使用-XX:-UseBiasedLocking=false禁用偏向鎖。
輕量級鎖
當偏向鎖升級為輕量級鎖時,其它線程嘗試通過CAS方式設置對象頭來獲取鎖。
- 會先在當前線程的棧幀中設置Lock Record,用于存儲當前對象頭中的mark word的拷貝。
- 復制mark word的內容到lock record,并嘗試使用cas將mark word的指針指向lock record
- 如果替換成功,則獲取偏向鎖
- 替換不成功,則會自旋重試一定次數。
- 自旋一定次數或有新的線程來競爭鎖時,輕量級鎖膨脹為重量級鎖。
CAS
CAS即compare and swap(比較并替換)。是一種樂觀鎖機制。通常有三個值
- V:內存中的實際值
- A:舊的預期值
- B:要修改的新值
即V與A相等時,則替換V為B。即內存中的實際值與我們的預期值相等時,則替換為新值。
CAS可能遇到ABA問題,即內存中的值為A,變為B后,又變為了A,此時A為新值,不應該替換。
可以采取:A-1,B-2,A-3的方式來避免這個問題
重量級鎖
自旋是消耗CPU的,因此在自旋一段時間,或者一個線程在自旋時,又有新的線程來競爭鎖,則輕量級鎖會膨脹為重量級鎖。
重量級鎖,通過monitor實現,monitor底層實際是依賴操作系統的mutex lock(互斥鎖)實現。
需要從用戶態,切換為內核態,成本比較高
總結
本文我們一起學習了
- synchronized的幾種用法:同步方法、同步代碼塊。實際上是同步類或同步實例對象。
- 鎖升級:無鎖、偏向鎖、輕量級鎖、重量級鎖以及其膨脹過程。
synchronized作為內置鎖,雖然幫我們解決了線程安全問題,但是帶來了性能的損失,因此一定不能濫用。使用時請注意同步塊的作用范圍。通常,作用范圍越小,對性能的影響也就越小(注意權衡獲取、釋放鎖的成本,不能為了縮小作用范圍,而頻繁的獲取、釋放)。
原文鏈接
本文為云棲社區原創內容,未經允許不得轉載。
總結
以上是生活随笔為你收集整理的【从入门到放弃-Java】并发编程-锁-synchronized的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Kubernetes-native 弹性
- 下一篇: 移动研发 DevOps 落地实践