四、JAVA多執行緒:執行緒安全與資料同步 (synchronized、This Monitor、Class Monitor)
本章首先從一個簡單的例子入手,講解了資料同步的概念,以及會引發資料不一致性問題的情況,然後非常詳細地介紹了synchronized關鍵字以及與其對應的JVM指令。本章的最後還分析了幾種可能引起程式進入死鎖的原因,以及如何使用工具進行診斷,執行緒安全與資料同步是執行緒中最重要也是最複雜的知識點之一,掌握好本章的內容可以使得程式在多執行緒的情況下既高效又安全的執行。
資料不一致問題分析(以叫號程式舉例)
1.號碼略過
2.號碼重複出現
3.號碼超過最大值
初識synchronized關鍵字
synchronized關鍵字可以實現一個簡單的策略來防止執行緒干擾和記憶體一致性錯誤。如果一個物件對於多個執行緒是可見的,那麼該物件的所有讀寫都將通過同步的方式進行
1.synchronized關鍵字提供了一種鎖的機制,確保共享變數的互斥訪問,防止資料不一致問題的出現
2.synchronized關鍵字包括monitor enter 和monitor exit兩個jvm指令,能保證在任何時候,任何執行緒執行到monitor enter成功之間都從主記憶體中獲取資料,而不是從快取中。在monitor exit 執行成功後,共享變數被更新後的值必須刷入主記憶體。
3.synchronized的指令嚴格準守java happens-before 規則, 一個monitor exit 指令之間必定要有一個monitor enter
synchronized關鍵字的用法
可以用於程式碼塊或者方法進行修飾,而不能用於對class以及變數進行修飾。
1.同步方法
[default|public|private|protected ] synchronized [static] type method()
程式碼示例:
public synchronized void sync() {
.....
}
public synchronized static void staticSync() {
.....
}
2.同步程式碼塊
private final Object MUTEX = new Object();
public void sync(){
synchronized(MUTEX){
.....
.....
}
}
叫號程式改寫:
public class TicketWindowRunnable implements Runnable {
private int index = 1 ;
private final static int MAX = 500;
private final static Object MUTEX = new Object();
@Override
public void run() {
synchronized (MUTEX) {
while (index <=MAX) {
System.out.println(Thread.currentThread()+" 的號碼是: " + (index++));
}
}
}
public static void main(String[] args) {
final TicketWindowRunnable task = new TicketWindowRunnable();
Thread windowThread1 = new Thread(task,"一號視窗") ;
Thread windowThread2 = new Thread(task,"二號視窗") ;
Thread windowThread3 = new Thread(task,"三號視窗") ;
Thread windowThread4 = new Thread(task,"四號視窗") ;
windowThread1.start();
windowThread2.start();
windowThread3.start();
windowThread4.start();
}
}
深入synchronized關鍵字
執行緒堆疊分析
monitorenter 計數器為0,標識monitor的lock還沒有被獲得, 某個執行緒獲得之後,立即對計數器加1.
monitorexit 釋放鎖 計數器減1.
使用synchronized需要注意的問題
1.與monitor關聯的物件不能為空
2.synchronized作用域過大
由於synchronized關鍵字存在排他性,也就是說所有的執行緒必須序列地通過synchronized保護的共享區域,
如果synchronized作用域越大,則代表著其效率越低,甚至會喪失併發優勢
public static class Task implements Runnable {
@Override
public synchronized void run (){
.................
}
}
上面的程式碼對整個執行緒的執行邏輯單元都進行了synchronized同步,從而喪失了併發能力。
synchronized關鍵字應該儘可能地只作用於共享資源(資料)的讀寫作用域
3.不同的monitor企圖鎖相同的方法
4.多個鎖的交叉,導致死鎖
多個鎖的交叉很容易引起執行緒出現死鎖的狀況,程式沒有任何錯誤輸出,就是不工作
This Monitor和Class Monitor的詳細介紹
this monitor
synchronized關鍵字修飾了同一個例項物件的兩個不同方法,只有一個方法被呼叫,另一個方法並沒有被呼叫。
synchronized關鍵字同步類的不同例項方法,爭搶的事同一個monitor的lock,而與之關聯的引用則是ThisMonitor的例項引用。
import java.util.concurrent.TimeUnit;
import static java.lang.Thread.currentThread ;
public class ThisMonitor {
public static void main(String args[]){
ThisMonitor thisMonitor = new ThisMonitor();
new Thread(thisMonitor::method1,"T1").start();
new Thread(thisMonitor::method2,"T2").start();
}
public synchronized void method1(){
System.out.println(currentThread().getName() + " enter to method 1 ");
while (true) {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void method2(){
synchronized (this){
System.out.println(currentThread().getName() + " enter to method 2 ");
while (true) {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class monitor
有兩個類方法(靜態方法)分別使用synchronized對其進行同步,只有一個方法被呼叫,另一個方法並沒有被呼叫。
package com.zl.step4;
import java.util.concurrent.TimeUnit;
import static java.lang.Thread.currentThread;
public class ClassMonitor {
public static void main(String args[]){
ClassMonitor thisMonitor = new ClassMonitor();
new Thread(ClassMonitor::method1,"T1").start();
new Thread(ClassMonitor::method2,"T2").start();
}
public static synchronized void method1(){
System.out.println(currentThread().getName() + " enter to method 1 ");
while (true) {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void method2(){
synchronized (ClassMonitor.class){
System.out.println(currentThread().getName() + " enter to method 2 ");
while (true) {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
該程式碼會有問題。
解決方式,請看下一章節。多執行緒間通訊
程式死鎖的原因以及如何診斷
1.交叉鎖可導致程式出現死鎖
執行緒A持有R1的鎖等待獲取R2的鎖,
執行緒B持有R2的鎖等待獲取R1的鎖。
2.記憶體不足
執行緒T1, 已經獲取了10M記憶體,還需要20M記憶體
執行緒T2,已經獲取了20M記憶體,還需要10M記憶體
兩個執行緒都需要等待彼此能夠釋放記憶體資源。才能繼續執行
3.一問一答的資料交換
服務端開啟某個埠,等待客戶端訪問,客戶端傳送請求立即等待接收,由於某種原因服務端錯過了客戶端的請求。
仍然在等待一問一答式的資料交換,此時服務端和客戶端都在等待著對方傳送資料
4.資料庫鎖
無論是資料庫表級別的鎖,還是行級別的鎖,比如某個執行緒執行了for update 語句退出了事務,其他執行緒訪問該資料庫時都將陷入死鎖。
5.檔案鎖
某執行緒獲得了檔案鎖,意外退出,其他讀取該檔案的執行緒也將會進入死鎖,知道系統釋放檔案控制代碼資源
6.死迴圈引起的死鎖
程式由於程式碼原因或者對某些異常處理不當,進入死迴圈。檢視執行緒堆疊資訊不會發現任何死鎖的跡象,但是程式不工作。cpu居高不下
這種死鎖一般稱為系統假死。排查較困難。
本文整理來源於:
《Java高併發程式設計詳解:多執行緒與架構設計》 --汪文君