曹工說面試題:一個執行緒協同問題,解法繁多,都要被玩壞了,趁著沒壞,一起玩吧

三國夢迴發表於2020-09-26

前言

最近兩個月寫文章很少,因為自己學習狀態也不是很好,我看了下,上一篇文章,都是一個月前了。

不知道大家有沒有感覺,小學初中讀的一些書,看的一些文章,到現在都印象深刻,反倒是高中學的知識,高考後就慢慢消散,直到遺忘。

我想說的是,記得初中學過魯迅的《藤野先生》,裡面有一段話,大意是:久了不聯絡,有時候想聯絡,卻又無從下筆,到最後就更是不了了之了。

我找了下原文:

將走的前幾天,他叫我到他家裡去,交給我一張照相,後面寫著兩個字道:“惜別”,還說希望將我的也送他。但我這時適值沒有照相了;他便叮囑我將來照了寄給他,並且時時通訊告訴他此後的狀況。

我離開仙台之後,就多年沒有照過相,又因為狀況也無聊,說起來無非使他失望,便連信也怕敢寫了。經過的年月一多,話更無從說起,所以雖然有時想寫信,卻又難以下筆,這樣的一直到現在,竟沒有寄過一封信和一張照片。從他那一面看起來,是一去之後,杳無訊息了。

其實寫文章也是這樣的,久了不寫更不想寫,但是心裡又時時記著這麼個事情,玩也不是很自在;今天先隨便寫一下,找下狀態吧,因為現在文章可能在部落格和公眾號發,比如部落格,一般來說會隨意點,但是公眾號的話,一般大家質量要求會高一些,結果就是,為了追求高質量,而非要找到一些很厲害的技術點,或者自己研究透了才動筆,這樣會導致一些想法難產,因為可能覺得很簡單,不值得發到公眾號,實際上,很多時候都是浮於表面地覺得很簡單,一旦深挖,立馬就廢。

扯這麼多,也是給我自己,或者其他剛開始寫技術公眾號的同學,也不用覺得非要寫的多麼多麼好才發出來,本來大家都是一步一步來的,各種大佬也不是一下就變成大佬的,把自己的學習過程和成長過程發出來,大家也就知道:哦,大佬原來也這麼菜啊,哈哈。

比如最近看到一些演算法大佬,一開始也是10道演算法題,全部都要看答案的好麼。。

扯了不少,言歸正傳吧,最近在網上看到一個面試題目,感覺挺有意思的,大意如下:

ok,大家看到這個題,可以先理解下,這裡啟動了兩個執行緒,a和b,但是雖然說a在b之前start,不一定就可以保證執行緒a的邏輯,可以先於執行緒b執行,所以,這裡的意思是,執行緒a和b,執行順序互不干擾,我們不應該假定其中一個執行緒可以先於另外一個執行。

另外,既然是面試題,那常規做法自然是不用上了,比如讓b先sleep幾秒鐘之類的,如果真這麼答,那可能面試就結束了吧。

ok,我們下面開始分析解法。

可見性保證

程式裡定義了一個全域性變數,var = 1;執行緒a會修改這個變數為2,執行緒b則在變數為2時,執行自己的業務邏輯。

那麼,這裡首先,我們要做的是,先講var使用volatile修飾,保證多執行緒操作時的可見性。

public static volatile int var = 1;

解法分析

經過前面的可見性保證的分析,我們知道,要想達到目的,其實就是要保證:

a中的對var+1的操作,需要先於b執行。

但是,現在的問題是,兩個執行緒同時啟動,不知道誰先誰後,怎麼保證a先執行,b後執行呢?

讓執行緒b先不執行,大概有兩種思路,一種是阻塞該執行緒,一種是不阻塞該執行緒,阻塞的話,我們可以想想,怎麼阻塞一個執行緒。

大概有:

  • synchronized,取不到鎖時,阻塞
  • java.util.concurrent.locks.ReentrantLock#lock,取不到鎖時,阻塞
  • object.wait,取到synchronized了,但是因為一些條件不滿足,執行不下去,呼叫wait,將釋放鎖,並進入等待佇列,執行緒暫停執行
  • java.util.concurrent.locks.Condition.await,和object.wait類似,只不過object.wait在jvm層面,使用c++實現,Condition.await在jdk層面使用java語言實現
  • threadA.join(),等待對應的執行緒threadA執行完成後,本執行緒再繼續執行;threadA沒結束,則當前執行緒阻塞;
  • CountDownLatch#await,在對應的state不為0時,阻塞
  • Semaphore#acquire(),在state為0時(即剩餘令牌為0時),阻塞
  • 其他阻塞佇列、FutureTask等等

如果不讓執行緒進入阻塞,則一般可以讓執行緒進入一個while迴圈,迴圈的退出條件,可以由執行緒a來修改,執行緒a修改後,執行緒b跳出迴圈。

比如:

volatile boolean stop = false;
while (!stop){
    ...
}

上面也說了這麼多了,我們實際上手寫一寫吧。

錯誤解法1--基於wait

下面的思路是基於wait、notify;執行緒b直接wait,執行緒a在修改了變數後,進行notify。

public class Global1 {
    public static volatile int var = 1;
    public static final Object monitor = new Object();

    public static void main(String[] args) {
        Thread a = new Thread(() -> {
            // 1
            Global1.var++;
            // 2
            synchronized (monitor) {
                monitor.notify();
            }
        });
        Thread b = new Thread(() -> {
            // 3
            synchronized (monitor) {
                try {
                    monitor.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 4
            if (Global1.var == 2) {
                //do something;
                System.out.println(Thread.currentThread().getName() + " good job");
            }
        });
        a.start();
        b.start();
    }
}

大家覺得這個程式碼能行嗎?實際是不行的。因為實際的順序可能是:

執行緒a--1

執行緒a--2

執行緒b--1

執行緒b--2

線上程a-2時,執行緒a去notify,但是此時執行緒b還沒開始wait,所以此時的notify是沒有任何效果的:沒人在等,notify個錘子。

怎麼修改,本方案才行得通呢?

那就是,修改執行緒a的程式碼,不要急著notify,先等等。

        Thread a = new Thread(() -> {
            Global1.var++;
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (monitor) {
                monitor.notify();
            }
        });

但是這樣的話,明顯不合適,有作弊嫌疑,也不優雅。

錯誤解法2--基於condition的signal

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class Global1 {
    public static volatile int var = 1;
    public static final ReentrantLock reentrantLock = new ReentrantLock();
    public static final Condition condition = reentrantLock.newCondition();

    public static void main(String[] args) {
        Thread a = new Thread(() -> {
            Global1.var++;
            final ReentrantLock lock = reentrantLock;
            lock.lock();
            try {
                condition.signal();
            } finally {
                lock.unlock();
            }
        });
        Thread b = new Thread(() -> {
            final ReentrantLock lock = reentrantLock;
            lock.lock();
            try {
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }

            if (Global1.var == 2) {
                //do something;
                System.out.println(Thread.currentThread().getName() + " good job");
            }
        });
        a.start();
        b.start();
    }
}

這個方案使用了Condition物件來實現object的notify、wait效果。當然,這個也有同樣的問題。

正確解法1--基於錯誤解法2進行改進

我們看看,前面問題的根源在於,我們執行緒a,在去通知執行緒b的時候,有可能執行緒b還沒開始wait,所以此時通知失效。

那麼,我們是不是可以先等等,等執行緒b開始wait了,再去通知呢?

        Thread a = new Thread(() -> {
            Global1.var++;
            final ReentrantLock lock = reentrantLock;
            lock.lock();
            try {
                // 1
                while (!reentrantLock.hasWaiters(condition)) {
                    Thread.yield();
                }
                condition.signal();
            } finally {
                lock.unlock();
            }
        });

1處程式碼,就是這個思想,在signal之前,判斷當前condition上是否有waiter執行緒,如果沒有,就死迴圈;如果有,才去執行signal。

這個方法實測是可行的。

正確解法2

對正確解法1,換一個api,就變成了正確解法2.

Thread a = new Thread(() -> {
    Global1.var++;
    final ReentrantLock lock = reentrantLock;
    lock.lock();
    try {
        // 1
        while (reentrantLock.getWaitQueueLength(condition) == 0) {
            Thread.yield();
        }
        condition.signal();
    } finally {
        lock.unlock();
    }
});

1這裡,獲取condition上等待佇列的長度,如果為0,說明沒有等待者,則死迴圈。

正確解法3--基於Semaphore

剛開始,我們初始化一個訊號量,state為0. 執行緒b去獲取訊號量的時候,就會阻塞。

然後我們執行緒a再去釋放一個訊號量,此時執行緒b就可以繼續執行。

public class Global1 {
    public static volatile int var = 1;
    public static final Semaphore semaphore = new Semaphore(0);

    public static void main(String[] args) {
        Thread a = new Thread(() -> {
            Global1.var++;
            semaphore.release();
        });
        a.setName("thread a");
        Thread b = new Thread(() -> {
            try {
                semaphore.acquire();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if (Global1.var == 2) {
                //do something;
                System.out.println(Thread.currentThread().getName() + " good job");
            }
        });
        b.setName("thread b");
        a.start();
        b.start();
    }
}

正確解法4--基於CountDownLatch

public class Global1 {
    public static volatile int var = 1;
    public static final CountDownLatch countDownLatch = new CountDownLatch(1);

    public static void main(String[] args) {
        Thread a = new Thread(() -> {
            Global1.var++;
            countDownLatch.countDown();
        });
        a.setName("thread a");
        Thread b = new Thread(() -> {
            try {
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if (Global1.var == 2) {
                //do something;
                System.out.println(Thread.currentThread().getName() + " good job");
            }
        });
        b.setName("thread b");
        a.start();
        b.start();
    }
}

正確解法5--基於BlockingQueue

這裡使用了ArrayBlockingQueue,其他的阻塞佇列也是可以的。

import countdown.CountdownTest;


public class Global1 {
    public static volatile int var = 1;
    public static final ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<Object>(1);

    public static void main(String[] args) {
        Thread a = new Thread(() -> {
            Global1.var++;
            arrayBlockingQueue.offer(new Object());
        });
        a.setName("thread a");
        Thread b = new Thread(() -> {
            try {
                arrayBlockingQueue.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if (Global1.var == 2) {
                //do something;
                System.out.println(Thread.currentThread().getName() + " good job");
            }
        });
        b.setName("thread b");
        a.start();
        b.start();
    }
}

正確解法6--基於FutureTask

我們也可以讓執行緒b等待一個task的執行結果;而執行緒a在執行完修改var為2後,執行該任務,任務執行完成後,執行緒b就會被通知繼續執行。

public class Global1 {
    public static volatile int var = 1;
    public static final FutureTask futureTask = new FutureTask<Object>(new Callable<Object>() {
        @Override
        public Object call() throws Exception {
            System.out.println("callable task ");
            return null;
        }
    });

    public static void main(String[] args) {
        Thread a = new Thread(() -> {
            Global1.var++;
            futureTask.run();
        });
        a.setName("thread a");
        Thread b = new Thread(() -> {
            try {
                futureTask.get();
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }

            if (Global1.var == 2) {
                //do something;
                System.out.println(Thread.currentThread().getName() + " good job");
            }
        });
        b.setName("thread b");
        a.start();
        b.start();
    }
}

正確解法7--基於join

這個可能是最簡潔直觀的,哈哈。也是群裡同學們提供的解法,真的有才!

public class Global1 {
    public static volatile int var = 1;

    public static void main(String[] args) {
        Thread a = new Thread(() -> {
            Global1.var++;
        });
        a.setName("thread a");
        Thread b = new Thread(() -> {
            try {
                a.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if (Global1.var == 2) {
                //do something;
                System.out.println(Thread.currentThread().getName() + " good job");
            }
        });
        b.setName("thread b");
        a.start();
        b.start();
    }
}

正確解法8--基於CompletableFuture

這個和第6種類似。都是基於future。

public class Global1 {
    public static volatile int var = 1;
    public static final CompletableFuture<Object> completableFuture =
            new CompletableFuture<Object>();

    public static void main(String[] args) {
        Thread a = new Thread(() -> {
            Global1.var++;
            completableFuture.complete(new Object());
        });
        a.setName("thread a");
        Thread b = new Thread(() -> {
            try {
                completableFuture.get();
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }

            if (Global1.var == 2) {
                //do something;
                System.out.println(Thread.currentThread().getName() + " good job");
            }
        });
        b.setName("thread b");
        a.start();
        b.start();
    }
}

非阻塞--正確解法9--忙等待

這種程式碼量也少,只要執行緒b在變數為1時,死迴圈就行了。

public class Global1 {
    public static volatile int var = 1;

    public static void main(String[] args) {
        Thread a = new Thread(() -> {
            Global1.var++;
        });
        a.setName("thread a");
        Thread b = new Thread(() -> {
            while (var == 1) {
                Thread.yield();
            }

            if (Global1.var == 2) {
                //do something;
                System.out.println(Thread.currentThread().getName() + " good job");
            }
        });
        b.setName("thread b");
        a.start();
        b.start();
    }
}

非阻塞--正確解法10--忙等待

忙等待的方案很多,反正就是某個條件不滿足時,不阻塞自己,阻塞了會釋放cpu,我們就是不希望釋放cpu的。

比如像下面這樣也可以。

public class Global1 {
    public static volatile int var = 1;
    public static final AtomicInteger atomicInteger =
            new AtomicInteger(1);

    public static void main(String[] args) {
        Thread a = new Thread(() -> {
            Global1.var++;
            atomicInteger.set(2);
        });
        a.setName("thread a");
        Thread b = new Thread(() -> {
            while (true) {
                boolean success = atomicInteger.compareAndSet(2, 1);
                if (success) {
                    break;
                } else {
                    Thread.yield();
                }
            }

            if (Global1.var == 2) {
                //do something;
                System.out.println(Thread.currentThread().getName() + " good job");
            }
        });
        b.setName("thread b");
        a.start();
        b.start();
    }
}

小結

暫時想了這麼寫,方案還是比較多的,大家可以開動腦筋,頭腦風暴吧!我是逐日,混跡成都的老java程式猿,部落格裡有我更多的一些文章,大家可以看看,暫時沒有遷移到公眾號的打算。

相關文章