1. 程式人生 > >三執行緒按順序交替列印ABC的四種方法

三執行緒按順序交替列印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();
    }
}