[JAVA][面試][多線程]多方法解決循環打印1~n的數字

Summary

實現方法:

  1. volatile
  2. synchronized
  3. wait()/notifyAll()
  4. RetrantLock
  5. LockSupport
  6. Semaphore

從實現方式上來看,【方法1】和【方法2】都是基於JAVA語言的原生實現。
【方法1】採用的是共享內存的方式,【方法6】採用的是消息傳遞,都是借線程通信的方式控制併發;【方法2】、【方法3】、【方法4】、【方法5】採用的是加鎖形式的併發控制。

  • 除了這幾種方法外,還有管道、FIFO(同步隊列)等實現方式,此略。

注:本質上,該問題是併發控制問題,因此可以採用線程通信的方式解決;而線程通信主要有兩種機制:共享內存消息傳遞,而消息傳遞亦可以基於共享內存來間接實現(非嚴格實現)。即,JAVA的併發採用的是隱式共享內存模型,但是在此基礎上,併發包中諸如信號量等消息傳遞的線程通信機制亦已基於共享內存的方式非嚴格間接實現。

  • 除標明外,爲了篇幅間接,本文中僅提供核心代碼,完整代碼及細節見:@gist@碼雲

1. volatile

要注意的是,volatile本身無原子性(如,a++),但對單個變量的讀寫具有原子性(如,a=1)。

volatile 關鍵字保證了共享變量的 ”可見性“,及其通過總線鎖、內存屏障(解決重排序問題)的方式,在不引起線程上下文切換的狀態下(因此比synchronized效率高),使一個變量總是對所有線程可見。

  • 這不是一個高效的解決方式,但是是一個面試官可以接受的回答。這裏採用自旋的方式,效率較低。

在本例中的實現代碼如下(完整):

public class syn {
    private static volatile int order = 0;  // 使用 volatile
    private static int n = 10;
    
    public static void main(String[] args){
        for(int i=0; i<syn.n; i++){
            new Thread(new R(i)).start();
        }
    }

    static class R implements Runnable{
        private int id;
        public R(int id){this.id = id;}

        @Override
        public void run() {
            while(true){
                if(syn.order == this.id){ // 此處靠只讀,保證原子性
                    System.out.println(this.id);
                    syn.order = (syn.order+1) % syn.n; // 保證了此處的原子性
                }
            }
        }
    }
}

也可以選擇在共享變量上自旋:

public void run() {
    while(true){
        while(syn.order != this.id){} // 自旋

        System.out.println(this.id);
        syn.order = (syn.order+1) % syn.n;
    }
}

2. synchronized

重量級鎖,實現思路也很簡單,基於synchronized構建一個自旋鎖的結構即可(但此處並不是一個自旋鎖),效率很低。至於爲什麼採用自旋鎖,這是因爲本例中其實是一個併發轉同步的問題,線程間彼此無法控制彼此的次序,因此只能靠一個全局變量synsyn.order(不爲volatile)來進行線程通信,synchronized鎖結構保證synsyn.order變量的可見性和一致性。相比於【方法1】,等於是把volatile基於鎖結構自行實現了一遍。

自旋鎖(SpinLock):如果鎖被其它線程獲取,則進入循環等待階段——不斷判斷是否可以獲得鎖。容易造成busy-waiting。

static class R implements Runnable{
    private int id;
    private final Object lock = new Object();

    public R(int id){this.id = id;}

    @Override
    public void run() {
        while(true){
            synchronized (lock){ // 直接加鎖,效率低
                if(synsyn.order == this.id){
                    System.out.println(this.id);
                    synsyn.order = (synsyn.order+1) % synsyn.n;
                }
            }
        }
    }
}

3. wait()/notifyAll()

在本例中,在自旋鎖的實現方式下,本質上和【方法2】一致,只不過自行控制了鎖釋放和鎖等待還容易出bug,不推薦。

static class R implements Runnable{
    private int id;
    private Object lock = new Object();

    public R(int id){this.id = id;}

