面试记录之synchronized的惨败经历
热乎的啊。刚面试完
面试题描述
3个线程,线程1打印A,线程2打印B,线程3打印C,循环n次
面试题分析
这个题有两个地方要考虑:第一,三个线程有序执行,A->B->C,第二,共循环10次。
那么有序执行可以通过锁+条件变量,循环10次可以粗暴的通过一个变量来控制。
synchronized+wait,ReentrantLock+Condition就可以。还有一个:信号量。我们知道synchronized+wait,ReentrantLock+Condition是标准的Java中的管程实现,那信号量也可以用来实现管程。
落地很重要,因为实现的时候会有很多意想不到的问题。
思路一:锁+条件变量
实现一:synchronized+wait+notify
synchronized+变量i来判断+wait+notify
Error实例:
public class PrintError {
private static volatile int i = 0;
private static final Object mutex = new Object();
//method1:sync+wai+notifyAll
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
System.out.println("=====threadA=====");
synchronized (mutex) {
if (i % 3 == 0) {
System.out.println("A");
i++;
mutex.notifyAll();
} else {
try {
mutex.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread threadB = new Thread(() -> {
System.out.println("=====threadB=====");
synchronized (mutex) {
if (i % 3 == 1) {
System.out.println("B");
i++;
mutex.notifyAll();
} else {
try {
mutex.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread threadC = new Thread(() -> {
System.out.println("=====threadC=====");
synchronized (mutex) {
if (i % 3 == 2) {
System.out.println("C");
i++;
} else {
try {
mutex.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
threadA.start();
threadB.start();
threadC.start();
//我的想法是,循环n次。。而当时面试官说了不让用thread.join。。orz
while(i<3*n){
Thread.sleep(500);
}
}
}
错误分析:
我的思路是:线程A,B,C。假设线程A来了,正好获取到锁,此时i=0,发现自己可以打印,然后i++,然后唤醒B跟C,然后她们俩再去打印。也就是说我认为B跟C被唤醒后是会从头执行的,看下运行结果:
运行结果:
emmm。。?我当时的反应是懵逼的,然后并没有反应过来是哪里有问题。注意看左侧,程序已经结束了,也就是说线程A跟线程B线程C在一轮循环之后,线程的生命周期已经结束了。
验证一下:
当然。是面试完验证的QAQ,如果面试的时候反应过来也不至于说是悲惨的经验了。。mmmp。。。
threadA执行完notifyAll后,线程状态:
也就是说线程A此时生命周期已经结束了
接下来面试官开始二连问:
被问倒の问题一:
while(i<3*n){
Thread.sleep(500);
}
这段代码有用吗?显然是没用的。。
我最开始为什么要加上这行,其实是因为想在主线程对变量i做控制,并且想让主线程等子线程结束(为什么要等子线程结束呢。。你别问我。。我也不知道,当时下意识就这么些写了)。当然,这也就是我路走窄的第一个地方。
被问倒の问题二:
上面三个线程什么时候结束?你这样写能打印N次吗?要不你来说说线程声明周期?
被问倒の问题三:
线程wait又被唤醒后,应该从哪里执行?wait方法下一行还是从头?
路走窄第二个地方:(说实话我的写法就表明了我错的离谱:我认为被唤醒后是从头开始执行的QAQ)
测试demo
public class WaitNotify {
public static void main(String[] args) {
Object mutex=new Object();
Thread threadB = new Thread(() -> {
while(true){
synchronized (mutex) {
System.out.println("threadB working...");
try {
System.out.println("threadB waiting...");
mutex.notifyAll();
mutex.wait();
System.out.println("threadB back!!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread threadC = new Thread(() -> {
while(true){
synchronized (mutex) {
System.out.println("threadC working...");
try {
System.out.println("threadC waiting...");
mutex.notifyAll();
mutex.wait();
System.out.println("threadC back!!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
threadC.start();
threadB.start();
}
}
Thread threadC = new Thread(() -> {
while(true){
synchronized (mutex){
if (i % 3 == 2) {
System.out.println("C");
i++;
}else{
try {
mutex.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
测试结果:
事实证明啊。是从wait的下一行开始执行的。
threadB working...
threadB waiting...
threadC back!!
threadC working...
threadC waiting...
正确的代码实例:
public class Print {
private static volatile int i=0;
private static final Object mutex=new Object();
//method1:sync+wai+notifyAll
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
while(true){
synchronized (mutex){
if (i % 3 == 0) {
System.out.println("A");
i++;
mutex.notifyAll();
}else{
try {
mutex.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
Thread threadB = new Thread(() -> {
while(true){
synchronized (mutex){
if (i % 3 == 1) {
System.out.println("B");
i++;
mutex.notifyAll();
}else{
try {
mutex.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
Thread threadC = new Thread(() -> {
while(true){
synchronized (mutex){
if (i % 3 == 2) {
System.out.println("C");
i++;
}else{
try {
mutex.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
threadWe.start();
threadNeed.start();
threadYou.start();
}
}
实现二:ReentrantLock+Condition
既然刚是用synchronized+变量i判断谁来输出+条件变量,那么ReentrantLock+Condition也是可以的
ReentrantLock错误示范:
public class ABC_ReetrantLockError {
private static final Lock lock = new ReentrantLock();
private static final Condition conditionA = lock.newCondition();
private static final Condition conditionB = lock.newCondition();
private static final Condition conditionC = lock.newCondition();
private static volatile int i;
public static void main(String[] args) {
//假设循环n次
Thread threadA = new Thread(() -> {
//这里有问题,到最后一次时,threadA是阻塞在conditionA。await上,被唤醒后,会继续往下走
while (i < 30) {
try {
lock.lock();
while (i % 3 != 0) {
try {
conditionA.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print("A");
i++;
conditionB.signal();
} finally {
lock.unlock();
}
}
});
Thread threadB = new Thread(() -> {
while (i < 30) {
try {
lock.lock();
while (i % 3 != 1) {
try {
conditionB.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print("B");
i++;
conditionC.signal();
} finally {
lock.unlock();
}
}
});
Thread threadC = new Thread(() -> {
while (i < 30) {
try {
lock.lock();
while (i % 3 != 2) {
try {
conditionC.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("C");
i++;
conditionA.signal();
} finally {
lock.unlock();
}
}
});
threadA.start();
threadB.start();
threadC.start();
}
}
正确示范:
先唤醒再判断条件i,并且需要主线程来唤醒线程A。再把重复的代码抽出来,over。
问把大象装冰箱分为几步?😃
public class ABC_ReetrantLockRight {
private static final Lock lock = new ReentrantLock();
private static final Condition conditionA = lock.newCondition();
private static final Condition conditionB = lock.newCondition();
private static final Condition conditionC = lock.newCondition();
private static volatile int i;
public static void main(String[] args) {
Thread threadA = new Thread(() -> run(conditionA, conditionB, "A"));
Thread threadB = new Thread(() -> run(conditionB, conditionC, "B"));
Thread threadC = new Thread(() -> run(conditionC, conditionA, "C"));
threadA.start();
threadB.start();
threadC.start();
new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock();
conditionA.signal();
} finally {
lock.unlock();
}
}
}).start();
}
private static void run(Condition awaitCondition, Condition signalCondition, String result) {
while (true) {
try {
lock.lock();
try {
awaitCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
signalCondition.signal();
//假设循环10次
if (i >= 30) {
break;
}
System.out.print(result);
i++;
} finally {
lock.unlock();
}
}
}
}
思路二:CyclicBarrier
因为看到了N轮,所以就想到了CyclicBarrier,但是还是需要条件变量保证顺序。
所以是可以,但是没必要啊。
思路三:信号量
根据要求,起几个线程搞几个信号量,然后按照顺序循环给信号量塞值即可。
public class ABC_SemaphoreRight {
private static final Semaphore semaphoreA = new Semaphore(1);
private static final Semaphore semaphoreB = new Semaphore(0);
private static final Semaphore semaphoreC = new Semaphore(0);
private static volatile int i;
public static void main(String[] args) {
Thread threadA = new Thread(() -> run(semaphoreA, semaphoreB, "A"));
Thread threadB = new Thread(() -> run(semaphoreB, semaphoreC, "B"));
Thread threadC = new Thread(() -> run(semaphoreC, semaphoreA, "C"));
threadA.start();
threadB.start();
threadC.start();
}
private static void run(Semaphore signalSemaphore, Semaphore awaitSemaphore, String result) {
while (true) {
try {
signalSemaphore.acquire();
//假设循环10次
if (i >= 30) {
break;
}
System.out.print(result);
i++;
awaitSemaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
知识扩展篇
你以为到上面就结束了?并不。接下来继续看下线程的几个同步器:
慢慢补吧。。睡觉了orz