synchronized原理與使用
1. synchronized作用
能夠保證原子性的Atomic變量和保證可見性、有序性的 volatile關鍵字是輕量級的實現併發同步的方式,但是都存在着一定的侷限,具體參考博文。
synchronized是重量級的同步實現方式,synchronized 作用域中的代碼是同步執行的。併發的情況下,執行到對同一個對象加鎖的 synchronized 代碼塊時,多線程會轉爲串行執行的。這裏注意,不是同一個同步代碼塊,而是對同一個對象上鎖的同步代碼塊。這意味着範圍更廣。
此外 JMM關於synchronized的兩條規定:
- 線程解鎖前,必須把共享變量的最新值刷新到主內存中
- 線程加鎖時,將清空工作內存中共享變量的值,從而使用共享變量時需要從主內存中重新獲取最新的值
由此可見synchronized 可以確保可見性,在一個線程執行完 synchronized 代碼後,所有代碼中對變量值的變化都能立即被其它線程所看到。
2. synchronized使用
同步代碼塊
以下面的代碼爲例,假設存在一個隊列對象 tasks,裏面存放着需要執行的任務(即Runnable的實現類),
// 消費者run方法中的同步代碼塊
synchronized (tasks) {
if (tasks.size() > 0) {
task = tasks.removeFirst();
sleep(100);
tasks.notifyAll();
} else {
tasks.wait();
}
}
// 生產者run方法中的同步代碼塊
synchronized (tasks) {
if (tasks.size() < MAX) {
Task task = new Task(new Random().nextInt(3) + 1, getPunishedWord());
tasks.addLast(task);
System.out.println(threadName + "留了作業,抄寫" + task.getWordToCopy() + " " + task.getLeftCopyCount() + "次");
tasks.notifyAll();
} else {
System.out.println(threadName+"開始等待");
tasks.wait();
System.out.println("teacher線程 " + threadName + "線程-" + name + "等待結束");
}
}
synchronized (tasks)
,這行代碼小括號裏的 tasks 對象和 synchronized 實現的方式相關。小括號裏的對象是可以是任意的對象。這個對象相當於是同步代碼塊的看門人,每個對其 synchronized 的線程,它都會記錄下來,然後等到同步代碼塊沒有線程執行的時候,它就會通知其它線程來執行同步代碼塊。所以並不是括號內的對象加鎖,只是讓它來維護秩序。
例子中,併發的線程並不是同樣類型的 Thread(因爲存在生產者和消費者兩種線程),一個是 Student,還一個是 Teacher。對於不同線程對象的同步控制,一定要選用兩種線程都持有的對象纔行。否則各自使用不同的對象,相當於聘用了兩個看門人,各看各的門,毫無瓜葛。那麼原本想要串行執行的代碼仍舊會並行執行。
同步方法
對於非靜態方法,
public synchronized void eat(){
.......
.......
}
同步方法的鎖對象是this,等效於下面的代碼塊,
public void eat(){
synchronized(this){
.......
.......
}
}
對於靜態方法,
public static synchronized void eat(){
.......
.......
}
此時同步方法爲類的 Class 對象。如果上述靜態方法所在的類爲 Test。那麼鎖對象就是 Test.class。
synchronized使用總結
- 選用一個鎖對象,可以是任意對象
- 鎖對象鎖住的是同步代碼塊,而不是自己
- 不同類型的線程如果執行相同的同步代碼,鎖對象要使用所有線程共同持有的一個對象
- sychronized代碼塊可以同時滿足併發的三個特性
3. synchronized原理
synchronized 的祕密其實都在同步對象上,這個對象就是一個看門人,每次只允許一個線程進來,進門後此線程可以做任何自己想做的事情,然後再出來。線程出來後,看門人會提示其它線程,總有個敏捷值最高的線程先衝入門內,那麼其它線程只好繼續等待。
每個對象Java都關聯了一個 monitor lock。當一個線程獲取了 monitor lock 後,其它線程如果運行到獲取同一個 monitor 的時候就會被 block 住。當這個線程執行完同步代碼,則會釋放 monitor lock。在後一個線程獲取鎖後,happens-before 原則生效,前一個線程所做的任何修改都會被這個線程看到。
再深入底層一點,每個 Java 對象在 JVM 的對等對象的頭中保存鎖狀態,指向 ObjectMonitor
。該模式是JVM內部基於C++實現的,模式圖如下,
- ObjectMonitor 保存了當前持有鎖的線程引用
- EntryList 中保存目前等待獲取鎖的線程,WaitSet 保存 wait 的線程
- 此外還有一個計數器,每當線程獲得 monitor 鎖,計數器 +1,當線程重入此鎖時,計數器還會 +1。當計數器不爲0時,其它嘗試獲取 monitor 鎖的線程將會被保存到EntryList中,並被阻塞
- 當持有鎖的線程釋放了monitor 鎖後,計數器 -1。當計數器歸位爲 0 時,所有 EntryList 中的線程會嘗試去獲取鎖,但只會有一個線程會成功,沒有成功的線程仍舊保存在 EntryList 中。
當一個線程需要獲取 Object 的鎖時,會被放入 EntrySet 中進行等待,如果該線程獲取到了鎖,成爲當前鎖的 owner。如果根據程序邏輯,一個已經獲得了鎖的線程缺少某些外部條件,而無法繼續進行下去(例如生產者發現隊列已滿或者消費者發現隊列爲空),那麼該線程可以通過調用 wait 方法將鎖釋放,進入 wait set 中阻塞進行等待,其它線程在這個時候有機會獲得鎖,去幹其它的事情,從而使得之前不成立的外部條件成立,這樣先前被阻塞的線程就可以重新進入 EntrySet 去競爭鎖。這個外部條件在 monitor 機制中稱爲條件變量。引自博文
4. synchronized注意事項
- synchronized 使用的爲非公平鎖,如果需要公平鎖,可以使用 ReentrantLock,設置爲公平鎖
- 鎖對象不能爲 null。如果鎖對象爲 null,就不存在與其關聯的 monitor 鎖
- 只把需要同步的代碼放入 synchronized 代碼塊。如果不思考,爲了線程安全把方法中全部代碼都放入同步代碼塊,那麼將會喪失多線程的優勢。再多的線程也只能串行執行,這完全違背了併發的初衷
- 只有使用同一個對象作爲鎖對象,才能同步。是同一個對象,而不是同一個類。有一種常犯的錯誤是,不同線程持有的是同一個類的不同實例。那麼該對象實例用作鎖對象的話,多個線程並不會同步。還一種錯誤是使用不同類的實例作爲鎖對象,但是期望不同位置的同步代碼塊能夠同步執行