最近看到一个面试题目,感觉挺有意思的,粗心如下:
ok,大家看到这个题,能够先了解下,这里启动了两个线程,a 和 b,然而尽管说 a 在 b 之前 start,不肯定就能够保障线程 a 的逻辑,能够先于线程 b 执行。
所以,这里的意思是,线程 a 和 b,执行程序互不烦扰,咱们不应该假设其中一个线程能够先于另外一个执行。
另外,既然是面试题,那惯例做法天然是不必上了,比方让 b 先 sleep 几秒钟之类的,如果真这么答,那可能面试就完结了吧。
好,咱们上面开始剖析解法。
可见性保障
程序里定义了一个全局变量,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,其余的阻塞队列也是能够的。
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();
}
}
临时想了这么些,计划还是比拟多的,大家能够开动脑筋,头脑风暴吧。
看看你还有什么骚操作,能够在评论区留言。
发表回复