前言
最近兩個月寫文章很少,因為自己學習狀態也不是很好,我看了下,上一篇文章,都是一個月前了。
不知道大家有沒有感覺,小學初中讀的一些書,看的一些文章,到現在都印象深刻,反倒是高中學的知識,高考後就慢慢消散,直到遺忘。
我想說的是,記得初中學過魯迅的《藤野先生》,裡面有一段話,大意是:久了不聯絡,有時候想聯絡,卻又無從下筆,到最後就更是不了了之了。
我找了下原文:
將走的前幾天,他叫我到他家裡去,交給我一張照相,後面寫著兩個字道:“惜別”,還說希望將我的也送他。但我這時適值沒有照相了;他便叮囑我將來照了寄給他,並且時時通訊告訴他此後的狀況。
我離開仙台之後,就多年沒有照過相,又因為狀況也無聊,說起來無非使他失望,便連信也怕敢寫了。經過的年月一多,話更無從說起,所以雖然有時想寫信,卻又難以下筆,這樣的一直到現在,竟沒有寄過一封信和一張照片。從他那一面看起來,是一去之後,杳無訊息了。
其實寫文章也是這樣的,久了不寫更不想寫,但是心裡又時時記著這麼個事情,玩也不是很自在;今天先隨便寫一下,找下狀態吧,因為現在文章可能在部落格和公眾號發,比如部落格,一般來說會隨意點,但是公眾號的話,一般大家質量要求會高一些,結果就是,為了追求高質量,而非要找到一些很厲害的技術點,或者自己研究透了才動筆,這樣會導致一些想法難產,因為可能覺得很簡單,不值得發到公眾號,實際上,很多時候都是浮於表面地覺得很簡單,一旦深挖,立馬就廢。
扯這麼多,也是給我自己,或者其他剛開始寫技術公眾號的同學,也不用覺得非要寫的多麼多麼好才發出來,本來大家都是一步一步來的,各種大佬也不是一下就變成大佬的,把自己的學習過程和成長過程發出來,大家也就知道:哦,大佬原來也這麼菜啊,哈哈。
比如最近看到一些演算法大佬,一開始也是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程式猿,部落格裡有我更多的一些文章,大家可以看看,暫時沒有遷移到公眾號的打算。