爲了避免臨界區的競態條件發生,有多種手段可以達到目的。
-
阻塞式的解決方案:synchronized,Lock
-
非阻塞式的解決方案:原子變量
此次介紹使用阻塞式的解決方案:synchronized,來解決上述問題,即俗稱的【對象鎖】,它採用互斥的方式讓同一時刻至多隻有一個線程能持有【對象鎖】,其它線程再想獲取這個【對象鎖】時就會阻塞住。這樣就能保證擁有鎖的線程可以安全的執行臨界區內的代碼,不用擔心線程上下文切換
注意
雖然 java 中互斥和同步都可以採用 synchronized 關鍵字來完成,但它們還是有區別的:
互斥是保證臨界區的競態條件發生,同一時刻只能有一個線程執行臨界區代碼
同步是由於線程執行的先後、順序不同、需要一個線程等待其它線程運行到某個點
synchronized 只能鎖對象
1、synchronized
語法
synchronized(對象) // 線程1獲得對象後, 線程2不能獲得此對象,陷入阻塞(blocked)
{
臨界區
}
解決上述問題(兩個線程對初始值爲 0 的靜態變量一個做自增,一個做自減,各做 5000 次,結果是 0 嗎?)
static int counter = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",counter);
}
輸出,可以嘗試運行多次,結果都是0。
2、分析
如下圖所示
你可以做這樣的類比:
-
synchronized(對象)
中的對象,可以想象爲一個房間(room),有唯一入口(門)房間只能一次進入一人進行計算,線程 t1,t2 想象成兩個人 -
當線程 t1 執行到
synchronized(room)
時就好比 t1 進入了這個房間,並鎖住了門拿走了鑰匙,在門內執行count++
代碼 -
這時候如果 t2 也運行到了
synchronized(room)
時,它發現門被鎖住了,只能在門外等待,發生了上下文切換,阻塞住了 -
這中間即使 t1 的 cpu 時間片不幸用完,被踢出了門外(不要錯誤理解爲鎖住了對象就能一直執行下去哦),這時門還是鎖住的,t1 仍拿着鑰匙,t2 線程還在阻塞狀態進不來,只有下次輪到 t1 自己再次獲得時間片時才能開門進入
-
當 t1 執行完
synchronized{}
塊內的代碼,這時候纔會從 obj 房間出來並解開門上的鎖,喚醒 t2 線程把鑰匙給他。t2 線程這時纔可以進入 obj 房間,鎖住了門拿上鑰匙,執行它的count--
代碼
用圖來表示
3、思考
synchronized 實際是用對象鎖保證了臨界區內代碼的原子性,臨界區內的代碼對外是不可分割的,不會被線程切換所打斷。
爲了加深理解,請思考下面的問題
-
如果把
synchronized(obj)
放在 for 循環的外面,如何理解?-- 原子性,意味者鎖住了整個循環。 -
如果 t1
synchronized(obj1)
而 t2synchronized(obj2)
會怎樣運作?-- 鎖對象,要保護共享資源,必須對同一個對象進行加鎖 -
如果 t1
synchronized(obj)
而 t2 沒有加會怎麼樣?如何理解?-- 鎖對象,t2沒有鎖,就不會被阻塞,就會正常執行。要保護共享資源,所有線程都必須要枷鎖。
4、面向對象改進
把需要保護的共享變量放入一個類
class Room {
int value = 0;
public void increment() {
synchronized (this) {
value++;
}
}
public void decrement() {
synchronized (this) { // 保護自己的對象
value--;
}
}
public int get() {
synchronized (this) {
return value;
}
}
}
@Slf4j
public class Test1 {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
room.increment();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
room.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("count: {}" , room.get());
}
}
5、方法上的 synchronized
加在成員方法上
class Test{
public synchronized void test() {
}
}
等價於
class Test{
public void test() {
synchronized(this) {
}
}
}
加在靜態方法上
class Test{
public synchronized static void test() {
}
}
等價於
class Test{
public static void test() {
synchronized(Test.class) { // 鎖住類對象
}
}
}
不加 synchronzied 的方法就好比不遵守規則的人,不去老實排隊(好比翻窗戶進去的)