java並發2--進階
五、互斥同步
Java 提供了兩種鎖機制來控制多個線程對共享資源的互斥訪問,第一個是 JVM 實現的 synchronized,而另一個是 JDK 實現的 ReentrantLock。
1.1 synchronized
1. 同步一個代碼塊
public void func() { synchronized (this) { // ... } }
它只作用於同一個對象,如果調用兩個對象上的同步代碼塊,就不會進行同步。
對於以下代碼,使用 ExecutorService 執行了兩個線程,由於調用的是同一個對象的同步代碼塊,因此這兩個線程會進行同步,當一個線程進入同步語句塊時,另一個線程就必須等待。
public class SynchronizedExample { public void func1() { synchronized (this) { for (int i = 0; i < 10; i++) { System.out.print(i + " "); } } } } public static void main(String[] args) { SynchronizedExample e1 = new SynchronizedExample(); ExecutorService executorService= Executors.newCachedThreadPool(); executorService.execute(() -> e1.func1()); executorService.execute(() -> e1.func1()); } 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
對於以下代碼,兩個線程調用了不同對象的同步代碼塊,因此這兩個線程就不需要同步。從輸出結果可以看出,兩個線程交叉執行。
public static void main(String[] args) { SynchronizedExample e1= new SynchronizedExample(); SynchronizedExample e2 = new SynchronizedExample(); ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> e1.func1()); executorService.execute(() -> e2.func1()); } 0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
2. 同步一個方法
public synchronized void func () { // ... }
它和同步代碼塊一樣,作用於同一個對象。
3. 同步一個類
public void func() { synchronized (SynchronizedExample.class) { // ... } }
作用於整個類,也就是說兩個線程調用同一個類的不同對象上的這種同步語句,也會進行同步。
public class SynchronizedExample { public void func2() { synchronized (SynchronizedExample.class) { for (int i = 0; i < 10; i++) { System.out.print(i + " "); } } } } public static void main(String[] args) { SynchronizedExample e1 = new SynchronizedExample(); SynchronizedExample e2 = new SynchronizedExample(); ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> e1.func2()); executorService.execute(() -> e2.func2()); } 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
4. 同步一個靜態方法
public synchronized static void fun() { // ... }
作用於整個類。
1.2 ReentrantLock
ReentrantLock 是 java.util.concurrent(J.U.C)包中的鎖。
public class LockExample { private Lock lock = new ReentrantLock(); public void func() { lock.lock(); try { for (int i = 0; i < 10; i++) { System.out.print(i + " "); } } finally { lock.unlock(); // 確保釋放鎖,從而避免發生死鎖。 } } } public static void main(String[] args) { LockExample lockExample = new LockExample(); ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> lockExample.func()); executorService.execute(() -> lockExample.func()); } 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
1.3 比較
鎖的實現
synchronized 是 JVM 實現的,而 ReentrantLock 是 JDK 實現的。
性能
新版本 Java 對 synchronized 進行了很多優化,例如自旋鎖等,synchronized 與 ReentrantLock 大致相同。
等待可中斷
當持有鎖的線程長期不釋放鎖的時候,正在等待的線程可以選擇放棄等待,改為處理其他事情。
ReentrantLock 可中斷,而 synchronized 不行。
公平鎖
公平鎖是指多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖。
synchronized 中的鎖是非公平的,ReentrantLock 默認情況下也是非公平的,但是也可以是公平的。
鎖綁定多個條件
一個 ReentrantLock 可以同時綁定多個 Condition 對象。
使用選擇
除非需要使用 ReentrantLock 的高級功能,否則優先使用 synchronized。這是因為 synchronized 是 JVM 實現的一種鎖機制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。並且使用 synchronized 不用擔心沒有釋放鎖而導致死鎖問題,因為 JVM 會確保鎖的釋放。
六、線程之間的協作
當多個線程可以一起工作去解決某個問題時,如果某些部分必須在其它部分之前完成,那麽就需要對線程進行協調。
1.4 join()
在線程中調用另一個線程的 join() 方法,會將當前線程掛起,而不是忙等待,直到目標線程結束。
對於以下代碼,雖然 b 線程先啟動,但是因為在 b 線程中調用了 a 線程的 join() 方法,b 線程會等待 a 線程結束才繼續執行,因此最後能夠保證 a 線程的輸出先於 b 線程的輸出。
public class JoinExample { private class A extends Thread { @Override public void run() { System.out.println("A"); } } private class B extends Thread { private A a; B(A a) { this.a = a; } @Override public void run() { try { a.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("B"); } } public void test() { A a = new A(); B b = new B(a); b.start(); a.start(); } } public static void main(String[] args) { JoinExample example = new JoinExample(); example.test(); } A B
1.5 wait() notify() notifyAll()
調用 wait() 使得線程等待某個條件滿足,線程在等待時會被掛起,當其他線程的運行使得這個條件滿足時,其它線程會調用 notify() 或者 notifyAll() 來喚醒掛起的線程。
它們都屬於 Object 的一部分,而不屬於 Thread。
只能用在同步方法或者同步控制塊中使用,否則會在運行時拋出 IllegalMonitorStateExeception。
使用 wait() 掛起期間,線程會釋放鎖。這是因為,如果沒有釋放鎖,那麽其它線程就無法進入對象的同步方法或者同步控制塊中,那麽就無法執行 notify() 或者 notifyAll() 來喚醒掛起的線程,造成死鎖。
public class WaitNotifyExample { public synchronized void before() { System.out.println("before"); notifyAll(); } public synchronized void after() { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("after"); } } public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); WaitNotifyExample example = new WaitNotifyExample(); executorService.execute(() -> example.after()); executorService.execute(() -> example.before()); } before after
wait() 和 sleep() 的區別
- wait() 是 Object 的方法,而 sleep() 是 Thread 的靜態方法;
- wait() 會釋放鎖,sleep() 不會。
1.6 await() signal() signalAll()
java.util.concurrent 類庫中提供了 Condition 類來實現線程之間的協調,可以在 Condition 上調用 await() 方法使線程等待,其它線程調用 signal() 或 signalAll() 方法喚醒等待的線程。
相比於 wait() 這種等待方式,await() 可以指定等待的條件,因此更加靈活。
使用 Lock 來獲取一個 Condition 對象。
public class AwaitSignalExample { private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void before() { lock.lock(); try { System.out.println("before"); condition.signalAll(); } finally { lock.unlock(); } } public void after() { lock.lock(); try { condition.await(); System.out.println("after"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); AwaitSignalExample example = new AwaitSignalExample(); executorService.execute(() -> example.after()); executorService.execute(() -> example.before()); } before after
java並發2--進階