三執行緒按順序交替列印ABC的四種方法
建立三個執行緒A、B、C,A執行緒列印10次字母A,B執行緒列印10次字母B,C執行緒列印10次字母C,但是要求三個執行緒同時執行,並且實現交替列印,即按照ABCABCABC的順序列印。
二、Synchronized同步法
1、基本思路
使用同步塊和wait、notify的方法控制三個執行緒的執行次序。具體方法如下:從大的方向上來講,該問題為三執行緒間的同步喚醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA迴圈執行三個執行緒。為了控制執行緒執行的順序,那麼就必須要確定喚醒、等待的順序,所以每一個執行緒必須同時持有兩個物件鎖,才能進行列印操作。一個物件鎖是prev,就是前一個執行緒所對應的物件鎖,其主要作用是保證當前執行緒一定是在前一個執行緒操作完成後(即前一個執行緒釋放了其對應的物件鎖)才開始執行。還有一個鎖就是自身物件鎖。主要的思想就是,為了控制執行的順序,必須要先持有prev鎖(也就前一個執行緒要釋放其自身物件鎖),然後當前執行緒再申請自己物件鎖,兩者兼備時列印。之後首先呼叫self.notify()喚醒下一個等待執行緒(注意notify不會立即釋放物件鎖,只有等到同步塊程式碼執行完畢後才會釋放),再呼叫prev.wait()立即釋放prev物件鎖,當前執行緒進入休眠,等待其他執行緒的notify操作再次喚醒。
2、程式碼
public class ABC_Synch {
public static class ThreadPrinter implements Runnable {
private String name;
private Object prev;
private Object self;
private ThreadPrinter(String name, Object prev, Object self) {
this.name = name;
this.prev = prev;
this.self = self;
}
@Override
public void run() {
int count = 10;
while (count > 0) {// 多執行緒併發,不能用if,必須使用whil迴圈
synchronized (prev) { // 先獲取 prev 鎖
synchronized (self) {// 再獲取 self 鎖
System.out.print(name);//列印
count--;
self.notifyAll();// 喚醒其他執行緒競爭self鎖,注意此時self鎖並未立即釋放。
}
//此時執行完self的同步塊,這時self鎖才釋放。
try {
prev.wait(); // 立即釋放 prev鎖,當前執行緒休眠,等待喚醒
/**
* JVM會在wait()物件鎖的執行緒中隨機選取一執行緒,賦予其物件鎖,喚醒執行緒,繼續執行。
*/
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) throws Exception {
Object a = new Object();
Object b = new Object();
Object c = new Object();
ThreadPrinter pa = new ThreadPrinter("A", c, a);
ThreadPrinter pb = new ThreadPrinter("B", a, b);
ThreadPrinter pc = new ThreadPrinter("C", b, c);
new Thread(pa).start();
Thread.sleep(10);//保證初始ABC的啟動順序
new Thread(pb).start();
Thread.sleep(10);
new Thread(pc).start();
Thread.sleep(10);
}
}
可以看到程式一共定義了a,b,c三個物件鎖,分別對應A、B、C三個執行緒。A執行緒最先執行,A執行緒按順序申請c,a物件鎖,列印操作後按順序釋放a,c物件鎖,並且通過notify操作喚醒執行緒B。執行緒B首先等待獲取A鎖,再申請B鎖,後列印B,再釋放B,A鎖,喚醒C。執行緒C等待B鎖,再申請C鎖,後列印C,再釋放C,B鎖,喚醒A。看起來似乎沒什麼問題,但如果你仔細想一下,就會發現有問題,就是初始條件,三個執行緒必須按照A,B,C的順序來啟動,但是這種假設依賴於JVM中執行緒排程、執行的順序。
wait() 與 notify/notifyAll() 是Object類的方法,在執行兩個方法時,要先獲得鎖。
當執行緒執行wait()時,會把當前的鎖釋放,然後讓出CPU,進入等待狀態。
當執行notify/notifyAll方法時,會喚醒一個處於等待該 物件鎖 的執行緒,然後繼續往下執行,直到執行完退出物件鎖鎖住的區域(synchronized修飾的程式碼塊)後再釋放鎖。
從這裡可以看出,notify/notifyAll()執行後,並不立即釋放鎖,而是要等到執行完臨界區中程式碼後,再釋放。所以在實際程式設計中,我們應該儘量線上程呼叫notify/notifyAll()後,立即退出臨界區。即不要在notify/notifyAll()後面再寫一些耗時的程式碼。
二、Lock鎖方法
1、基本思路
通過ReentrantLock我們可以很方便的進行顯式的鎖操作,即獲取鎖和釋放鎖,對於同一個物件鎖而言,統一時刻只可能有一個執行緒拿到了這個鎖,此時其他執行緒通過lock.lock()來獲取物件鎖時都會被阻塞,直到這個執行緒通過lock.unlock()操作釋放這個鎖後,其他執行緒才能拿到這個鎖。
2、程式碼
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ABC_Lock {
private static Lock lock = new ReentrantLock();// 通過JDK5中的Lock鎖來保證執行緒的訪問的互斥
private static int state = 0;//通過state的值來確定是否列印
static class ThreadA extends Thread {
@Override
public void run() {
for (int i = 0; i < 10;) {
try {
lock.lock();
while (state % 3 == 0) {// 多執行緒併發,不能用if,必須用迴圈測試等待條件,避免虛假喚醒
System.out.print("A");
state++;
i++;
}
} finally {
lock.unlock();// unlock()操作必須放在finally塊中
}
}
}
}
static class ThreadB extends Thread {
@Override
public void run() {
for (int i = 0; i < 10;) {
try {
lock.lock();
while (state % 3 == 1) {
System.out.print("B");
state++;
i++;
}
} finally {
lock.unlock();// unlock()操作必須放在finally塊中
}
}
}
}
static class ThreadC extends Thread {
@Override
public void run() {
for (int i = 0; i < 10;) {
try {
lock.lock();
while (state % 3 == 2) {
System.out.print("C");
state++;
i++;
}
} finally {
lock.unlock();// unlock()操作必須放在finally塊中
}
}
}
}
public static void main(String[] args) {
new ThreadA().start();
new ThreadB().start();
new ThreadC().start();
}
}
值得注意的是ReentrantLock是可重入鎖,它持有一個鎖計數器,當已持有鎖的執行緒再次獲得該鎖時計數器值加1,每呼叫一次lock.unlock()時所計數器值減一,直到所計數器值為0,此時執行緒釋放鎖。
三、ReentrantLock結合Condition
1、基本思路
與ReentrantLock搭配的通行方式是Condition,如下:
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
condition.await();//this.wait();
condition.signal();//this.notify();
condition.signalAll();//this.notifyAll();
Condition是被繫結到Lock上的,必須使用lock.newCondition()才能建立一個Condition。從上面的程式碼可以看出,Synchronized能實現的通訊方式,Condition都可以實現,功能類似的程式碼寫在同一行中。這樣解題思路就和第一種方法基本一致,只是採用的方法不同。
2、程式碼
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ABC_Condition {
private static Lock lock = new ReentrantLock();
private static Condition A = lock.newCondition();
private static Condition B = lock.newCondition();
private static Condition C = lock.newCondition();
private static int count = 0;
static class ThreadA extends Thread {
@Override
public void run() {
try {
lock.lock();
for (int i = 0; i < 10; i++) {
while (count % 3 != 0)//注意這裡是不等於0,也就是說在count % 3為0之前,當前執行緒一直阻塞狀態
A.await(); // A釋放lock鎖
System.out.print("A");
count++;
B.signal(); // A執行完喚醒B執行緒
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
static class ThreadB extends Thread {
@Override
public void run() {
try {
lock.lock();
for (int i = 0; i < 10; i++) {
while (count % 3 != 1)
B.await();// B釋放lock鎖,當前面A執行緒執行後會通過B.signal()喚醒該執行緒
System.out.print("B");
count++;
C.signal();// B執行完喚醒C執行緒
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
static class ThreadC extends Thread {
@Override
public void run() {
try {
lock.lock();
for (int i = 0; i < 10; i++) {
while (count % 3 != 2)
C.await();// C釋放lock鎖,當前面B執行緒執行後會通過C.signal()喚醒該執行緒
System.out.print("C");
count++;
A.signal();// C執行完喚醒A執行緒
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
new ThreadA().start();
new ThreadB().start();
new ThreadC().start();
}
}
四、Semaphore訊號量方式
1、基本思路
Semaphore又稱訊號量,是作業系統中的一個概念,在Java併發程式設計中,訊號量控制的是執行緒併發的數量。
public Semaphore(int permits)
其中引數permits就是允許同時執行的執行緒數目;
Semaphore是用來保護一個或者多個共享資源的訪問,Semaphore內部維護了一個計數器,其值為可以訪問的共享資源的個數。一個執行緒要訪問共享資源,先獲得訊號量,如果訊號量的計數器值大於1,意味著有共享資源可以訪問,則使其計數器值減去1,再訪問共享資源。如果計數器值為0,執行緒進入休眠。當某個執行緒使用完共享資源後,釋放訊號量,並將訊號量內部的計數器加1,之前進入休眠的執行緒將被喚醒並再次試圖獲得訊號量。
Semaphore使用時需要先構建一個引數來指定共享資源的數量,Semaphore構造完成後即是獲取Semaphore、共享資源使用完畢後釋放Semaphore。
Semaphore semaphore = new Semaphore(3,true);
semaphore.acquire();
//do something here
semaphore.release();
2、程式碼
import java.util.concurrent.Semaphore;
public class ABC_Semaphore {
// 以A開始的訊號量,初始訊號量數量為1
private static Semaphore A = new Semaphore(1);
// B、C訊號量,A完成後開始,初始訊號數量為0
private static Semaphore B = new Semaphore(0);
private static Semaphore C = new Semaphore(0);
static class ThreadA extends Thread {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
A.acquire();// A獲取訊號執行,A訊號量減1,當A為0時將無法繼續獲得該訊號量
System.out.print("A");
B.release();// B釋放訊號,B訊號量加1(初始為0),此時可以獲取B訊號量
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class ThreadB extends Thread {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
B.acquire();
System.out.print("B");
C.release();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class ThreadC extends Thread {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
C.acquire();
System.out.println("C");
A.release();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
new ThreadA().start();
new ThreadB().start();
new ThreadC().start();
}
}