    @Override
    public void run() {
        while(true){
            synchronized (lock){ // 注意 while 和 synchronized的 次序
                if(synnotify.order == this.id){
                    System.out.println(this.id);
                    synnotify.order = (synnotify.order+1) % synnotify.n;
                    lock.notifyAll();
                } else {
                    try {
                        lock.wait();
                    } catch (InterruptedException ignore){}
                }
            }
        }
    }
}

4. RetrantLock

雖然本質上也是自旋鎖(但採用的是同步隊列AbstractQueuedSynchronizer,效率更高)的結構,但是採用了非公平鎖,能提升部分效率。

private static ReentrantLock lock = new ReentrantLock(); // unfair lock
static class R implements Runnable{
    private int id;
    public R(int id){this.id = id;}

    @Override
    public void run() {
        while(true){
            lock.lock(); // 本質上還是個自旋鎖
            if(synlock.order == this.id){
                System.out.println(this.id);
                synlock.order = (synlock.order+1) % synlock.n;
            }
            lock.unlock();
        }
    }
}

5. LockSupport

LockSupport是一個線程阻塞工具類,操作上更符合直觀,例如下文中採用一個有序list保存所有線程,然後讓線程在完成操作後依次解鎖下一個要執行的線程,效率較高,本質上和【方法6】相同,都是通過控制信號量的方式進行線程通信,並以此完成併發控制。

private static LinkedList<Thread> list = new LinkedList<>();
// LockSupport.unpark(list.get(0));
static class R implements Runnable{
    private int id;
    R(int id){this.id = id; }

    @Override
    public void run(){
        while (true){
            LockSupport.park(); // 鎖住每一個線程,直到被解鎖
            System.out.println(this.id);
            synjoin.order  = (synjoin.order + 1) % synjoin.n;
            LockSupport.unpark(list.get(synjoin.order)); // 解鎖下一個線程
        }
    }
}

6. Semaphore

信號量解法是本例中最推薦的解法,效率高,是面試官最願意聽到的解法。這裏可以直接基於JDK實現,你也可以通過volatile來實現這個信號量算法。

基於JDK實現的完整代碼:

import java.util.concurrent.Semaphore;

public class synsemaphore {
    private static int order = 0;
    private static int n = 10;
    private static Semaphore[] semaphores = new Semaphore[synsemaphore.n];

    public static void main(String[] args) throws InterruptedException{

        for(int i=0; i<synsemaphore.n; i++){
            semaphores[i] = new Semaphore(1);
            semaphores[i].acquire();
            new Thread(new R(i)).start();
        }
        semaphores[0].release();
    }


    static class R implements Runnable{
        private int id;
        public R(int id){this.id = id;}

        @Override
        public void run() {
            while(true){
                try{
                    synsemaphore.semaphores[this.id].acquire();
                } catch (InterruptedException ignore){}

                System.out.println(this.id);
                synsemaphore.order = (synsemaphore.order+1) % synsemaphore.n;

                synsemaphore.semaphores[synsemaphore.order].release();
            }
        }
    }
}

也可以不用list結構完成信號量的控制,本質上就是上一個線程持有下一個線程的信號量,構成一個迴環:

import java.util.concurrent.Semaphore;

public class synse2 {
    private static int n = 10;

    public static void main(String[] args) throws InterruptedException{
        Semaphore crn = new Semaphore(1); crn.acquire();
        Semaphore next = new Semaphore(1); next.acquire();
        Semaphore head = crn;
        for(int i=0; i<synse2.n; i++) {
            if(i==synse2.n-1) new Thread(new R(i, crn, head)).start();
            else new Thread(new R(i, crn, next)).start();
            crn = next;
            next = new Semaphore(1);
            next.acquire();
        }

        head.release();
    }

    static class R implements Runnable{
        private int id;
        private Semaphore me, next;

        public R(int id, Semaphore me, Semaphore next){
            this.id = id;
            this.me = me;
            this.next = next;
        }

        @Override
        public void run() {
            while(true){
                try {
                    this.me.acquire();
                    System.out.println(this.id);
                    this.next.release();
                } catch (InterruptedException ignore){}
            }
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章