【多线程与高并发】线程间通信的两个面试问题——两个线程交替打印奇数和偶数
线程间通信
同个进程下的线程之间都是共享进程的资源,只要是共享变量都可以做到线程间通信,比如全局变量
对于线程间关注的不是通信方式,而是关注多线程竞争共享资源的问题,信号量也同样可以在线程间实现互斥与同步:
- 互斥的方式,可保证任意时刻只有一个线程访问共享资源;
- 同步的方式,可保证线程 A 应在线程 B 之前执行;
常见问题:两个线程交替打印奇数和偶数
实际就是wait()、notify()、notifyAll()的问题
还可以用来实现数据库连接池等操作,即没有可用线程时使用wait等待,当其他线程执行完成之后使用notifyAll()通知其他线程拿资源
两线程奇偶数打印
讨巧的方法:用一个线程进行循环,在每次循环里面都会做是奇数还是偶数的判断,然后打印这个我们想要的结果。
正确解法:需要控制两个线程的执行顺序,偶线程执行完之后奇数线程执行,有点像通知机制,偶线程通知奇线程,奇线程再通知偶线程。而一看到通知/等待,立马想到wait和notify。
package Mytest;/**
* Copyright (C), 2019-2021
* author candy_chen
* date 2021/5/6 15:45
*
* @Classname 两线程奇偶数打印
* Description: 测试
*/
/**
* 通过notify和wait用来控制我们线程的执行
*/
class 两个线程打印奇偶数 {
static class SolutionTask implements Runnable{
static int value = 0;
@Override
public void run() {
//把加锁放在外面更好些,没有加锁的话,两个线程会同时进来,会多打印一遍,也就是可能执行101次的情况
synchronized (SolutionTask.class){判
while (value <= 100){
System.out.println(Thread.currentThread().getName() + " :" + value++);
SolutionTask.class.notify();//通知
try {
SolutionTask.class.wait();//通知后等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) {
new Thread(new SolutionTask(),"偶数").start();
new Thread(new SolutionTask(),"奇数").start();
}
}
这里我们有两个线程,通过notify和wait用来控制我们线程的执行,从而打印出我们目标的结果
N个线程循环打印
public class N个线程循环打印 {
public static class test1 implements Runnable{
private static final Object LOCK = new Object();
//当前即将打印的数字
private static int current = 0;
//当前线程编号,从0开始
private int threadNo;
//线程数量
private int threadCount;
//打印的最大数量
private int maxInt;
public test1(int threadNo,int threadCount,int maxInt){
this.threadNo = threadNo;
this.threadCount = threadCount;
this.maxInt = maxInt;
}
@Override
public void run() {
while (true){
synchronized (LOCK){
//判断是否轮到当前线程执行
while (current % threadCount != threadNo){
if (current > maxInt){
break;
}
try {
//如果不是,则当前线程进入wait
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//最大值跳出循环
if (current > maxInt){
break;
}
System.out.println("thread " + threadNo +" : " + current);
current++;
//唤醒其他wait线程
LOCK.notifyAll();
}
}
}
public static void main(String[] args) {
int threadCount = 3;
int max = 100;
for (int i = 0; i < threadCount; i++) {
new Thread(new test1(i,threadCount,max)).start();
}
}
}
}
N个线程循环打印相对于两个线程打印的区别在,把notify改成了notifyAll。notifyAll会将wait的线程解除当前wait状态,也叫作唤醒,由于我们这里用同步锁synchronized块包裹住,那么唤醒的线程会做抢夺同步锁。
但是上面代码存在要给问题,就是线程数很大的时候,由于我们不确定唤醒的线程到底是否是下一个要执行的就有可能会出现抢到了锁但不该自己执行,然后又进入wait的情况,比如现在有100个线程,现在是第一个线程在执行,他执行完之后需要第二个线程执行,但是第100个线程抢到了,发现不是自己然后又进入wait,然后第99个线程抢到了,发现不是自己然后又进入wait,然后第98,97…直到第3个线程都抢到了,最后才到第二个线程抢到同步锁,这里就会白白的多执行很多过程,虽然最后能完成目标。
插句话:notify和notifyAll的区别?
notify可能会导致死锁,而notifyAll则不会
任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行synchronized 中的代码使用notifyall,可以唤醒所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify只能唤醒一个。
wait() 应配合while循环使用,不应使用if,务必在wait()调用前后都检查条件,如果不满足,必须调用notify()唤醒另外的线程来处理,自己继续wait()直至条件满足再往下执行。
notify() 是对notifyAll()的一个优化,但它有很精确的应用场景,并且要求正确使用。不然可能导致死锁。正确的场景应该是 WaitSet中等待的是相同的条件,唤醒任一个都能正确处理接下来的事项,如果唤醒的线程无法正确处理,务必确保继续notify()下一个线程,并且自身需要重新回到WaitSet中。
**改进的方法:**使用同步器,也就是使用Semaphore,信号量,我们上一个线程持有下一个线程的信号量,通过一个信号量数组将全部关联起来
什么是信号量(Semaphore)?
信号量本质上就是一个计数器,用来限制线程的数量。当一个线程开始时,通过acquire方法申请一个证书,对应计数器减1,当线程结束,通过release()返回证书,对应计数器减1
底层是基于自旋+原子操作实现的。
static int result = 0;
public static void main(String[] args) throws InterruptedException {
int N = 3;
Thread[] threads = new Thread[N];
final Semaphore[] syncObjects = new Semaphore[N];
for (int i = 0; i < N; i++) {
syncObjects[i] = new Semaphore(1);
if (i != N-1){
syncObjects[i].acquire();
}
}
for (int i = 0; i < N; i++) {
final Semaphore lastSemphore = i == 0 ? syncObjects[N - 1] : syncObjects[i - 1];
final Semaphore curSemphore = syncObjects[i];
final int index = i;
threads[i] = new Thread(new Runnable() {
public void run() {
try {
while (true) {
lastSemphore.acquire();
System.out.println("thread" + index + ": " + result++);
if (result > 100){
System.exit(0);
}
curSemphore.release();
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
threads[i].start();
}
}
我们就不会有白白唤醒的线程,每一个线程都按照我们所约定的顺序去执行
参考:阿里多线程面试题