一、同步的基本概念
1、同步的場景
執行緒獲取同步鎖,獲取失敗則阻塞等待,獲取成功則執行任務,執行完畢後釋放鎖。2、執行緒安全問題
(1)記憶體讀取
- cpu在記憶體讀取資料時,順序優先順序:暫存器-快取記憶體-記憶體
- 計算完成後快取資料寫回記憶體中
(2)可見性
- 每個執行緒都有獨立的工作記憶體,並對其他執行緒是不可見的。執行緒執行如用到某變數,將變數從主記憶體複製到工作記憶體,對變數操作完成後再將變數寫回主記憶體。
- 可見性就是指執行緒A對某變數操作後,其他執行緒能夠看到被修改的值,可以通過volidate等方式實現可見性。
(3)原子性
對於基本型別的賦值操作是原子操作,即這些操作是不可被中斷的,要麼執行,要麼不執行。
x = 10; //語句1(原子性操作)
y = x; //語句2
x++; //語句3
複製程式碼
- 語句1直接將10賦值給x,會直接將10寫入到工作記憶體
- 語句2需要先讀x,然後將x寫入工作記憶體,然後賦值,不是原子性操作
- 語句3需要先讀x,然後+1,然後賦值。
(4)有序性
- Java記憶體模型中允許編譯器和處理器對指令進行重排序,雖然重排序過程不會影響到單執行緒執行的正確性,但是會影響到多執行緒併發執行的正確性。
- 可以通過volidate來保證有序性,synchronized和Lock保證每個時刻只有一個執行緒執行同步程式碼,這相當於是讓執行緒順序執行同步程式碼,從而保證了有序性。
(5)執行緒安全問題的原因
執行緒A和執行緒B需要將共享變數拷貝到本地記憶體中,並對各自的共享變數副本進行操作,操作完成後同步到主記憶體中,可能導致共享記憶體資料錯亂的問題。二、synchronized
1、基本特性:
- synchronized可以用於修飾類的例項方法、靜態方法和程式碼塊
- 可重入性:對同一個執行執行緒,它在獲得了鎖之後,在呼叫其他需要同樣鎖的程式碼時,可以直接呼叫。
- 記憶體可見性:在釋放鎖時,所有寫入都會寫回記憶體,而獲得鎖後,都會從記憶體中讀最新資料。
- 當一個執行緒訪問object的一個synchronized(this)同步程式碼塊時,另一個執行緒仍然可以訪問該object中的非synchronized(this)同步程式碼塊。
2、類鎖和物件鎖
public class SyncTest {
private static int num;
public void setClassText() {
//類鎖
synchronized (SyncTest.class) {
System.out.println("類鎖開始執行");
++num;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("類鎖執行完成");
}
}
public static synchronized void setObject1Text() {
//類鎖
System.out.println("類鎖2開始執行");
++num;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("類鎖2執行完成");
}
public synchronized void setObject1Text() {
//物件鎖
System.out.println("物件鎖1開始執行");
++num;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("物件鎖1執行完成");
}
public void setObject2Text() {
//物件鎖
synchronized(this) {
System.out.println("物件鎖2開始執行");
++num;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("物件鎖2執行完成");
}
}
}
複製程式碼
注:
- 執行緒A持有物件鎖,不會影響到執行緒B去持有類鎖
- 執行緒A和B操作同一個物件鎖,執行緒A持有物件鎖,執行緒B只能等到該物件鎖釋放
- synchronized如果修飾static方法(類鎖),不會影響到非static(物件鎖)修飾的方法的呼叫
3、wait、notify
- void notifyAll( ) 解除那些在該物件上呼叫 wait 方法的執行緒的阻塞狀態。該方法只能在同步方法或同步塊內部呼叫。 如果當前執行緒不是物件鎖的持有者,該方法丟擲一個IllegalMonitorStateException異常。
- void notify() 隨機選擇一個在該物件上呼叫 wait 方法的執行緒,解除其阻塞狀態。 該方法只能在一個同步方法或同步塊中呼叫。 如果當前執行緒不是物件鎖的持有者, 該方法丟擲一個 IllegalMonitorStateException 異常。
- void wait(long millis) 導致執行緒進人等待狀態直到它被通知。該方法只能在一個同步方法中呼叫。如果當前執行緒不是物件鎖的持有者,該方法丟擲一個 IllegalMonitorStateException 異常。
- void wait(long millis, int nanos) 導致執行緒進入等待狀態直到它被通知或者經過指定的時間。 這些方法只能在一個同步方法中呼叫。如果當前執行緒不是物件鎖的持有者該方法丟擲一個 IllegalMonitorStateException 異常。
注意:
- 呼叫notify會把在條件佇列中等待的執行緒喚醒並從佇列中移除,但它不會釋放物件鎖,只有在包含notify的synchronzied程式碼塊執行完後,等待的執行緒才會從wait呼叫中返回。
- 呼叫wait把當前執行緒放入條件等待佇列,釋放物件鎖。等待時間到或被其他執行緒呼叫notify/notifyAll從條件佇列中移除,此時要重新競爭物件鎖。
三、鎖物件
鎖可以理解為進入某個門的鑰匙,兩個人都想進入門內,A持有鑰匙可進入,B只能等待。A放下了鑰匙,B才有機會獲取到鑰匙進入。
- 鎖用於保護程式碼片段,只有一個執行緒執行被保護的程式碼
- 需要保證是同一個鎖物件
- 需要通過呼叫unlock去釋放鎖
- 鎖可以擁有一個或多個相關的條件物件
1、Lock
public interface Lock {
/**
* 獲取鎖,如果鎖被另一執行緒擁有則發生阻塞
*/
void lock();
/**
* 嘗試獲取鎖,立即返回不阻塞
* @return 獲取成功返回true
*/
boolean tryLock();
/**
* 嘗試獲取鎖,如果成功立即返回,否則阻塞等待,阻塞時間不會超 * 過給定的值
* @return 獲取成功返回true
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
* 釋放鎖
*/
void unlock();
/**
* 獲取一個與該鎖相關的條件
* @return
*/
Condition newCondition();
......
}
複製程式碼
synchronized和Lock對比:
- 鎖可以以非阻塞方式獲取鎖、可以響應中斷、可以限時
- synchronized會自動釋放鎖,而Lock一定要求程式設計師手工釋放
2、ReentrantLock:
(1)構造:
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* 構建一個公平策略的鎖,公平鎖偏愛等待時候最長的執行緒,會降低效能
* @param fair
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
複製程式碼
(2)條件物件
public Condition newCondition() {
......
}
複製程式碼
一個鎖物件可以有一個或多個條件物件,通過newCondition獲取一個條件物件
(3)await、signalAll
- void await( ) 將該執行緒放到條件的等待集中。
- void signalAll( ) 解除該條件的等待集中的所有執行緒的阻塞狀態。
- void signal() 從該條件的等待集中隨機地選擇一個執行緒, 解除其阻塞狀態。
注意:
- await()對應於Object的wait(),signal()對應於notify,signalAll()對應於notifyAll()
- 呼叫await、signalAll等方法前需要先獲取鎖,如果沒有鎖,會丟擲異常IllegalMonitorStateException
3、讀寫鎖:
特性:
只要沒有任何執行緒寫入變數,併發讀取可變變數通常是安全的。所以讀鎖可以同時被多個執行緒持有,只要沒有執行緒持有寫鎖。這樣可以提升效能和吞吐量,因為讀取比寫入更加頻繁。
使用場景:多執行緒操作同一檔案,讀操作比較多,寫操作比較少的情況。
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
//獲取讀、寫鎖
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
//對讀的操作使用讀鎖
readLock.lock();
try {
......
} finally {
readLock.unlock();
}
//對寫的操作使用寫鎖
writeLock.lock();
try {
......
} finally {
writeLock.unlock();
}
複製程式碼
四、Volidate域
1、應用場景:
- 如果宣告一個域為 volatile, 它會保證修改的值會立即被更新到主存,當有其他執行緒需要讀取時,它會去記憶體中讀取新值。
- 對變數的寫操作不會依賴於當前值。(不保證原子性)
- 該變數沒有包含在具有其他變數的不變式中
特性:
- 保證了不同執行緒對這個變數進行操作時的可見性,即一個執行緒修改了某個變數的值,這新值對其他執行緒來說是立即可見的。
- 禁止進行指令重排序。
- Volatile變數不能提供原子性
例如
public class VolatileTest {
volatile boolean flag;
public void finish() {
flag = true;
}
public void begin() {
flag =false;
}
}
複製程式碼
通過volatile修飾保證標識位在多執行緒中的可見性
2、原理:
- 它確保指令重排序時不會把其後面的指令排到被volidate修飾的變數之前,也不會把前面的指令排到該變數的後面;
- 它會強制將對快取的修改操作立即寫入主存;
- 如果是寫操作,它會導致其他CPU中對應的快取行無效。
3、synchronized和volatile比較:
- volatile是執行緒同步的輕量級實現,並且volatile只能修飾於變數,而synchronized可以修飾方法,以及程式碼塊。
- 多執行緒訪問volatile不會發生阻塞,而synchronized會出現阻塞
- volatile能保證資料的可見性,但不能保證原子性;而synchronized可以保證原子性
五、死鎖
有a, b兩個執行緒,a持有鎖A,在等待鎖B,而b持有鎖B,在等待鎖A,a,b陷入了互相等待,最後誰都執行不下去。
public class DeadLockDemo {
private static Object lockA = new Object();
private static Object lockB = new Object();
private static void startThreadA() {
Thread aThread = new Thread() {
@Override
public void run() {
synchronized (lockA) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
synchronized (lockB) {
}
}
}
};
aThread.start();
}
private static void startThreadB() {
Thread bThread = new Thread() {
@Override
public void run() {
synchronized (lockB) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
synchronized (lockA) {
}
}
}
};
bThread.start();
}
public static void main(String[] args) {
startThreadA();
startThreadB();
}
}
複製程式碼
解決方式:
- 應該儘量避免在持有一個鎖的同時去申請另一個鎖,如果確實需要多個鎖,所有程式碼都應該按照相同的順序去申請鎖
- 顯式鎖介面Lock,它支援嘗試獲取鎖(tryLock)和帶時間限制的獲取鎖方法,可以在獲取不到鎖的時候釋放已經持有的鎖,然後再次嘗試獲取鎖或乾脆放棄,以避免死鎖。