關於java多線程並發控制,除了內建關鍵字synchronized外,還有lock,lock的一個實作類別就是ReetrantLock,Lock可以實現更靈活的多線程並發控制。
如何使用,舉個栗子
public class ReetrantLockDemo { static final Lock lock = new ReentrantLock(); static Runnable runnable1 = new Runnable() { @Override public void run() { lock.lock(); try { for (int i = 0; i < 5; i++) { System.out.println("runnable1 running " + i); } } finally { lock.unlock(); } } }; static Runnable runnable2 = new Runnable() { @Override public void run() { lock.lock(); try { for (int i = 0; i < 5; i++) { System.out.println("runnable2 running " + i); } } finally { lock.unlock(); } } }; public static void main(String[] args) { new Thread(runnable1).start(); new Thread(runnable2).start(); }}
這就是lock的最基本使用,使用lock和unlock方法對代碼片進行加鎖瞭解鎖,效果和synchronized一樣,兩個線程序列化執行了,控制台輸出如下:
runnable1 running 0
runnable1 running 1
runnable1 running 2
runnable1 running 3
runnable1 running 4
runnable2 running 0
runnable2 running 1
runnable2 running 2
runnable2 running 3
runnable2 running 4
也就是說synchronized能做的lock都能做,那麼看看lock還能做什麼。它還提供了trylock方法,這個方法就是說我會嘗試擷取鎖,但擷取鎖如果失敗,不會導致當前線程阻塞,直接跳過,把上面的栗子稍微改造一下。
public class ReetrantLockDemo { static final Lock lock = new ReentrantLock(); static Runnable runnable1 = new Runnable() { @Override public void run() { lock.lock(); try { for (int i = 0; i < 5; i++) { System.out.println("runnable1 running " + i); } } finally { lock.unlock(); } } }; static Runnable runnable2 = new Runnable() { @Override public void run() { if (lock.tryLock()) { try { for (int i = 0; i < 5; i++) { System.out.println("runnable2 running " + i); } } finally { lock.unlock(); } } else { System.out.println("Can not get the lock, skip running"); } } }; public static void main(String[] args) { new Thread(runnable1).start(); new Thread(runnable2).start(); }}
線上程2中嘗試擷取鎖,如果擷取不到,就直接跳過,控制台輸出如下:
runnable1 running 0
Can not get the lock, skip running
runnable1 running 1
runnable1 running 2
runnable1 running 3
runnable1 running 4
另外trylock方法還可以設定等待時間,等待多少時間後擷取不了鎖就跳過。在構造ReetrantLock時可以設定一個boolean值,就是 new ReetrantLock(true/false),選擇是否構造一個“公平鎖”,所謂公平鎖就是讓所有等待線程按他們的等待順序來得到當前鎖,防止一些線程很久都不被執行,不過需要注意實現公平鎖需要一些額外計算開銷,慎用。
lock還有一個功能就是condition,簡單的說就是條件設定。舉個栗子,比如上面2個線程,需求是第一個線程迴圈到3的時候第二個線程開始,這時候第一個線程停止,等到第二個線程執行到3的時候在啟動。那麼這裡就有2個條件:
1. 第二個線程需要在第一個線程執行到3的時候才能啟動,用condition1表示
2. 第一個線程需要在第二個線程執行到3的時候重啟。用condition2表示
public class ReetrantLockDemo { static final Lock lock = new ReentrantLock(); static boolean thread1Arrive3 = false; static boolean thread2Arrive3 = false; static final Condition condition1 = lock.newCondition(); static final Condition condition2 = lock.newCondition(); static Runnable runnable1 = new Runnable() { @Override public void run() { lock.lock(); try { for (int i = 0; i < 5; i++) { System.out.println("runnable1 running " + i); if(i==3){ thread1Arrive3=true; condition1.signal(); if(!thread2Arrive3){ condition2.await(); } } } } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }; static Runnable runnable2 = new Runnable() { @Override public void run() { lock.lock(); try { if(!thread1Arrive3){ condition1.await(); } for (int i = 0; i < 5; i++) { System.out.println("runnable2 running " + i); if(i==3){ thread2Arrive3=true; condition2.signal(); } } } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }; public static void main(String[] args) { new Thread(runnable1).start(); new Thread(runnable2).start(); }}
運行結果:
runnable1 running 0
runnable1 running 1
runnable1 running 2
runnable1 running 3
runnable2 running 0
runnable2 running 1
runnable2 running 2
runnable2 running 3
runnable2 running 4
runnable1 running 4
condition的await方法可以讓當前線程釋放鎖並且在指定的condition上面進行等待,而signal方法則通知在condition上等待的線程擷取鎖並繼續執行。condition其實維護了一個隊列的資料結構,await相當於往隊列中put,而signal則是從隊列中取出。所以需要注意如果在執行signal時,沒有線程在這個condition上去await,singal方法也不會阻塞。所以需要注意使用condition的時候一定是在滿足條件的時候才去進行await和signal(範例程式碼中使用2個boolean變數作為判定條件),如果signal先執行,await後執行就直接死結了。