3

最近遇到个Java面试题,还有点意思呢。

 2 years ago
source link: https://blog.51cto.com/u_15357269/5449830
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

最近看到一个面试题目,感觉挺有意思的,大意如下:

最近遇到个Java面试题,还有点意思呢。_面试

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();
}
}

暂时想了这么些,方案还是比较多的,大家可以开动脑筋,头脑风暴吧。

看看你还有什么骚操作,可以在评论区留言。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK