4 共享模型之管程
本章內容
- 共享問題
- synchronized
- 線程安全分析
- Monitor
- wait/notify
- 線程狀態轉換
- 活躍性
- Lock
4.1 共享帶來的問題
4.1.1 小故事
- 老王(操作系統)有一個功能強大的算盤(CPU),現在想把它租出去,賺一點外快
-
小南、小女(線程)來使用這個算盤來進行一些計算,并按照時間給老王支付費用
-
但小南不能一天24小時使用算盤,他經常要小憩一會(sleep),又或是去吃飯上廁所(阻塞 io 操作),有時還需要一根煙,沒煙時思路全無(wait)這些情況統稱為(阻塞)
-
在這些時候,算盤沒利用起來(不能收錢了),老王覺得有點不劃算
-
另外,小女也想用用算盤,如果總是小南占著算盤,讓小女覺得不公平
-
于是,老王靈機一動,想了個辦法 [ 讓他們每人用一會,輪流使用算盤 ]
-
這樣,當小南阻塞的時候,算盤可以分給小女使用,不會浪費,反之亦然
-
最近執行的計算比較復雜,需要存儲一些中間結果,而學生們的腦容量(工作內存)不夠,所以老王申請了一個筆記本(主存),把一些中間結果先記在本上
-
計算流程是這樣的
-
但是由于分時系統,有一天還是發生了事故
-
小南剛讀取了初始值 0 做了個 +1 運算,還沒來得及寫回結果
-
老王說 [ 小南,你的時間到了,該別人了,記住結果走吧 ],于是小南念叨著 [ 結果是1,結果是1…] 不甘心地到一邊待著去了(上下文切換)
-
老王說 [ 小女,該你了 ],小女看到了筆記本上還寫著 0 做了一個 -1 運算,將結果 -1 寫入筆記本
-
這時小女的時間也用完了,老王又叫醒了小南:[小南,把你上次的題目算完吧],小南將他腦海中的結果 1 寫入了筆記本
- 小南和小女都覺得自己沒做錯,但筆記本里的結果是 1 而不是 0
4.1.2 Java 的體現
兩個線程對初始值為 0 的靜態變量一個做自增,一個做自減,各做 5000 次,結果是 0 嗎?
static int counter = 0;
public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {counter++;}}, "t1");Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {counter--;}}, "t2");t1.start();t2.start();t1.join();t2.join();log.debug("{}",counter);
}
4.1.3 問題分析
以上的結果可能是正數、負數、零。為什么呢?因為 Java 中對靜態變量的自增,自減并不是原子操作,要徹底理解,必須從字節碼來進行分析
例如對于 i++
而言(i 為靜態變量),實際會產生如下的 JVM 字節碼指令:
getstatic i // 獲取靜態變量i的值
iconst_1 // 準備常量1
iadd // 自增
putstatic i // 將修改后的值存入靜態變量i
而對應i--
也是類似:
getstatic i // 獲取靜態變量i的值
iconst_1 // 準備常量1
isub // 自減
putstatic i // 將修改后的值存入靜態變量i
而 Java 的內存模型如下,完成靜態變量的自增,自減需要在主存和工作內存中進行數據交換:
如果是單線程以上 8 行代碼是順序執行(不會交錯)沒有問題:
但多線程下這 8 行代碼可能交錯運行:
出現正數的情況:
臨界區 Critical Section
-
一個程序運行多個線程本身是沒有問題的
-
問題出在多個線程訪問共享資源
- 多個線程讀共享資源其實也沒有問題
- 在多個線程對共享資源讀寫操作時發生指令交錯,就會出現問題
-
一段代碼塊內如果存在對共享資源的多線程讀寫操作,稱這段代碼塊為臨界區
例如,下面代碼中的臨界區
static int counter = 0;
static void increment()
// 臨界區
{counter++;
}static void decrement()
// 臨界區
{counter--;
}
競態條件 Race Condition
多個線程在臨界區內執行,由于代碼的執行序列不同而導致結果無法預測,稱之為發生了競態條件
4.2 synchronized 解決方案
4.2.1 應用之互斥
為了避免臨界區的競態條件發生,有多種手段可以達到目的。
-
阻塞式的解決方案:synchronized,Lock
-
非阻塞式的解決方案:原子變量
本次課使用阻塞式的解決方案:synchronized,來解決上述問題,即俗稱的【對象鎖】,它采用互斥的方式讓同一時刻至多只有一個線程能持有【對象鎖】,其它線程再想獲取這個【對象鎖】時就會阻塞住。這樣就能保證擁有鎖的線程可以安全的執行臨界區內的代碼,不用擔心線程上下文切換
注意
雖然 java 中互斥和同步都可以采用 synchronized 關鍵字來完成,但它們還是有區別的:
- 互斥是保證臨界區的競態條件發生,同一時刻只能有一個線程執行臨界區代碼
- 同步是由于線程執行的先后、順序不同、需要一個線程等待其它線程運行到某個點
4.2.2 synchronized
語法
synchronized(對象) // 線程1, 線程2(blocked)
{臨界區
}
解決
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);
}
你可以做這樣的類比:
-
synchronized(對象)
中的對象,可以想象為一個房間(room),有唯一入口(門)房間只能一次進入一人進行計算,線程 t1,t2 想象成兩個人 -
當線程 t1 執行到
synchronized(room)
時就好比 t1 進入了這個房間,并鎖住了門拿走了鑰匙,在門內執行count++
代碼 -
這時候如果 t2 也運行到了
synchronized(room)
時,它發現門被鎖住了,只能在門外等待,發生了上下文切換,阻塞住了 -
這中間即使 t1 的 cpu 時間片不幸用完,被踢出了門外(不要錯誤理解為鎖住了對象就能一直執行下去哦),這時門還是鎖住的,t1 仍拿著鑰匙,t2 線程還在阻塞狀態進不來,只有下次輪到 t1 自己再次獲得時間片時才能開門進入
-
當 t1 執行完
synchronized{}
塊內的代碼,這時候才會從 obj 房間出來并解開門上的鎖,喚醒 t2 線程把鑰匙給他。t2 線程這時才可以進入 obj 房間,鎖住了門拿上鑰匙,執行它的count--
代碼.
用圖表示
思考
synchronized 實際是用對象鎖保證了臨界區內代碼的原子性,臨界區內的代碼對外是不可分割的,不會被線程切換所打斷。
為了加深理解,請思考下面的問題
- 如果把
synchronized(obj)
放在 for 循環的外面,如何理解?-- 原子性 - 如果 t1
synchronized(obj1)
而t2 synchronized(obj2)
會怎樣運作?-- 鎖對象 - 如果 t1
synchronized(obj)
而 t2 沒有加會怎么樣?如何理解?-- 鎖對象
4.2.3 面向對象改進
把需要保護的共享變量放入一個類
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());}
}
4.3 方法上的 synchronized
4.3.1 與使用synchronized(X)等效
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) {}}
}
4.3.2 不加 synchronized 的方法
不加 synchronzied 的方法就好比不遵守規則的人,不去老實排隊(好比翻窗戶進去的)
4.3.3 所謂的“線程八鎖”
其實就是考察 synchronized 鎖住的是哪個對象
情況1:12 或 21
@Slf4j(topic = "c.Number")
class Number{public synchronized void a() {log.debug("1");}public synchronized void b() {log.debug("2");}
}public static void main(String[] args) {Number n1 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n1.b(); }).start();
}
情況2:1s后12,或 2 1s后 1
@Slf4j(topic = "c.Number")
class Number{public synchronized void a() {sleep(1);log.debug("1");}public synchronized void b() {log.debug("2");}
}public static void main(String[] args) {Number n1 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n1.b(); }).start();
}
情況3:3 1s 12 或 23 1s 1 或 32 1s 1
@Slf4j(topic = "c.Number")
class Number{public synchronized void a() {sleep(1);log.debug("1");}public synchronized void b() {log.debug("2");}public void c() {log.debug("3");}
}public static void main(String[] args) {Number n1 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n1.b(); }).start();new Thread(()->{ n1.c(); }).start();
}
情況4:2 1s 后 1
@Slf4j(topic = "c.Number")
class Number{public synchronized void a() {sleep(1);log.debug("1");}public synchronized void b() {log.debug("2");}
}public static void main(String[] args) {Number n1 = new Number();Number n2 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n2.b(); }).start();
}
情況5:2 1s 后 1
@Slf4j(topic = "c.Number")
class Number{public static synchronized void a() {sleep(1);log.debug("1");}public synchronized void b() {log.debug("2");}
}public static void main(String[] args) {Number n1 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n1.b(); }).start();
}
情況6:1s 后12, 或 2 1s后 1
@Slf4j(topic = "c.Number")
class Number{public static synchronized void a() {sleep(1);log.debug("1");}public static synchronized void b() {log.debug("2");}
}public static void main(String[] args) {Number n1 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n1.b(); }).start();
}
情況7:2 1s 后 1
@Slf4j(topic = "c.Number")
class Number{public static synchronized void a() {sleep(1);log.debug("1");}public synchronized void b() {log.debug("2");}
}public static void main(String[] args) {Number n1 = new Number();Number n2 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n2.b(); }).start();
}
情況8:1s 后12, 或 2 1s后 1
@Slf4j(topic = "c.Number")
class Number{public static synchronized void a() {sleep(1);log.debug("1");}public static synchronized void b() {log.debug("2");}
}public static void main(String[] args) {Number n1 = new Number();Number n2 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n2.b(); }).start();
}
4.4 變量的線程安全分析
成員變量和靜態變量是否線程安全?
-
如果它們沒有共享,則線程安全
-
如果它們被共享了,根據它們的狀態是否能夠改變,又分兩種情況
- 如果只有讀操作,則線程安全
- 如果有讀寫操作,則這段代碼是臨界區,需要考慮線程安全
局部變量是否線程安全?
-
局部變量是線程安全的
-
但局部變量引用的對象則未必
- 如果該對象沒有逃離方法的作用訪問,它是線程安全的
- 如果該對象逃離方法的作用范圍,需要考慮線程安全
4.4.1 局部變量線程安全分析
public static void test1() {int i = 10;i++;
}
每個線程調用 test1() 方法時局部變量 i,會在每個線程的棧幀內存中被創建多份,因此不存在共享
public static void test1();descriptor: ()Vflags: ACC_PUBLIC, ACC_STATICCode:stack=1, locals=1, args_size=00: bipush 102: istore_03: iinc 0, 16: returnLineNumberTable:line 10: 0line 11: 3line 12: 6LocalVariableTable:Start Length Slot Name Signature3 4 0 i I
如圖
局部變量的引用稍有不同
先看一個成員變量的例子
class ThreadUnsafe {ArrayList<String> list = new ArrayList<>();public void method1(int loopNumber) {for (int i = 0; i < loopNumber; i++) {// { 臨界區, 會產生競態條件method2();method3();// } 臨界區}}private void method2() {list.add("1");}private void method3() {list.remove(0);}
}
執行
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {ThreadUnsafe test = new ThreadUnsafe();for (int i = 0; i < THREAD_NUMBER; i++) {new Thread(() -> {test.method1(LOOP_NUMBER);}, "Thread" + i).start();}
}
其中一種情況是,如果線程2 還未 add,線程1 remove 就會報錯:
Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 at java.util.ArrayList.rangeCheck(ArrayList.java:657) at java.util.ArrayList.remove(ArrayList.java:496) at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35) at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26) at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14) at java.lang.Thread.run(Thread.java:748)
t1 add之后準備將size記為1但還沒記的時候被 t2搶走,此時size仍未0
t2 add操作,并成功將size記為1,然后又被t1搶回, t1 繼續未完操作,再次將size記為1,這時又被t2搶走
t2 繼續操作,remove之后,size記為0,然后又被t1搶走
此時t1再去remove時發現size為0,就報了異常
這個發生的概率還是極低的…
分析:
-
無論哪個線程中的 method2 引用的都是同一個對象中的 list 成員變量
-
method3 與 method2 分析相同
將 list 修改為局部變量
class ThreadSafe {public final void method1(int loopNumber) {ArrayList<String> list = new ArrayList<>();for (int i = 0; i < loopNumber; i++) {method2(list);method3(list);}}private void method2(ArrayList<String> list) {list.add("1");}private void method3(ArrayList<String> list) {list.remove(0);}
}
那么就不會有上述問題了
分析:
-
list 是局部變量,每個線程調用時會創建其不同實例,沒有共享
-
而 method2 的參數是從 method1 中傳遞過來的,與 method1 中引用同一個對象
-
method3 的參數分析與 method2 相同
方法訪問修飾符帶來的思考,如果把 method2 和 method3 的方法修改為 public 會不會出現線程安全問題?
-
情況1:有其它線程調用 method2 和 method3
-
情況2:在 情況1 的基礎上,為 ThreadSafe 類添加子類,子類覆蓋 method2 或 method3 方法,即
class ThreadSafe {public final void method1(int loopNumber) {ArrayList<String> list = new ArrayList<>();for (int i = 0; i < loopNumber; i++) {method2(list);method3(list);}}private void method2(ArrayList<String> list) {list.add("1");}private void method3(ArrayList<String> list) {list.remove(0);}
}class ThreadSafeSubClass extends ThreadSafe{@Overridepublic void method3(ArrayList<String> list) {new Thread(() -> {list.remove(0);}).start();}
}
從這個例子可以看出 private 或 final 提供【安全】的意義所在,請體會開閉原則中的【閉】
4.4.2 常見線程安全類
- String
- Integer
- StringBuffffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的類
這里說它們是線程安全的是指,多個線程調用它們同一個實例的某個方法時,是線程安全的。也可以理解為
Hashtable table = new Hashtable();new Thread(()->{table.put("key", "value1");
}).start();new Thread(()->{table.put("key", "value2");
}).start();
-
它們的每個方法是原子的
-
但注意它們多個方法的組合不是原子的,見后面分析
4.4.3 線程安全類方法的組合
分析下面代碼是否線程安全?
Hashtable table = new Hashtable();
// 線程1,線程2
if( table.get("key") == null) {table.put("key", value);
}
4.4.4 不可變類線程安全性
String、Integer 等都是不可變類,因為其內部的狀態不可以改變,因此它們的方法都是線程安全的
有同學或許有疑問,String 有 replace,substring 等方法【可以】改變值啊,那么這些方法又是如何保證線程安全的呢?
public class Immutable{private int value = 0;public Immutable(int value){this.value = value;}public int getValue(){return this.value;}
}
如果想增加一個增加的方法呢?
public class Immutable{private int value = 0;public Immutable(int value){this.value = value;}public int getValue(){return this.value;}public Immutable add(int v){return new Immutable(this.value + v);}
}
實例分析
例1:
public class MyServlet extends HttpServlet {// 是否安全? 不安全Map<String,Object> map = new HashMap<>();// 是否安全? 安全String S1 = "...";// 是否安全? 安全final String S2 = "...";// 是否安全? 不安全Date D1 = new Date();// 是否安全? 不安全final Date D2 = new Date();public void doGet(HttpServletRequest request, HttpServletResponse response) {// 使用上述變量}
}
例2:
public class MyServlet extends HttpServlet {// 是否安全? 不安全private UserService userService = new UserServiceImpl();public void doGet(HttpServletRequest request, HttpServletResponse response) {userService.update(...);}
}public class UserServiceImpl implements UserService {// 記錄調用次數private int count = 0;public void update() {// ...count++;}
}
例3:
@Aspect
@Component
public class MyAspect {// 是否安全? 不安全private long start = 0L;@Before("execution(* *(..))")public void before() {start = System.nanoTime();}@After("execution(* *(..))")public void after() {long end = System.nanoTime();System.out.println("cost time:" + (end-start));}
}
例4
public class MyServlet extends HttpServlet {// 是否安全 安全private UserService userService = new UserServiceImpl();public void doGet(HttpServletRequest request, HttpServletResponse response) {userService.update(...);}
}public class UserServiceImpl implements UserService {// 是否安全 安全private UserDao userDao = new UserDaoImpl();public void update() {userDao.update();}
}public class UserDaoImpl implements UserDao {public void update() {String sql = "update user set password = ? where username = ?";// 是否安全 安全try (Connection conn = DriverManager.getConnection("","","")){// ...} catch (Exception e) {// ...}}
}
例5
public class MyServlet extends HttpServlet {// 是否安全private UserService userService = new UserServiceImpl();public void doGet(HttpServletRequest request, HttpServletResponse response) {userService.update(...);}
}public class UserServiceImpl implements UserService {// 是否安全private UserDao userDao = new UserDaoImpl();public void update() {userDao.update();}
}public class UserDaoImpl implements UserDao {// 是否安全 private Connection conn = null;public void update() throws SQLException {String sql = "update user set password = ? where username = ?";conn = DriverManager.getConnection("","","");// ...conn.close();}
}
例6
public class MyServlet extends HttpServlet {// 是否安全private UserService userService = new UserServiceImpl();public void doGet(HttpServletRequest request, HttpServletResponse response) {userService.update(...);}
}public class UserServiceImpl implements UserService {public void update() {UserDao userDao = new UserDaoImpl();userDao.update();}
}public class UserDaoImpl implements UserDao {// 是否安全private Connection = null;public void update() throws SQLException {String sql = "update user set password = ? where username = ?";conn = DriverManager.getConnection("","","");// ...conn.close();}
}
例7
public abstract class Test {public void bar() {// 是否安全SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");foo(sdf);}public abstract foo(SimpleDateFormat sdf);public static void main(String[] args) {new Test().bar();}}
其中 foo 的行為是不確定的,可能導致不安全的發生,被稱之為外星方法
public void foo(SimpleDateFormat sdf) {String dateStr = "1999-10-11 00:00:00";for (int i = 0; i < 20; i++) {new Thread(() -> {try {sdf.parse(dateStr);} catch (ParseException e) {e.printStackTrace();}}).start();}
}
請比較 JDK 中 String 類的實現
例8
private static Integer i = 0;
public static void main(String[] args) throws InterruptedException {List<Thread> list = new ArrayList<>();for (int j = 0; j < 2; j++) {Thread thread = new Thread(() -> {for (int k = 0; k < 5000; k++) {synchronized (i) {i++;}}}, "" + j);list.add(thread);}list.stream().forEach(t -> t.start());list.stream().forEach(t -> {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}});log.debug("{}", i);
}
4.5 習題
4.5.1 賣票練習
測試下面代碼是否存在線程安全問題,并嘗試改正
public class ExerciseSell {public static void main(String[] args) {TicketWindow ticketWindow = new TicketWindow(2000);List<Thread> list = new ArrayList<>();// 用來存儲買出去多少張票List<Integer> sellCount = new Vector<>();for (int i = 0; i < 2000; i++) {Thread t = new Thread(() -> {// 分析這里的競態條件int count = ticketWindow.sell(randomAmount());sellCount.add(count);});list.add(t);t.start();}list.forEach((t) -> {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}});// 賣出去的票求和log.debug("selled count:{}",sellCount.stream().mapToInt(c -> c).sum());// 剩余票數log.debug("remainder count:{}", ticketWindow.getCount());}// Random 為線程安全static Random random = new Random();// 隨機 1~5public static int randomAmount() {return random.nextInt(5) + 1;}
}class TicketWindow {private int count;public TicketWindow(int count) {this.count = count;}public int getCount() {return count;}public int sell(int amount) {if (this.count >= amount) {this.count -= amount;return amount;} else {return 0;}}
}
另外,用下面的代碼行不行,為什么?
List<Integer> sellCount = new ArrayList<>();
測試腳本
for /L %n in (1,1,10) do java -cp ".;C:\Users\manyh\.m2\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;C:\Users\manyh\.m2\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;C:\Users\manyh\.m2\repository\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar" cn.itcast.n4.exercise.ExerciseSell
4.5.2 轉賬練習
測試下面代碼是否存在線程安全問題,并嘗試改正
public class ExerciseTransfer {public static void main(String[] args) throws InterruptedException {Account a = new Account(1000);Account b = new Account(1000);Thread t1 = new Thread(() -> {for (int i = 0; i < 1000; i++) {a.transfer(b, randomAmount());}}, "t1");Thread t2 = new Thread(() -> {for (int i = 0; i < 1000; i++) {b.transfer(a, randomAmount());}}, "t2");t1.start();t2.start();t1.join();t2.join();// 查看轉賬2000次后的總金額log.debug("total:{}",(a.getMoney() + b.getMoney()));}// Random 為線程安全static Random random = new Random();// 隨機 1~100public static int randomAmount() {return random.nextInt(100) +1;}
}class Account {private int money;public Account(int money) {this.money = money;}public int getMoney() {return money;}public void setMoney(int money) {this.money = money;}public void transfer(Account target, int amount) {if (this.money > amount) {this.setMoney(this.getMoney() - amount);target.setMoney(target.getMoney() + amount);}}
}
這樣改正行不行,為什么? 不行,鎖要用同一個共用鎖,而這里的account是不同對象
public synchronized void transfer(Account target, int amount) {if (this.money > amount) {this.setMoney(this.getMoney() - amount);target.setMoney(target.getMoney() + amount);}
}
4.6 Monitor 概念
4.6.1 Java 對象頭
以 32 位虛擬機為例
普通對象
數組對象
其中 Mark Word 結構為
64 位虛擬機 Mark Word
參考資料
https://stackoverflow.com/questions/26357186/what-is-in-java-object-header
4.6.2 原理之 Monitor(鎖)
Monitor 被翻譯為監視器或管程
每個 Java 對象都可以關聯一個 Monitor 對象,如果使用 synchronized 給對象上鎖(重量級)之后,該對象頭的Mark Word 中就被設置指向 Monitor 對象的指針
Monitor 結構
- 剛開始 Monitor 中 Owner 為 null
- 當 Thread-2 執行 synchronized(obj) 就會將 Monitor 的所有者 Owner 置為 Thread-2,Monitor中只能有一個 Owner
- 在 Thread-2 上鎖的過程中,如果 Thread-3,Thread-4,Thread-5 也來執行 synchronized(obj),就會進入EntryList BLOCKED
- Thread-2 執行完同步代碼塊的內容,然后喚醒 EntryList 中等待的線程來競爭鎖,競爭的時是非公平的
- 圖中 WaitSet 中的 Thread-0,Thread-1 是之前獲得過鎖,但條件不滿足進入 WAITING 狀態的線程,后面講wait-notify 時會分析
注意:
- synchronized 必須是進入同一個對象的 monitor 才有上述的效果
- 不加 synchronized 的對象不會關聯監視器,不遵從以上規則
4.6.3 原理之 synchronized
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {synchronized (lock) {counter++;}
}
對應的字節碼為
注意
方法級別的 synchronized 不會在字節碼指令中有所體現
小故事
故事角色
- 老王 - JVM
- 小南 - 線程
- 小女 - 線程
- 房間 - 對象
- 房間門上 - 防盜鎖 - Monitor
- 房間門上 - 小南書包 - 輕量級鎖
- 房間門上 - 刻上小南大名 - 偏向鎖
- 批量重刻名 - 一個類的偏向鎖撤銷到達 20 閾值
- 不能刻名字 - 批量撤銷該類對象的偏向鎖,設置該類不可偏向
小南要使用房間保證計算不被其它人干擾(原子性),最初,他用的是防盜鎖,當上下文切換時,鎖住門。這樣即使他離開了,別人也進不了門,他的工作就是安全的。
但是,很多情況下沒人跟他來競爭房間的使用權。小女是要用房間,但使用的時間上是錯開的,小南白天用,小女晚上用。每次上鎖太麻煩了,有沒有更簡單的辦法呢?
小南和小女商量了一下,約定不鎖門了,而是誰用房間,誰把自己的書包掛在門口,但他們的書包樣式都一樣,因此每次進門前得翻翻書包,看課本是誰的,如果是自己的,那么就可以進門,這樣省的上鎖解鎖了。萬一書包不是自己的,那么就在門外等,并通知對方下次用鎖門的方式。
后來,小女回老家了,很長一段時間都不會用這個房間。小南每次還是掛書包,翻書包,雖然比鎖門省事了,但仍然覺得麻煩。
于是,小南干脆在門上刻上了自己的名字:【小南專屬房間,其它人勿用】,下次來用房間時,只要名字還在,那么說明沒人打擾,還是可以安全地使用房間。如果這期間有其它人要用這個房間,那么由使用者將小南刻的名字擦掉,升級為掛書包的方式。
同學們都放假回老家了,小南就膨脹了,在 20 個房間刻上了自己的名字,想進哪個進哪個。后來他自己放假回老家了,這時小女回來了(她也要用這些房間),結果就是得一個個地擦掉小南刻的名字,升級為掛書包的方式。老王覺得這成本有點高,提出了一種批量重刻名的方法,他讓小女不用掛書包了,可以直接在門上刻上自己的名字
后來,刻名的現象越來越頻繁,老王受不了了:算了,這些房間都不能刻名了,只能掛書包
4.6.4 原理之 synchronized 進階
輕量級鎖
輕量級鎖的使用場景:如果一個對象雖然有多線程要加鎖,但加鎖的時間是錯開的(也就是沒有競爭),那么可以使用輕量級鎖來優化。
輕量級鎖對使用者是透明的,即語法仍然是 synchronized
假設有兩個方法同步塊,利用同一個對象加鎖
static final Object obj = new Object();public static void method1() {synchronized( obj ) {// 同步塊 Amethod2();}
}public static void method2() {synchronized( obj ) {// 同步塊 B}
}
- 創建 鎖記錄(Lock Record)對象,每個線程的棧幀都會包含一個鎖記錄的結構,內部可以存儲鎖定對象的Mark Word
- 讓鎖記錄中 Object reference 指向鎖對象,并嘗試用 cas 替換 Object 的 Mark Word,將 Mark Word 的值存入鎖記錄
- 如果 cas 替換成功,對象頭中存儲了
鎖記錄地址和狀態 00
,表示由該線程給對象加鎖,這時圖示如下
- 如果 cas 失敗,有兩種情況
- 如果是其它線程已經持有了該 Object 的輕量級鎖,這時表明有競爭,進入鎖膨脹過程
- 如果是自己執行了 synchronized 鎖重入,那么再添加一條 Lock Record 作為重入的計數
- 當退出 synchronized 代碼塊(解鎖時)如果有取值為 null 的鎖記錄,表示有重入,這時重置鎖記錄,表示重入計數減一
- 當退出 synchronized 代碼塊(解鎖時)鎖記錄的值不為 null,這時使用 cas 將 Mark Word 的值恢復給對象頭
- 成功,則解鎖成功
- 失敗,說明輕量級鎖進行了鎖膨脹或已經升級為重量級鎖,進入重量級鎖解鎖流程
鎖膨脹
如果在嘗試加輕量級鎖的過程中,CAS 操作無法成功,這時一種情況就是有其它線程為此對象加上了輕量級鎖(有競爭),這時需要進行鎖膨脹,將輕量級鎖變為重量級鎖。
static Object obj = new Object();
public static void method1() {synchronized( obj ) {// 同步塊}
}
- 當 Thread-1 進行輕量級加鎖時,Thread-0 已經對該對象加了輕量級鎖
- 這時 Thread-1 加輕量級鎖失敗,進入鎖膨脹流程
- 即為 Object 對象申請 Monitor 鎖,讓 Object 指向重量級鎖地址
- 然后自己進入 Monitor 的 EntryList BLOCKED
- 當 Thread-0 退出同步塊解鎖時,使用 cas 將 Mark Word 的值恢復給對象頭,失敗。這時會進入重量級解鎖流程,即按照 Monitor 地址找到 Monitor 對象,設置 Owner 為 null,喚醒 EntryList 中 BLOCKED 線程
自旋優化
重量級鎖競爭的時候,還可以使用自旋(循環嘗試獲取重量級鎖)來進行優化,如果當前線程自旋成功(即這時候持鎖線程已經退出了同步塊,釋放了鎖),這時當前線程就可以避免阻塞。 (進入阻塞再恢復,會發生上下文切換,比較耗費性能)
自旋重試成功的情況
自旋重試失敗的情況
- 自旋會占用 CPU 時間,單核 CPU 自旋就是浪費,多核 CPU 自旋才能發揮優勢。
- 在 Java 6 之后自旋鎖是自適應的,比如對象剛剛的一次自旋操作成功過,那么認為這次自旋成功的可能性會高,就多自旋幾次;反之,就少自旋甚至不自旋,總之,比較智能。
- Java 7 之后不能控制是否開啟自旋功能
偏向鎖
輕量級鎖在沒有競爭時(就自己這個線程),每次重入仍然需要執行 CAS 操作。
Java 6 中引入了偏向鎖來做進一步優化:只有第一次使用 CAS 將線程 ID 設置到對象的 Mark Word 頭,之后發現這個線程 ID 是自己的就表示沒有競爭,不用重新 CAS。以后只要不發生競爭,這個對象就歸該線程所有
這里的線程id是操作系統賦予的id 和 Thread的id是不同的
例如:
static final Object obj = new Object();public static void m1() {synchronized( obj ) {// 同步塊 Am2();}
}public static void m2() {synchronized( obj ) {// 同步塊 Bm3();}
}public static void m3() {synchronized( obj ) {// 同步塊 C}
}
偏向狀態
回憶一下對象頭格式
一個對象創建時:
-
如果開啟了偏向鎖(默認開啟),那么對象創建后,markword 值為 0x05 即最后 3 位為 101,這時它的 thread、epoch、age 都為 0
-
偏向鎖是默認是延遲的,不會在程序啟動時立即生效,如果想避免延遲,可以加 VM 參數
-XX:BiasedLockingStartupDelay=0
來禁用延遲
-
如果沒有開啟偏向鎖,那么對象創建后,markword 值為 0x01 即最后 3 位為 001,這時它的 hashcode、age 都為 0,第一次用到 hashcode 時才會賦值
1) 測試延遲特性
偏向鎖是默認是延遲的,不會在程序啟動時立即生效,如果想避免延遲,可以加 VM 參數 -XX:BiasedLockingStartupDelay=0
來禁用延遲
2) 測試偏向鎖
class Dog {}
利用 jol 第三方工具來查看對象頭信息(注意這里up主擴展了 jol 讓它輸出更為簡潔)
public static void main(String[] args) throws IOException {Dog d = new Dog();ClassLayout classLayout = ClassLayout.parseInstance(d);new Thread(() -> {log.debug("synchronized 前");System.out.println(classLayout.toPrintableSimple(true));synchronized (d) {log.debug("synchronized 中");System.out.println(classLayout.toPrintableSimple(true));}log.debug("synchronized 后");System.out.println(classLayout.toPrintableSimple(true));}, "t1").start();
}
輸出
11:08:58.117 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
11:08:58.121 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101
11:08:58.121 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101
注意
處于偏向鎖的對象解鎖后,線程 id 仍存儲于對象頭中
也就是偏(心)向某個線程了
3)測試禁用
在上面測試代碼運行時在添加 VM 參數 -XX:-UseBiasedLocking 禁用偏向鎖
輸出
11:13:10.018 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
11:13:10.021 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00100000 00010100 11110011 10001000
11:13:10.021 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
**4)測試hashCode **
在Dog d = new Dog();
后加上一句 d.hashCode();
-
正常狀態對象一開始是沒有 hashCode 的,第一次調用才生成
-
調用了 hashCode() 后會撤銷該對象的偏向鎖
撤銷(偏向) - 調用對象 hashCode
調用了對象的 hashCode,但偏向鎖的對象 MarkWord 中存儲的是線程 id,如果調用 hashCode 會導致偏向鎖被撤銷
-
輕量級鎖會在鎖記錄中記錄 hashCode
-
重量級鎖會在 Monitor 中記錄 hashCode
記得去掉 -XX:-UseBiasedLocking
在調用 hashCode 后使用偏向鎖,
輸出
11:22:10.386 c.TestBiased [main] - 調用 hashCode:1778535015
11:22:10.391 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001
11:22:10.393 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00100000 11000011 11110011 01101000
11:22:10.393 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001
撤銷(偏向) - 其它線程(錯開)使用對象
當有其它線程使用偏向鎖對象時,會將偏向鎖升級為輕量級鎖
private static void test2() throws InterruptedException {Dog d = new Dog();Thread t1 = new Thread(() -> {log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));synchronized (d) {log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));}log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));synchronized (TestBiased.class) {TestBiased.class.notify();}// 如果不用 wait/notify 使用 join 必須打開下面的注釋// 因為:t1 線程不能結束,否則底層線程可能被 jvm 重用作為 t2 線程,底層線程 id 是一樣的/*try {System.in.read();} catch (IOException e) {e.printStackTrace();}*/}, "t1");t1.start();Thread t2 = new Thread(() -> {synchronized (TestBiased.class) {try {TestBiased.class.wait();} catch (InterruptedException e) {e.printStackTrace();}}log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));synchronized (d) {log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));}log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));}, "t2");t2.start();
}
輸出
撤銷(偏向) - 調用 wait/notify
重量級鎖才支持 wait/notify
public static void main(String[] args) throws InterruptedException {Dog d = new Dog();Thread t1 = new Thread(() -> {log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));synchronized (d) {log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));try {d.wait();} catch (InterruptedException e) {e.printStackTrace();}log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));}}, "t1");t1.start();new Thread(() -> {try {Thread.sleep(6000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (d) {log.debug("notify");d.notify();}}, "t2").start();
}
輸出
[t1] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
[t1] - 00000000 00000000 00000000 00000000 00011111 10110011 11111000 00000101
[t2] - notify
[t1] - 00000000 00000000 00000000 00000000 00011100 11010100 00001101 11001010
批量重偏向
如果對象雖然被多個線程訪問,但沒有競爭,這時偏向了線程 T1 的對象仍有機會重新偏向 T2,重偏向會重置對象的 Thread ID
當(某類型對象)撤銷偏向鎖閾值超過 20 次后,jvm 會這樣覺得,我是不是偏向錯了呢,于是會在給(所有這種類型的狀態為偏向鎖的)對象加鎖時重新偏向至新的加鎖線程
private static void test3() throws InterruptedException {Vector<Dog> list = new Vector<>();Thread t1 = new Thread(() -> {for (int i = 0; i < 30; i++) {Dog d = new Dog();list.add(d);synchronized (d) {log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));}}synchronized (list) {list.notify();}}, "t1");t1.start();Thread t2 = new Thread(() -> {synchronized (list) {try {list.wait();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("===============> ");for (int i = 0; i < 30; i++) {Dog d = list.get(i);log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));synchronized (d) {log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));}log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));}}, "t2");t2.start();
}
輸出
[t1] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 2 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 3 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 4 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 5 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 6 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 7 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 8 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 9 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 10 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 11 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 12 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 13 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 14 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 15 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 16 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 17 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - ===============>
[t2] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 0 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 0 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 1 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 1 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 2 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 2 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 2 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 3 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 3 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 3 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 4 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 4 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 4 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 5 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 5 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 5 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 6 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 6 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 6 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 7 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 7 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 7 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 8 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 8 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 8 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 9 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 9 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 9 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 10 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 10 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 10 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 11 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 11 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 11 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 12 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 12 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 12 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 13 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 13 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 13 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 14 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 14 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 14 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 15 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 15 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 15 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 16 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 16 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 16 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 17 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 17 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 17 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 18 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 18 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
批量撤銷(偏向)
當撤銷偏向鎖閾值超過 40 次后,jvm 會這樣覺得,自己確實偏向錯了,根本就不該偏向。于是整個類的所有對象都會變為不可偏向的,新建的該類型對象也是不可偏向的
static Thread t1,t2,t3;
private static void test4() throws InterruptedException {Vector<Dog> list = new Vector<>();int loopNumber = 39;t1 = new Thread(() -> {for (int i = 0; i < loopNumber; i++) {Dog d = new Dog();list.add(d);synchronized (d) {log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));}}LockSupport.unpark(t2);}, "t1");t1.start();t2 = new Thread(() -> {LockSupport.park();log.debug("===============> ");for (int i = 0; i < loopNumber; i++) {Dog d = list.get(i);log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));synchronized (d) {log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));}log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));}LockSupport.unpark(t3);}, "t2");t2.start();t3 = new Thread(() -> {LockSupport.park();log.debug("===============> ");for (int i = 0; i < loopNumber; i++) {Dog d = list.get(i);log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));synchronized (d) {log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));}log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));}}, "t3");t3.start();t3.join();log.debug(ClassLayout.parseInstance(new Dog()).toPrintableSimple(true));
}
參考資料
https://github.com/farmerjohngit/myblog/issues/12https://www.cnblogs.com/LemonFive/p/11246086.html
https://www.cnblogs.com/LemonFive/p/11248248.html
偏向鎖論文: https://www.oracle.com/technetwork/java/biasedlocking-oopsla2006-wp-149958.pdf
鎖消除
鎖消除 JIT即時編譯器會對字節碼做進一步優化
@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations=3)
@Measurement(iterations=5)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark {static int x = 0;@Benchmarkpublic void a() throws Exception {x++;}@Benchmarkpublic void b() throws Exception {//這里的o是局部變量,不會被共享,JIT做熱點代碼優化時會做鎖消除Object o = new Object();synchronized (o) {x++;}}
}
java -jar benchmarks.jar
發現兩部分的差別并不大,甚至b加了鎖比a沒加鎖還快
java -XX:-EliminateLocks -jar benchmarks.jar
使用 -XX:-EliminateLocks
禁用鎖消除后就會發現 b性能比a差勁多了
鎖粗化 (up沒有找到真正能證明鎖粗化的例子,所以沒講)
對相同對象多次加鎖,導致線程發生多次重入,可以使用鎖粗化方式來優化,這不同于之前講的細分鎖的粒度。
4.7 wait / notify
4.7.1 小故事 - 為什么需要 wait
-
由于條件不滿足,小南不能繼續進行計算
-
但小南如果一直占用著鎖,其它人就得一直阻塞,效率太低
- 于是老王單開了一間休息室(調用 wait 方法),讓小南到休息室(WaitSet)等著去了,但這時鎖釋放開,其它人可以由老王隨機安排進屋
- 直到小M將煙送來,大叫一聲 [ 你的煙到了 ] (調用 notify 方法)
- 小南于是可以離開休息室,重新進入競爭鎖的隊列
4.7.2 原理之 wait / notify
- Owner 線程發現條件不滿足,調用 wait 方法,即可進入 WaitSet 變為 WAITING 狀態
- BLOCKED 和 WAITING 的線程都處于阻塞狀態,不占用 CPU 時間片
- BLOCKED 線程會在 Owner 線程釋放鎖時喚醒
- WAITING 線程會在 Owner 線程調用 notify 或 notifyAll 時喚醒,但喚醒后并不意味者立刻獲得鎖,仍需進入EntryList 重新競爭
4.7.3 API 介紹
-
obj.wait()
讓進入 object 監視器的線程到 waitSet 等待 -
obj.notify()
在 object 上正在 waitSet 等待的線程中挑一個喚醒 -
obj.notifyAll()
讓 object 上正在 waitSet 等待的線程全部喚醒
它們都是線程之間進行協作的手段,都屬于 Object 對象的方法。必須獲得此對象的鎖,才能調用這幾個方法
final static Object obj = new Object();public static void main(String[] args) {new Thread(() -> {synchronized (obj) {log.debug("執行....");try {obj.wait(); // 讓線程在obj上一直等待下去} catch (InterruptedException e) {e.printStackTrace();}log.debug("其它代碼....");}}).start();new Thread(() -> {synchronized (obj) {log.debug("執行....");try {obj.wait(); // 讓線程在obj上一直等待下去} catch (InterruptedException e) {e.printStackTrace();}log.debug("其它代碼....");}}).start();// 主線程兩秒后執行sleep(2);log.debug("喚醒 obj 上其它線程");synchronized (obj) {obj.notify(); // 喚醒obj上一個線程// obj.notifyAll(); // 喚醒obj上所有等待線程}
}
notify 的一種結果
20:00:53.096 [Thread-0] c.TestWaitNotify - 執行....
20:00:53.099 [Thread-1] c.TestWaitNotify - 執行....
20:00:55.096 [main] c.TestWaitNotify - 喚醒 obj 上其它線程
20:00:55.096 [Thread-0] c.TestWaitNotify - 其它代碼....
notifyAll 的結果
19:58:15.457 [Thread-0] c.TestWaitNotify - 執行....
19:58:15.460 [Thread-1] c.TestWaitNotify - 執行....
19:58:17.456 [main] c.TestWaitNotify - 喚醒 obj 上其它線程
19:58:17.456 [Thread-1] c.TestWaitNotify - 其它代碼....
19:58:17.456 [Thread-0] c.TestWaitNotify - 其它代碼....
wait()
方法會釋放對象的鎖,進入 WaitSet 等待區,從而讓其他線程就機會獲取對象的鎖。無限制等待,直到notify 為止
wait(long n)
有時限的等待, 到 n 毫秒后結束等待,或是被 notify
4.8 wait notify 的正確姿勢
開始之前先看看
sleep(long n)
和 wait(long n)
的區別
- sleep 是 Thread 方法,而 wait 是 Object 的方法
- sleep 不需要強制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
- sleep 在睡眠的同時,不會釋放對象鎖的,但 wait 在等待的時候會釋放對象鎖
- 它們狀態 TIMED_WAITING
4.8.1 step/例 1 : sleep會阻礙其它線程執行
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
思考下面的解決方案好不好,為什么?
new Thread(() -> {synchronized (room) {log.debug("有煙沒?[{}]", hasCigarette);if (!hasCigarette) {log.debug("沒煙,先歇會!");sleep(2);}log.debug("有煙沒?[{}]", hasCigarette);if (hasCigarette) {log.debug("可以開始干活了");}}
}, "小南").start();for (int i = 0; i < 5; i++) {new Thread(() -> {synchronized (room) {log.debug("可以開始干活了");}}, "其它人").start();
}sleep(1);
new Thread(() -> {// 這里能不能加 synchronized (room)? 不能hasCigarette = true;log.debug("煙到了噢!");
}, "送煙的").start();
輸出
20:49:49.883 [小南] c.TestCorrectPosture - 有煙沒?[false]
20:49:49.887 [小南] c.TestCorrectPosture - 沒煙,先歇會!
20:49:50.882 [送煙的] c.TestCorrectPosture - 煙到了噢!
20:49:51.887 [小南] c.TestCorrectPosture - 有煙沒?[true]
20:49:51.887 [小南] c.TestCorrectPosture - 可以開始干活了
20:49:51.887 [其它人] c.TestCorrectPosture - 可以開始干活了
20:49:51.887 [其它人] c.TestCorrectPosture - 可以開始干活了
20:49:51.888 [其它人] c.TestCorrectPosture - 可以開始干活了
20:49:51.888 [其它人] c.TestCorrectPosture - 可以開始干活了
20:49:51.888 [其它人] c.TestCorrectPosture - 可以開始干活了
-
其它干活的線程,都要一直阻塞,效率太低
-
小南線程必須睡足 2s 后才能醒來,就算煙提前送到,也無法立刻醒來
-
加了 synchronized (room) 后,就好比小南在里面反鎖了門睡覺,煙根本沒法送進門,main 沒加 synchronized 就好像 main 線程是翻窗戶進來的
-
sleep妨礙其它人干活 解決方法,使用 wait - notify
4.8.2 step/例 2 : wait替代sleep
思考下面的實現行嗎,為什么?
new Thread(() -> {synchronized (room) {log.debug("有煙沒?[{}]", hasCigarette);if (!hasCigarette) {log.debug("沒煙,先歇會!");try {room.wait(2000);} catch (InterruptedException e) {e.printStackTrace();}}log.debug("有煙沒?[{}]", hasCigarette);if (hasCigarette) {log.debug("可以開始干活了");}}
}, "小南").start();for (int i = 0; i < 5; i++) {new Thread(() -> {synchronized (room) {log.debug("可以開始干活了");}}, "其它人").start();
}sleep(1);
new Thread(() -> {synchronized (room) {hasCigarette = true;log.debug("煙到了噢!");room.notify();}
}, "送煙的").start();
輸出
20:51:42.489 [小南] c.TestCorrectPosture - 有煙沒?[false]
20:51:42.493 [小南] c.TestCorrectPosture - 沒煙,先歇會!
20:51:42.493 [其它人] c.TestCorrectPosture - 可以開始干活了
20:51:42.493 [其它人] c.TestCorrectPosture - 可以開始干活了
20:51:42.494 [其它人] c.TestCorrectPosture - 可以開始干活了
20:51:42.494 [其它人] c.TestCorrectPosture - 可以開始干活了
20:51:42.494 [其它人] c.TestCorrectPosture - 可以開始干活了
20:51:43.490 [送煙的] c.TestCorrectPosture - 煙到了噢!
20:51:43.490 [小南] c.TestCorrectPosture - 有煙沒?[true]
20:51:43.490 [小南] c.TestCorrectPosture - 可以開始干活了
-
解決了其它干活的線程阻塞的問題
-
但如果有其它線程也在等待條件呢?
4.8.3 step/例 3 : 會發生虛假喚醒*
new Thread(() -> {synchronized (room) {log.debug("有煙沒?[{}]", hasCigarette);if (!hasCigarette) {log.debug("沒煙,先歇會!");try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("有煙沒?[{}]", hasCigarette);if (hasCigarette) {log.debug("可以開始干活了");} else {log.debug("沒干成活...");}}
}, "小南").start();new Thread(() -> {synchronized (room) {Thread thread = Thread.currentThread();log.debug("外賣送到沒?[{}]", hasTakeout);if (!hasTakeout) {log.debug("沒外賣,先歇會!");try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("外賣送到沒?[{}]", hasTakeout);if (hasTakeout) {log.debug("可以開始干活了");} else {log.debug("沒干成活...");}}
}, "小女").start();sleep(1);
new Thread(() -> {synchronized (room) {hasTakeout = true;log.debug("外賣到了噢!");room.notify();}
}, "送外賣的").start();
輸出
20:53:12.173 [小南] c.TestCorrectPosture - 有煙沒?[false]
20:53:12.176 [小南] c.TestCorrectPosture - 沒煙,先歇會!
20:53:12.176 [小女] c.TestCorrectPosture - 外賣送到沒?[false]
20:53:12.176 [小女] c.TestCorrectPosture - 沒外賣,先歇會!
20:53:13.174 [送外賣的] c.TestCorrectPosture - 外賣到了噢!
20:53:13.174 [小南] c.TestCorrectPosture - 有煙沒?[false]
20:53:13.174 [小南] c.TestCorrectPosture - 沒干成活...
-
notify 只能隨機喚醒一個 WaitSet 中的線程,這時如果有其它線程也在等待,那么就可能喚醒不了正確的線程,稱之為【虛假喚醒】
-
發生虛假喚醒: 解決方法,改為 notifyAll
4.8.4 step/例 4 : if+wait 僅由1次判斷機會
new Thread(() -> {synchronized (room) {hasTakeout = true;log.debug("外賣到了噢!");room.notifyAll();}
}, "送外賣的").start();
輸出
20:55:23.978 [小南] c.TestCorrectPosture - 有煙沒?[false]
20:55:23.982 [小南] c.TestCorrectPosture - 沒煙,先歇會!
20:55:23.982 [小女] c.TestCorrectPosture - 外賣送到沒?[false]
20:55:23.982 [小女] c.TestCorrectPosture - 沒外賣,先歇會!
20:55:24.979 [送外賣的] c.TestCorrectPosture - 外賣到了噢!
20:55:24.979 [小女] c.TestCorrectPosture - 外賣送到沒?[true]
20:55:24.980 [小女] c.TestCorrectPosture - 可以開始干活了
20:55:24.980 [小南] c.TestCorrectPosture - 有煙沒?[false]
20:55:24.980 [小南] c.TestCorrectPosture - 沒干成活...
-
用 notifyAll 僅解決某個線程的喚醒問題,但使用 if + wait 判斷僅有一次機會,一旦條件不成立,就沒有重新判斷的機會了
-
notifyAll喚醒了所有,但使用if+wait僅有一次機會,解決方法,一旦條件不成立,就沒有重新判斷的機會了.解決辦法: 用 while + wait,當條件不成立,再次 wait
4.8.5 step 5 : while+wait
將 if 改為 while
if (!hasCigarette) {log.debug("沒煙,先歇會!");try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}
}
改動后
while (!hasCigarette) {log.debug("沒煙,先歇會!");try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}
}
輸出
20:58:34.322 [小南] c.TestCorrectPosture - 有煙沒?[false]
20:58:34.326 [小南] c.TestCorrectPosture - 沒煙,先歇會!
20:58:34.326 [小女] c.TestCorrectPosture - 外賣送到沒?[false]
20:58:34.326 [小女] c.TestCorrectPosture - 沒外賣,先歇會!
20:58:35.323 [送外賣的] c.TestCorrectPosture - 外賣到了噢!
20:58:35.324 [小女] c.TestCorrectPosture - 外賣送到沒?[true]
20:58:35.324 [小女] c.TestCorrectPosture - 可以開始干活了
20:58:35.324 [小南] c.TestCorrectPosture - 沒煙,先歇會!
synchronized(lock) {while(條件不成立) {lock.wait();}// 干活
}//另一個線程
synchronized(lock) {lock.notifyAll();
}
4.8.6 (同步)模式之保護性暫停
- 定義
即 Guarded Suspension,用在一個線程等待另一個線程的執行結果
要點
-
有一個結果需要從一個線程傳遞到另一個線程,讓他們關聯同一個 GuardedObject
-
如果有結果不斷從一個線程到另一個線程那么可以使用消息隊列(見生產者/消費者)
-
JDK 中,join 的實現、Future 的實現,采用的就是此模式
-
因為要等待另一方的結果,因此歸類到同步模式
- 實現
class GuardedObject {private Object response;private final Object lock = new Object();public Object get() {synchronized (lock) {// 條件不滿足則等待while (response == null) {try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();} }return response; }}public void complete(Object response) {synchronized (lock) {// 條件滿足,通知等待線程this.response = response;lock.notifyAll();}}}
應用:
一個線程等待另一個線程的執行結果
public static void main(String[] args) {GuardedObject guardedObject = new GuardedObject();new Thread(() -> {try {// 子線程執行下載List<String> response = download();log.debug("download complete...");guardedObject.complete(response);} catch (IOException e) {e.printStackTrace();}}).start();log.debug("waiting...");// 主線程阻塞等待Object response = guardedObject.get();log.debug("get response: [{}] lines", ((List<String>) response).size());
}
執行結果
08:42:18.568 [main] c.TestGuardedObject - waiting...
08:42:23.312 [Thread-0] c.TestGuardedObject - download complete...
08:42:23.312 [main] c.TestGuardedObject - get response: [3] lines
- 帶超時版 GuardedObject
如果要控制超時時間呢
class GuardedObjectV2 {private Object response;private final Object lock = new Object();public Object get(long millis) {synchronized (lock) {// 1) 記錄最初時間long begin = System.currentTimeMillis();// 2) 已經經歷的時間long timePassed = 0;while (response == null) {// 4) 假設 millis 是 1000,結果在 400 時喚醒了,那么還有 600 要等long waitTime = millis - timePassed;log.debug("waitTime: {}", waitTime);if (waitTime <= 0) {log.debug("break...");break; }try {lock.wait(waitTime);} catch (InterruptedException e) {e.printStackTrace();}// 3) 如果提前被喚醒,這時已經經歷的時間假設為 400timePassed = System.currentTimeMillis() - begin;log.debug("timePassed: {}, object is null {}", timePassed, response == null);}return response; }}public void complete(Object response) {synchronized (lock) {// 條件滿足,通知等待線程this.response = response;log.debug("notify...");lock.notifyAll();}}
}
測試,沒有超時
public static void main(String[] args) {GuardedObjectV2 v2 = new GuardedObjectV2();new Thread(() -> {sleep(1);v2.complete(null);sleep(1);v2.complete(Arrays.asList("a", "b", "c"));}).start();Object response = v2.get(2500);if (response != null) {log.debug("get response: [{}] lines", ((List<String>) response).size());} else {log.debug("can't get response");}}
輸出
08:49:39.917 [main] c.GuardedObjectV2 - waitTime: 2500
08:49:40.917 [Thread-0] c.GuardedObjectV2 - notify...
08:49:40.917 [main] c.GuardedObjectV2 - timePassed: 1003, object is null true
08:49:40.917 [main] c.GuardedObjectV2 - waitTime: 1497
08:49:41.918 [Thread-0] c.GuardedObjectV2 - notify...
08:49:41.918 [main] c.GuardedObjectV2 - timePassed: 2004, object is null false
08:49:41.918 [main] c.TestGuardedObjectV2 - get response: [3] lines
測試,超時
// 等待時間不足
List<String> lines = v2.get(1500);
輸出
08:47:54.963 [main] c.GuardedObjectV2 - waitTime: 1500
08:47:55.963 [Thread-0] c.GuardedObjectV2 - notify...
08:47:55.963 [main] c.GuardedObjectV2 - timePassed: 1002, object is null true
08:47:55.963 [main] c.GuardedObjectV2 - waitTime: 498
08:47:56.461 [main] c.GuardedObjectV2 - timePassed: 1500, object is null true
08:47:56.461 [main] c.GuardedObjectV2 - waitTime: 0
08:47:56.461 [main] c.GuardedObjectV2 - break...
08:47:56.461 [main] c.TestGuardedObjectV2 - can't get response
08:47:56.963 [Thread-0] c.GuardedObjectV2 - notify...
原理之 join
public final synchronized void join(long millis)throws InterruptedException {long base = System.currentTimeMillis();long now = 0;if (millis < 0) {throw new IllegalArgumentException("timeout value is negative");}if (millis == 0) {while (isAlive()) {wait(0);}} else {while (isAlive()) {long delay = millis - now;if (delay <= 0) {break;}wait(delay);now = System.currentTimeMillis() - base;}}}
- 多任務版 GuardedObject
圖中 Futures 就好比居民樓一層的信箱(每個信箱有房間編號),左側的 t0,t2,t4 就好比等待郵件的居民,右
側的 t1,t3,t5 就好比郵遞員
如果需要在多個類之間使用 GuardedObject 對象,作為參數傳遞不是很方便,因此設計一個用來解耦的中間類,
這樣不僅能夠解耦【結果等待者】和【結果生產者】,還能夠同時支持多個任務的管理
新增 id 用來標識 Guarded Object
class GuardedObject {// 標識 Guarded Objectprivate int id;public GuardedObject(int id) {this.id = id;}public int getId() {return id;}// 結果private Object response;// 獲取結果// timeout 表示要等待多久 2000public Object get(long timeout) {synchronized (this) {// 開始時間 15:00:00long begin = System.currentTimeMillis();// 經歷的時間long passedTime = 0;while (response == null) {// 這一輪循環應該等待的時間long waitTime = timeout - passedTime;// 經歷的時間超過了最大等待時間時,退出循環if (timeout - passedTime <= 0) {break;}try {this.wait(waitTime); // 虛假喚醒 15:00:01} catch (InterruptedException e) {e.printStackTrace();}// 求得經歷時間passedTime = System.currentTimeMillis() - begin; // 15:00:02 1s}return response;}}// 產生結果public void complete(Object response) {synchronized (this) {// 給結果成員變量賦值this.response = response;this.notifyAll();}}
}
中間解耦類
class Mailboxes {private static Map<Integer, GuardedObject> boxes = new Hashtable<>();private static int id = 1;// 產生唯一 idprivate static synchronized int generateId() {return id++;}public static GuardedObject getGuardedObject(int id) {return boxes.remove(id);}public static GuardedObject createGuardedObject() {GuardedObject go = new GuardedObject(generateId());boxes.put(go.getId(), go);return go;}public static Set<Integer> getIds() {return boxes.keySet();}
}
業務相關類
class People extends Thread{@Overridepublic void run() {// 收信GuardedObject guardedObject = Mailboxes.createGuardedObject();log.debug("開始收信 id:{}", guardedObject.getId());Object mail = guardedObject.get(5000);log.debug("收到信 id:{}, 內容:{}", guardedObject.getId(), mail);}
}
class Postman extends Thread {private int id;private String mail;public Postman(int id, String mail) {this.id = id;this.mail = mail;}@Overridepublic void run() {GuardedObject guardedObject = Mailboxes.getGuardedObject(id);log.debug("送信 id:{}, 內容:{}", id, mail);guardedObject.complete(mail);}
}
測試
public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 3; i++) {new People().start();}Sleeper.sleep(1);for (Integer id : Mailboxes.getIds()) {new Postman(id, "內容" + id).start();}
}
某次運行結果
10:35:05.689 c.People [Thread-1] - 開始收信 id:3
10:35:05.689 c.People [Thread-2] - 開始收信 id:1
10:35:05.689 c.People [Thread-0] - 開始收信 id:2
10:35:06.688 c.Postman [Thread-4] - 送信 id:2, 內容:內容2
10:35:06.688 c.Postman [Thread-5] - 送信 id:1, 內容:內容1
10:35:06.688 c.People [Thread-0] - 收到信 id:2, 內容:內容2
10:35:06.688 c.People [Thread-2] - 收到信 id:1, 內容:內容1
10:35:06.688 c.Postman [Thread-3] - 送信 id:3, 內容:內容3
10:35:06.689 c.People [Thread-1] - 收到信 id:3, 內容:內容3
4.8.7 (異步)模式之生產者/消費者
- 定義
要點
-
與前面的保護性暫停中的 GuardObject 不同,不需要產生結果和消費結果的線程一一對應
-
消費隊列可以用來平衡生產和消費的線程資源
-
生產者僅負責產生結果數據,不關心數據該如何處理,而消費者專心處理結果數據
-
消息隊列是有容量限制的,滿時不會再加入數據,空時不會再消耗數據
-
JDK 中各種阻塞隊列,采用的就是這種模式
- 實現
class Message {private int id;private Object message;public Message(int id, Object message) {this.id = id;this.message = message;}public int getId() {return id;}public Object getMessage() {return message;}
}class MessageQueue {private LinkedList<Message> queue;private int capacity;public MessageQueue(int capacity) {this.capacity = capacity;queue = new LinkedList<>();}public Message take() {synchronized (queue) {while (queue.isEmpty()) {log.debug("沒貨了, wait");try {queue.wait();} catch (InterruptedException e) {e.printStackTrace();}}Message message = queue.removeFirst();queue.notifyAll();return message;}}public void put(Message message) {synchronized (queue) {while (queue.size() == capacity) {log.debug("庫存已達上限, wait");try {queue.wait();} catch (InterruptedException e) {e.printStackTrace();}}queue.addLast(message);queue.notifyAll();}}
}
應用 :
MessageQueue messageQueue = new MessageQueue(2);// 4 個生產者線程, 下載任務
for (int i = 0; i < 4; i++) {int id = i;new Thread(() -> {try {log.debug("download...");List<String> response = Downloader.download();log.debug("try put message({})", id);messageQueue.put(new Message(id, response));} catch (IOException e) {e.printStackTrace();}}, "生產者" + i).start();
}// 1 個消費者線程, 處理結果
new Thread(() -> {while (true) {Message message = messageQueue.take();List<String> response = (List<String>) message.getMessage();log.debug("take message({}): [{}] lines", message.getId(), response.size());}
}, "消費者").start();
某次運行結果
10:48:38.070 [生產者3] c.TestProducerConsumer - download...
10:48:38.070 [生產者0] c.TestProducerConsumer - download...
10:48:38.070 [消費者] c.MessageQueue - 沒貨了, wait
10:48:38.070 [生產者1] c.TestProducerConsumer - download...
10:48:38.070 [生產者2] c.TestProducerConsumer - download...
10:48:41.236 [生產者1] c.TestProducerConsumer - try put message(1)
10:48:41.237 [生產者2] c.TestProducerConsumer - try put message(2)
10:48:41.236 [生產者0] c.TestProducerConsumer - try put message(0)
10:48:41.237 [生產者3] c.TestProducerConsumer - try put message(3)
10:48:41.239 [生產者2] c.MessageQueue - 庫存已達上限, wait
10:48:41.240 [生產者1] c.MessageQueue - 庫存已達上限, wait
10:48:41.240 [消費者] c.TestProducerConsumer - take message(0): [3] lines
10:48:41.240 [生產者2] c.MessageQueue - 庫存已達上限, wait
10:48:41.240 [消費者] c.TestProducerConsumer - take message(3): [3] lines
10:48:41.240 [消費者] c.TestProducerConsumer - take message(1): [3] lines
10:48:41.240 [消費者] c.TestProducerConsumer - take message(2): [3] lines
10:48:41.240 [消費者] c.MessageQueue - 沒貨了, wait
4.9 Park & Unpark
4.9.1 基本使用
它們是 LockSupport 類中的方法
// 暫停當前線程
LockSupport.park();
// 恢復某個線程的運行
LockSupport.unpark(暫停線程對象)
先 park 再 unpark
Thread t1 = new Thread(() -> {log.debug("start...");sleep(1);log.debug("park...");LockSupport.park();log.debug("resume...");
},"t1");
t1.start();sleep(2);
log.debug("unpark...");
LockSupport.unpark(t1);
輸出
18:42:52.585 c.TestParkUnpark [t1] - start...
18:42:53.589 c.TestParkUnpark [t1] - park...
18:42:54.583 c.TestParkUnpark [main] - unpark...
18:42:54.583 c.TestParkUnpark [t1] - resume...
先 unpark 再 park
Thread t1 = new Thread(() -> {log.debug("start...");sleep(2);log.debug("park...");LockSupport.park();log.debug("resume...");
}, "t1");
t1.start();sleep(1);
log.debug("unpark...");
LockSupport.unpark(t1);
輸出
18:43:50.765 c.TestParkUnpark [t1] - start...
18:43:51.764 c.TestParkUnpark [main] - unpark...
18:43:52.769 c.TestParkUnpark [t1] - park...
18:43:52.769 c.TestParkUnpark [t1] - resume...
4.9.2 特點
與 Object 的 wait & notify 相比
-
wait,notify 和 notifyAll 必須配合 Object Monitor 一起使用,而 park,unpark 不必
-
park & unpark 是以線程為單位來【阻塞】和【喚醒】線程,而 notify 只能隨機喚醒一個等待線程,notifyAll是喚醒所有等待線程,就不那么【精確】
-
park & unpark 可以先 unpark,而 wait & notify 不能先 notify
4.9.3 原理之 park & unpark
每個線程都有自己的一個(C代碼實現的) Parker 對象,由三部分組成 _counter
, _cond
和_mutex
打個比喻
-
線程就像一個旅人,Parker 就像他隨身攜帶的背包,條件變量就好比背包中的帳篷。_counter 就好比背包中的備用干糧(0 為耗盡,1 為充足)
-
調用 park 就是要看需不需要停下來歇息
- 如果備用干糧耗盡,那么鉆進帳篷歇息
- 如果備用干糧充足,那么不需停留,繼續前進
-
調用 unpark,就好比令干糧充足
- 如果這時線程還在帳篷,就喚醒讓他繼續前進
- 如果這時線程還在運行,那么下次他調用 park 時,僅是消耗掉備用干糧,不需停留,繼續前進
- 因為背包空間有限,多次調用 unpark 僅會補充一份備用干糧,也就是多次unpark后只會讓緊跟著的一次park失效
先調用park 再調用unpark
- 當前線程調用 Unsafe.park() 方法
- 檢查 _counter ,本情況為 0,這時,獲得 _mutex 互斥鎖
- 線程進入 _cond 條件變量阻塞
- 設置 _counter = 0
- 調用 Unsafe.unpark(Thread_0) 方法,設置 _counter 為 1
- 喚醒 _cond 條件變量中的 Thread_0
- Thread_0 恢復運行
- 設置 _counter 為 0
先調用unpark 再調用park
- 調用 Unsafe.unpark(Thread_0) 方法,設置 _counter 為 1
- 當前線程調用 Unsafe.park() 方法
- 檢查 _counter ,本情況為 1,這時線程無需阻塞,繼續運行
- 設置 _counter 為 0
4.10 重新理解線程狀態轉換
概覽圖
假設有線程 Thread t
情況1 NEW --> RUNNABLE
當調用 t.start() 方法時,由 NEW --> RUNNABLE
情況2 RUNNABLE <–> WAITING
t 線程用 synchronized(obj)
獲取了對象鎖后
-
調用 obj.wait() 方法時,t 線程從 RUNNABLE --> WAITING
-
調用 obj.notify() , obj.notifyAll() , t.interrupt() 時
- 競爭鎖成功,t 線程從WAITING --> RUNNABLE
- 競爭鎖失敗,t 線程從WAITING --> BLOCKED
public class TestWaitNotify {final static Object obj = new Object();public static void main(String[] args) {new Thread(() -> {synchronized (obj) {log.debug("執行....");try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}log.debug("其它代碼...."); // 斷點}},"t1").start();new Thread(() -> {synchronized (obj) {log.debug("執行....");try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}log.debug("其它代碼...."); // 斷點}},"t2").start();sleep(0.5);log.debug("喚醒 obj 上其它線程");synchronized (obj) {obj.notifyAll(); // 喚醒obj上所有等待線程 斷點}}
}
情況3 RUNNABLE <–> WAITING
-
當前線程調用 t.join() 方法時,當前線程從 RUNNABLE --> WAITING
- 注意是當前線程在t 線程對象的監視器上等待
-
t 線程運行結束,或調用了當前線程的 interrupt() 時,當前線程從 WAITING --> RUNNABLE
情況4 RUNNABLE <–> WAITING
-
當前線程調用 LockSupport.park() 方法會讓當前線程從 RUNNABLE --> WAITING
-
調用 LockSupport.unpark(目標線程) 或調用了線程 的 interrupt() ,會讓目標線程從 WAITING -->RUNNABLE
情況5 RUNNABLE <–> TIMED_WAITING
t 線程用 synchronized(obj) 獲取了對象鎖后
-
調用 obj.wait(long n) 方法時,t 線程從 RUNNABLE --> TIMED_WAITING
-
t 線程等待時間超過了 n 毫秒,或調用 obj.notify() , obj.notifyAll() , t.interrupt() 時
- 競爭鎖成功,t 線程從TIMED_WAITING --> RUNNABLE
- 競爭鎖失敗,t 線程從TIMED_WAITING --> BLOCKED
情況6 RUNNABLE <–> TIMED_WAITING
-
當前線程調用 t.join(long n) 方法時,當前線程從 RUNNABLE --> TIMED_WAITING
- 注意是當前線程在t 線程對象的監視器上等待
-
當前線程等待時間超過了 n 毫秒,或t 線程運行結束,或調用了當前線程的 interrupt() 時,當前線程從 TIMED_WAITING --> RUNNABLE
情況7 RUNNABLE <–> TIMED_WAITING
-
當前線程調用 Thread.sleep(long n) ,當前線程從 RUNNABLE --> TIMED_WAITING
-
當前線程等待時間超過了 n 毫秒,當前線程從TIMED_WAITING --> RUNNABLE
情況8 RUNNABLE <–> TIMED_WAITING
-
當前線程調用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 時,當前線 程從 RUNNABLE --> TIMED_WAITING
-
調用 LockSupport.unpark(目標線程) 或調用了線程 的 interrupt() ,或是等待超時,會讓目標線程從 TIMED_WAITING–> RUNNABLE
情況9 RUNNABLE <–> BLOCKED
- t 線程用synchronized(obj) 獲取對象鎖時如果競爭失敗,從RUNNABLE --> BLOCKED
- 持 obj 鎖線程的同步代碼塊執行完畢,會喚醒該對象上所有 BLOCKED的線程重新競爭,如果其中 t 線程競爭 成功,從 BLOCKED --> RUNNABLE ,其它失敗的線程仍然BLOCKED
情況10 RUNNABLE --> TERMINATED
當前線程所有代碼運行完畢,進入 TERMINATED
4.11 多把鎖
多把不相干的鎖
一間大屋子有兩個功能:睡覺、學習,互不相干。
現在小南要學習,小女要睡覺,但如果只用一間屋子(一個對象鎖)的話,那么并發度很低
解決方法是準備多個房間(多個對象鎖)
例如
class BigRoom {public void sleep() {synchronized (this) {log.debug("sleeping 2 小時");Sleeper.sleep(2);}}public void study() {synchronized (this) {log.debug("study 1 小時");Sleeper.sleep(1);}}}
執行
BigRoom bigRoom = new BigRoom();new Thread(() -> {bigRoom.study();
},"小南").start();new Thread(() -> {bigRoom.sleep();
},"小女").start();
某次結果
12:13:54.471 [小南] c.BigRoom - study 1 小時
12:13:55.476 [小女] c.BigRoom - sleeping 2 小時
改進
class BigRoom {private final Object studyRoom = new Object();private final Object bedRoom = new Object();public void sleep() {synchronized (bedRoom) {log.debug("sleeping 2 小時");Sleeper.sleep(2);}}public void study() {synchronized (studyRoom) {log.debug("study 1 小時");Sleeper.sleep(1);}}}
某次執行結果
12:15:35.069 [小南] c.BigRoom - study 1 小時
12:15:35.069 [小女] c.BigRoom - sleeping 2 小時
將鎖的粒度細分
-
好處,是可以增強并發度
-
壞處,如果一個線程需要同時獲得多把鎖,就容易發生死鎖
4.12 活躍性
4.12.1 死鎖
有這樣的情況:一個線程需要同時獲取多把鎖,這時就容易發生死鎖
示例
t1 線程
獲得 A對象
鎖,接下來想獲取 B對
的鎖t2 線程
獲得 B對象
鎖,接下來想獲取 A對象
的鎖 例:
Object A = new Object();
Object B = new Object();Thread t1 = new Thread(() -> {synchronized (A) {log.debug("lock A");sleep(1);synchronized (B) {log.debug("lock B");log.debug("操作...");}}
}, "t1");Thread t2 = new Thread(() -> {synchronized (B) {log.debug("lock B");sleep(0.5);synchronized (A) {log.debug("lock A");log.debug("操作...");}}
}, "t2");t1.start();
t2.start();
結果
12:22:06.962 [t2] c.TestDeadLock - lock B
12:22:06.962 [t1] c.TestDeadLock - lock A
4.12.2 定位死鎖
- 檢測死鎖可以使用 jconsole工具,或者使用 jps 定位進程 id,再用 jstack 定位死鎖:
-
避免死鎖要注意加鎖順序
-
另外如果由于某個線程進入了死循環,導致其它線程一直等待,對于這種情況 linux 下可以通過 top 先定位到CPU 占用高的 Java 進程,再利用 top -Hp 進程id 來定位是哪個線程,最后再用 jstack 排查
4.12.3 哲學家就餐問題
有五位哲學家,圍坐在圓桌旁。
- 他們只做兩件事,思考和吃飯,思考一會吃口飯,吃完飯后接著思考。
- 吃飯時要用兩根筷子吃,桌上共有 5 根筷子,每位哲學家左右手邊各有一根筷子。
- 如果筷子被身邊的人拿著,自己就得等待
筷子類
class Chopstick {String name;public Chopstick(String name) {this.name = name;}@Overridepublic String toString() {return "筷子{" + name + '}';}
}
哲學家類
class Philosopher extends Thread {Chopstick left;Chopstick right;public Philosopher(String name, Chopstick left, Chopstick right) {super(name);this.left = left;this.right = right;}private void eat() {log.debug("eating...");Sleeper.sleep(1);}@Overridepublic void run() {while (true) {// 獲得左手筷子synchronized (left) {// 獲得右手筷子synchronized (right) {// 吃飯eat();}// 放下右手筷子}// 放下左手筷子}}}
就餐
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");new Philosopher("蘇格拉底", c1, c2).start();
new Philosopher("柏拉圖", c2, c3).start();
new Philosopher("亞里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c5, c1).start();
執行不多會,就執行不下去了
12:33:15.575 [蘇格拉底] c.Philosopher - eating...
12:33:15.575 [亞里士多德] c.Philosopher - eating...
12:33:16.580 [阿基米德] c.Philosopher - eating...
12:33:17.580 [阿基米德] c.Philosopher - eating...
// 卡在這里, 不向下運行
使用 jconsole 檢測死鎖,發現
-------------------------------------------------------------------------
名稱: 阿基米德
狀態: cn.itcast.Chopstick@1540e19d (筷子1) 上的BLOCKED, 擁有者: 蘇格拉底
總阻止數: 2, 總等待數: 1堆棧跟蹤:
cn.itcast.Philosopher.run(TestDinner.java:48)- 已鎖定 cn.itcast.Chopstick@6d6f6e28 (筷子5)
-------------------------------------------------------------------------
名稱: 蘇格拉底
狀態: cn.itcast.Chopstick@677327b6 (筷子2) 上的BLOCKED, 擁有者: 柏拉圖
總阻止數: 2, 總等待數: 1堆棧跟蹤:
cn.itcast.Philosopher.run(TestDinner.java:48)- 已鎖定 cn.itcast.Chopstick@1540e19d (筷子1)
-------------------------------------------------------------------------
名稱: 柏拉圖
狀態: cn.itcast.Chopstick@14ae5a5 (筷子3) 上的BLOCKED, 擁有者: 亞里士多德
總阻止數: 2, 總等待數: 0堆棧跟蹤:
cn.itcast.Philosopher.run(TestDinner.java:48)- 已鎖定 cn.itcast.Chopstick@677327b6 (筷子2)
-------------------------------------------------------------------------
名稱: 亞里士多德
狀態: cn.itcast.Chopstick@7f31245a (筷子4) 上的BLOCKED, 擁有者: 赫拉克利特
總阻止數: 1, 總等待數: 1堆棧跟蹤:
cn.itcast.Philosopher.run(TestDinner.java:48)- 已鎖定 cn.itcast.Chopstick@14ae5a5 (筷子3)
-------------------------------------------------------------------------
名稱: 赫拉克利特
狀態: cn.itcast.Chopstick@6d6f6e28 (筷子5) 上的BLOCKED, 擁有者: 阿基米德
總阻止數: 2, 總等待數: 0堆棧跟蹤:
cn.itcast.Philosopher.run(TestDinner.java:48)- 已鎖定 cn.itcast.Chopstick@7f31245a (筷子4)
這種線程沒有按預期結束,執行不下去的情況,歸類為【活躍性】問題,除了死鎖以外,還有活鎖和饑餓者兩種情
況
4.12.4 活鎖
活鎖出現在兩個線程互相改變對方的結束條件,最后誰也無法結束,例如
public class TestLiveLock {static volatile int count = 10;static final Object lock = new Object();public static void main(String[] args) {new Thread(() -> {// 期望減到 0 退出循環while (count > 0) {sleep(0.2);count--;log.debug("count: {}", count);}}, "t1").start();new Thread(() -> {// 期望超過 20 退出循環while (count < 20) {sleep(0.2);count++;log.debug("count: {}", count);}}, "t2").start();}
}
饑餓
很多教程中把饑餓定義為,一個線程由于優先級太低,始終得不到 CPU 調度執行,也不能夠結束,饑餓的情況不
易演示,講讀寫鎖時會涉及饑餓問題
下面我講一下我遇到的一個線程饑餓的例子,
先來看看使用順序加鎖的方式解決之前的死鎖問題
順序加鎖的解決方案
但順序加鎖容易產生饑餓問題
例如 哲學家就餐時
new Philosopher("蘇格拉底", c1, c2).start();
new Philosopher("柏拉圖", c2, c3).start();
new Philosopher("亞里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
// new Philosopher("阿基米德", c5, c1).start();
new Philosopher("阿基米德", c1, c5).start(); //線程饑餓
4.13 ReentrantLock
相對于 synchronized 它具備如下特點
- 可中斷
- 可以設置超時時間
- 可以設置為公平鎖
- 支持多個條件變量
與 synchronized 一樣,都支持可重入
基本語法
// 獲取鎖
reentrantLock.lock();
try {// 臨界區
} finally {// 釋放鎖reentrantLock.unlock();
}
4.13.1 可重入
可重入是指同一個線程如果首次獲得了這把鎖,那么因為它是這把鎖的擁有者,因此有權利再次獲取這把鎖
如果是不可重入鎖,那么第二次獲得鎖時,自己也會被鎖擋住
static ReentrantLock lock = new ReentrantLock();public static void main(String[] args) {method1();
}public static void method1() {lock.lock();try {log.debug("execute method1");method2();} finally {lock.unlock();}
}public static void method2() {lock.lock();try {log.debug("execute method2");method3();} finally {lock.unlock();}
}public static void method3() {lock.lock();try {log.debug("execute method3");} finally {lock.unlock();}
}
輸出
17:59:11.862 [main] c.TestReentrant - execute method1
17:59:11.865 [main] c.TestReentrant - execute method2
17:59:11.865 [main] c.TestReentrant - execute method3
4.13.2 可打斷 lock.lockInterruptibly()
示例
ReentrantLock lock = new ReentrantLock();Thread t1 = new Thread(() -> {log.debug("啟動...");try {//沒有競爭就會獲取鎖//有競爭就進入阻塞隊列等待,但可以被打斷lock.lockInterruptibly();//lock.lock(); //不可打斷} catch (InterruptedException e) {e.printStackTrace();log.debug("等鎖的過程中被打斷");return;}try {log.debug("獲得了鎖");} finally {lock.unlock();}
}, "t1");lock.lock();
log.debug("獲得了鎖");
t1.start();try {sleep(1);log.debug("執行打斷");t1.interrupt();
} finally {lock.unlock();
}
輸出
18:02:40.520 [main] c.TestInterrupt - 獲得了鎖
18:02:40.524 [t1] c.TestInterrupt - 啟動...
18:02:41.530 [main] c.TestInterrupt - 執行打斷
java.lang.InterruptedException
at
java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchr
onizer.java:898) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222) at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335) at cn.itcast.n4.reentrant.TestInterrupt.lambda$main$0(TestInterrupt.java:17) at java.lang.Thread.run(Thread.java:748)
18:02:41.532 [t1] c.TestInterrupt - 等鎖的過程中被打斷
注意如果是不可中斷模式,那么即使使用了 interrupt 也不會讓等待中斷
ReentrantLock lock = new ReentrantLock();Thread t1 = new Thread(() -> {log.debug("啟動...");lock.lock();try {log.debug("獲得了鎖");} finally {lock.unlock();}
}, "t1");lock.lock();
log.debug("獲得了鎖");
t1.start();try {sleep(1);log.debug("執行打斷");t1.interrupt();sleep(1);
} finally {log.debug("釋放了鎖");lock.unlock();
}
輸出
18:06:56.261 [main] c.TestInterrupt - 獲得了鎖
18:06:56.265 [t1] c.TestInterrupt - 啟動...
18:06:57.266 [main] c.TestInterrupt - 執行打斷 // 這時 t1 并沒有被真正打斷, 而是仍繼續等待鎖
18:06:58.267 [main] c.TestInterrupt - 釋放了鎖
18:06:58.267 [t1] c.TestInterrupt - 獲得了鎖
4.13.3 鎖(可設置)超時
立刻返回結果 lock.tryLock()
ReentrantLock lock = new ReentrantLock();Thread t1 = new Thread(() -> {log.debug("啟動...");if (!lock.tryLock()) {log.debug("獲取立刻失敗,返回");return;}try {log.debug("獲得了鎖");} finally {lock.unlock();}
}, "t1");lock.lock();
log.debug("獲得了鎖");
t1.start();try {sleep(2);
} finally {lock.unlock();
}
輸出
18:15:02.918 [main] c.TestTimeout - 獲得了鎖
18:15:02.921 [t1] c.TestTimeout - 啟動...
18:15:02.921 [t1] c.TestTimeout - 獲取立刻失敗,返回
嘗試一定時間 lock.tryLock
ReentrantLock lock = new ReentrantLock();Thread t1 = new Thread(() -> {log.debug("啟動...");try {if (!lock.tryLock(1, TimeUnit.SECONDS)) {log.debug("獲取等待 1s 后失敗,返回");return;}} catch (InterruptedException e) {e.printStackTrace();}try {log.debug("獲得了鎖");} finally {lock.unlock();}
}, "t1");lock.lock();
log.debug("獲得了鎖");
t1.start();try {sleep(2);
} finally {lock.unlock();
}
輸出
18:19:40.537 [main] c.TestTimeout - 獲得了鎖
18:19:40.544 [t1] c.TestTimeout - 啟動...
18:19:41.547 [t1] c.TestTimeout - 獲取等待 1s 后失敗,返回
使用 tryLock 解決哲學家就餐問題
class Chopstick extends ReentrantLock {String name;public Chopstick(String name) {this.name = name;}@Overridepublic String toString() {return "筷子{" + name + '}';}
}
class Philosopher extends Thread {Chopstick left;Chopstick right;public Philosopher(String name, Chopstick left, Chopstick right) {super(name);this.left = left;this.right = right;}@Overridepublic void run() {while (true) {// 嘗試獲得左手筷子if (left.tryLock()) {try {// 嘗試獲得右手筷子if (right.tryLock()) {try {eat();} finally {right.unlock();}}} finally {left.unlock();}}}}private void eat() {log.debug("eating...");Sleeper.sleep(1);}}
4.13.4 (可設置是否為)公平鎖
公平: 先來就能先執行
不公平: 不保證先來就先執行
ReentrantLock 默認是不公平的
ReentrantLock lock = new ReentrantLock(false);lock.lock();
for (int i = 0; i < 500; i++) {new Thread(() -> {lock.lock();try {System.out.println(Thread.currentThread().getName() + " running...");} finally {lock.unlock();}}, "t" + i).start();
}
// 1s 之后去爭搶鎖
Thread.sleep(1000);new Thread(() -> {System.out.println(Thread.currentThread().getName() + " start...");lock.lock();try {System.out.println(Thread.currentThread().getName() + " running...");} finally {lock.unlock();}
}, "強行插入").start();lock.unlock();
強行插入,有機會在中間輸出
注意:該實驗不一定總能復現
t39 running...
t40 running...
t41 running...
t42 running...
t43 running...
強行插入 start...
強行插入 running...
t44 running...
t45 running...
t46 running...
t47 running...
t49 running...
改為公平鎖后
ReentrantLock lock = new ReentrantLock(true);
強行插入,總是在最后輸出
t465 running...
t464 running...
t477 running...
t442 running...
t468 running...
t493 running...
t482 running...
t485 running...
t481 running...
強行插入 running...
公平鎖一般沒有必要,會降低并發度,后面分析原理時會講解
4.13.5 (多個)條件變量
synchronized 中也有條件變量,就是我們講原理時那個 waitSet 休息室,當條件不滿足時進入 waitSet 等待
ReentrantLock 的條件變量比 synchronized 強大之處在于,它是支持多個條件變量的,這就好比
-
synchronized 是那些不滿足條件的線程都在一間休息室等消息
-
而 ReentrantLock 支持多間休息室,有專門等煙的休息室、專門等早餐的休息室、喚醒時也是按休息室來喚醒
使用要點:
-
await 前需要獲得鎖
-
await 執行后,會釋放鎖,進入 conditionObject 等待
-
await 的線程被喚醒(或打斷、或超時)去重新競爭 lock 鎖
-
競爭 lock 鎖成功后,從 await 后繼續執行
例子:
static ReentrantLock lock = new ReentrantLock();static Condition waitCigaretteQueue = lock.newCondition();
static Condition waitbreakfastQueue = lock.newCondition();static volatile boolean hasCigrette = false;
static volatile boolean hasBreakfast = false;public static void main(String[] args) {new Thread(() -> {try {lock.lock();while (!hasCigrette) {try {waitCigaretteQueue.await();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("等到了它的煙");} finally {lock.unlock();}}).start();new Thread(() -> {try {lock.lock();while (!hasBreakfast) {try {waitbreakfastQueue.await();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("等到了它的早餐");} finally {lock.unlock();}}).start();sleep(1);sendBreakfast();sleep(1);sendCigarette();
}private static void sendCigarette() {lock.lock();try {log.debug("送煙來了");hasCigrette = true;waitCigaretteQueue.signal();} finally {lock.unlock();}
}private static void sendBreakfast() {lock.lock();try {log.debug("送早餐來了");hasBreakfast = true;waitbreakfastQueue.signal();} finally {lock.unlock();}
}
輸出
18:52:27.680 [main] c.TestCondition - 送早餐來了
18:52:27.682 [Thread-1] c.TestCondition - 等到了它的早餐
18:52:28.683 [main] c.TestCondition - 送煙來了
18:52:28.683 [Thread-0] c.TestCondition - 等到了它的煙
4.13.6 同步模式之順序控制
- 固定運行順序
比如,必須先 2 后 1 打印
wait notify 版
// 用來同步的對象
static Object obj = new Object();
// t2 運行標記, 代表 t2 是否執行過
static boolean t2runed = false;public static void main(String[] args) {Thread t1 = new Thread(() -> {synchronized (obj) {// 如果 t2 沒有執行過while (!t2runed) { try {// t1 先等一會obj.wait(); } catch (InterruptedException e) {e.printStackTrace();}}}System.out.println(1);});Thread t2 = new Thread(() -> {System.out.println(2);synchronized (obj) {// 修改運行標記t2runed = true;// 通知 obj 上等待的線程(可能有多個,因此需要用 notifyAll)obj.notifyAll();}});t1.start();t2.start();
}
Park Unpark 版
可以看到,實現上很麻煩:
-
首先,需要保證先 wait 再 notify,否則 wait 線程永遠得不到喚醒。因此使用了『運行標記』來判斷該不該wait
-
第二,如果有些干擾線程錯誤地 notify 了 wait 線程,條件不滿足時還要重新等待,使用了 while 循環來解決此問題
-
最后,喚醒對象上的 wait 線程需要使用 notifyAll,因為『同步對象』上的等待線程可能不止一個
可以使用 LockSupport 類的 park 和 unpark 來簡化上面的題目:
Thread t1 = new Thread(() -> {try { Thread.sleep(1000); } catch (InterruptedException e) { }// 當沒有『許可』時,當前線程暫停運行;有『許可』時,用掉這個『許可』,當前線程恢復運行LockSupport.park();System.out.println("1");
});Thread t2 = new Thread(() -> {System.out.println("2");// 給線程 t1 發放『許可』(多次連續調用 unpark 只會發放一個『許可』)LockSupport.unpark(t1);
});t1.start();
t2.start();
park 和 unpark 方法比較靈活,他倆誰先調用,誰后調用無所謂。
并且是以線程為單位進行『暫停』和『恢復』,不需要『同步對象』和『運行標記』
- 交替輸出
線程 1 輸出 a 5 次,線程 2 輸出 b 5 次,線程 3 輸出 c 5 次。現在要求輸出 abcabcabcabcabc 怎么實現
wait notify 版
class SyncWaitNotify {private int flag;private int loopNumber;public SyncWaitNotify(int flag, int loopNumber) {this.flag = flag;this.loopNumber = loopNumber;}public void print(int waitFlag, int nextFlag, String str) {for (int i = 0; i < loopNumber; i++) {synchronized (this) {while (this.flag != waitFlag) {try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.print(str);flag = nextFlag;this.notifyAll();}}}
}
SyncWaitNotify syncWaitNotify = new SyncWaitNotify(1, 5);new Thread(() -> {syncWaitNotify.print(1, 2, "a");
}).start();new Thread(() -> {syncWaitNotify.print(2, 3, "b");
}).start();new Thread(() -> {syncWaitNotify.print(3, 1, "c");
}).start();
Lock 條件變量版
class AwaitSignal extends ReentrantLock{private int loopNumber;public AwaitSignal(int loopNumber) {this.loopNumber = loopNumber;}// 參數1 打印內容, 參數2 進入哪一間休息室, 參數3 下一間休息室public void print(String str, Condition current, Condition next) {for (int i = 0; i < loopNumber; i++) {lock();try {current.await();System.out.print(str);next.signal();} catch (InterruptedException e) {e.printStackTrace();} finally {unlock();}}}
}
public static void main(String[] args) throws InterruptedException {AwaitSignal awaitSignal = new AwaitSignal(5);Condition a = awaitSignal.newCondition();Condition b = awaitSignal.newCondition();Condition c = awaitSignal.newCondition();new Thread(() -> {awaitSignal.print("a", a, b);}).start();new Thread(() -> {awaitSignal.print("b", b, c);}).start();new Thread(() -> {awaitSignal.print("c", c, a);}).start();Thread.sleep(1000);awaitSignal.lock();try {System.out.println("開始...");a.signal();} finally {awaitSignal.unlock();}}
Park Unpark 版
package com.tobestronger.n4._4_13.JiaoTiShuChu;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.locks.LockSupport;@Slf4j(topic = "c.JiaoTiShuChuParkUnpark")
public class JiaoTiShuChuParkUnpark {static Thread t1;static Thread t2;static Thread t3;public static void main(String[] args) {ParkUnpark pu = new ParkUnpark(5);t1 = new Thread(() -> {pu.print("a", t2);});t2 = new Thread(() -> {pu.print("b", t3);});t3 = new Thread(() -> {pu.print("c", t1);});t1.start();t2.start();t3.start();LockSupport.unpark(t1);}
}class ParkUnpark {private int loopNumber;public ParkUnpark(int loopNumber) {this.loopNumber = loopNumber;}public void print(String str, Thread next) {for (int i = 0; i < loopNumber; i++) {LockSupport.park();System.out.print(str);LockSupport.unpark(next);}}}
本章小結
本章我們需要重點掌握的是
-
分析多線程訪問共享資源時,哪些代碼片段屬于臨界區
-
使用 synchronized 互斥解決臨界區的線程安全問題
- 掌握 synchronized 鎖對象語法
- 掌握 synchronzied 加載成員方法和靜態方法語法
- 掌握 wait/notify 同步方法
-
使用 lock 互斥解決臨界區的線程安全問題
- 掌握 lock 的使用細節:可打斷、鎖超時、公平鎖、條件變量
-
學會分析變量的線程安全性、掌握常見線程安全類的使用
-
了解線程活躍性問題:死鎖、活鎖、饑餓
-
應用方面
- 互斥:使用 synchronized 或 Lock 達到共享資源互斥效果
- 同步:使用 wait/notify 或 Lock 的條件變量來達到線程間通信效果
-
原理方面
- monitor、synchronized 、wait/notify 原理
- synchronized 進階原理
- park & unpark 原理
-
模式方面
-
同步模式之保護性暫停
-
異步模式之生產者消費者
public SyncWaitNotify(int flag, int loopNumber) {
this.flag = flag;
this.loopNumber = loopNumber;
}public void print(int waitFlag, int nextFlag, String str) {
for (int i = 0; i < loopNumber; i++) {
synchronized (this) {
while (this.flag != waitFlag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}System.out.print(str);flag = nextFlag;this.notifyAll();} }
}
}
-
```java
SyncWaitNotify syncWaitNotify = new SyncWaitNotify(1, 5);new Thread(() -> {syncWaitNotify.print(1, 2, "a");
}).start();new Thread(() -> {syncWaitNotify.print(2, 3, "b");
}).start();new Thread(() -> {syncWaitNotify.print(3, 1, "c");
}).start();
Lock 條件變量版
class AwaitSignal extends ReentrantLock{private int loopNumber;public AwaitSignal(int loopNumber) {this.loopNumber = loopNumber;}// 參數1 打印內容, 參數2 進入哪一間休息室, 參數3 下一間休息室public void print(String str, Condition current, Condition next) {for (int i = 0; i < loopNumber; i++) {lock();try {current.await();System.out.print(str);next.signal();} catch (InterruptedException e) {e.printStackTrace();} finally {unlock();}}}
}
public static void main(String[] args) throws InterruptedException {AwaitSignal awaitSignal = new AwaitSignal(5);Condition a = awaitSignal.newCondition();Condition b = awaitSignal.newCondition();Condition c = awaitSignal.newCondition();new Thread(() -> {awaitSignal.print("a", a, b);}).start();new Thread(() -> {awaitSignal.print("b", b, c);}).start();new Thread(() -> {awaitSignal.print("c", c, a);}).start();Thread.sleep(1000);awaitSignal.lock();try {System.out.println("開始...");a.signal();} finally {awaitSignal.unlock();}}
Park Unpark 版
package com.tobestronger.n4._4_13.JiaoTiShuChu;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.locks.LockSupport;@Slf4j(topic = "c.JiaoTiShuChuParkUnpark")
public class JiaoTiShuChuParkUnpark {static Thread t1;static Thread t2;static Thread t3;public static void main(String[] args) {ParkUnpark pu = new ParkUnpark(5);t1 = new Thread(() -> {pu.print("a", t2);});t2 = new Thread(() -> {pu.print("b", t3);});t3 = new Thread(() -> {pu.print("c", t1);});t1.start();t2.start();t3.start();LockSupport.unpark(t1);}
}class ParkUnpark {private int loopNumber;public ParkUnpark(int loopNumber) {this.loopNumber = loopNumber;}public void print(String str, Thread next) {for (int i = 0; i < loopNumber; i++) {LockSupport.park();System.out.print(str);LockSupport.unpark(next);}}}
本章小結
本章我們需要重點掌握的是
-
分析多線程訪問共享資源時,哪些代碼片段屬于臨界區
-
使用 synchronized 互斥解決臨界區的線程安全問題
- 掌握 synchronized 鎖對象語法
- 掌握 synchronzied 加載成員方法和靜態方法語法
- 掌握 wait/notify 同步方法
-
使用 lock 互斥解決臨界區的線程安全問題
- 掌握 lock 的使用細節:可打斷、鎖超時、公平鎖、條件變量
-
學會分析變量的線程安全性、掌握常見線程安全類的使用
-
了解線程活躍性問題:死鎖、活鎖、饑餓
-
應用方面
- 互斥:使用 synchronized 或 Lock 達到共享資源互斥效果
- 同步:使用 wait/notify 或 Lock 的條件變量來達到線程間通信效果
-
原理方面
- monitor、synchronized 、wait/notify 原理
- synchronized 進階原理
- park & unpark 原理
-
模式方面
- 同步模式之保護性暫停
- 異步模式之生產者消費者
- 同步模式之順序控制