线程循环运行问题
面试题中经常出现多个线程交替运行情况,遇到该种问题不要惊慌,先考虑几个解决方案
1、几个线程是否会反复运行,如果不是则可以利用Thread.join()方法监视线程状态,一个线程死亡后再去启动下一个线程
执行顺序为A.start();A.join();A.start();B.join();A.start();B.join();…
2、如果线程会反复运行,则不能再利用join方法了,因为线程不能反复启动。此时我们就要考虑利用synchronized与 wait、notify/notifyAll来
分析
控制线程的状态了。该种问题易出题 多线程交替打印某个范围内的奇偶数,或者几个顺序排列的字母比如交替打印A、B、C
方案1 比较容易理解,这种情况实际上并不属于并发执行,因为同一时间只有一个线程执行,(顶多加上主线程算俩),一个前一个线程执行完毕死亡了再去启动下一个线程 很好理解,在此不必详细说明
方案2 情况比较复杂,代码量肯定比方案1多,还经常在纸上写代码,不能调试(一把辛酸泪)。代码结构上主要关心的是最外层是一个大循环,交替次数都有上限,否则就是个死循环,卷子上一般有说明,面试官直接要求写出代码的话要问清楚,否则当你写出个不能停下来的程序后就得面临他的质疑了(咆哮面试官没说要求停下来也没用。。。心好累),有时题目会要求每个线程再循环n次,这时可能会有点疑惑,其实这种循环没啥意义,直接写个for循环就行了 跟写一行不循环的代码本质上一样,出题的人脑子有坑!!!
下面我们来实战一下
面试题1:A、B两个线程交替运行,线程A循环10次,线程B循环10次,如此循环50次
问题解析:问题相当于A、B两个线程交替运行50次, 每个线程内部循环运行次数跟一次没啥区别,就是执行某个指令写个for循环就行,问题主要在于两个线程交替运行
线程交替运行方法有两种
一种是利用Thread.join() 方法,但这种方法只能监视到某个线程执行完毕后(也就是线程死亡)才能执行下一个线程,而题目明显要求两个线程会反复运行
另一种方法是通过加锁以及wait、notify/notifyAll方法使得两个线程相互唤醒+阻塞,以此来使得线程睡眠->唤醒循环
public class ThreadDemo {
static Object lock = new Object();
static AtomicBoolean n = new AtomicBoolean(true);
static AtomicInteger count = new AtomicInteger(1);
static int max = 51;
public static void main(String[] args) {
a().start();
b().start();
}
public static Thread a() {
return new Thread(()-> {
while(count.get() < max) {
synchronized(lock) {
if(!n.get()) {
// 状态不对 阻塞线程 交出对象锁
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
print("线程A", 10);
//修改状态 通知线程B
n.set(false);
lock.notifyAll();
}
}
}
});
}
public static Thread b() {
return new Thread(()-> {
while(count.get() < max) {
synchronized(lock) {
if(n.get()) {
// 状态不对 阻塞线程 交出对象锁
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
print("线程B", 10);
//修改状态 通知线程B
n.set(true);
count.incrementAndGet();
lock.notifyAll();
}
}
}
});
}
public static void print(String name, int num) {
for(int i =0; i < num;i++) {
System.out.println("" + count.get() + ":" + name + "循环" + (i+1) + "次");
}
}
}
面试题2 创建三个线程交替循环打印“A”、“B”、“C”,循环100次
解题思路:交替循环打印3个变量比两个变量看起来复杂一些,但本质没变,每个线程运行的条件没变,比如创建线程 1、线程2、线程3,每个线程打印的字母其实都没变,只要保证这3个线程按照顺序交替运行就行。
之前在网上其他地方看到有代码是依靠多个锁来解决的,这个我觉得有点复杂,我觉得这里只创建一个可操作的状态变量与次数变量以及一个对象锁,这个变量有3幢状态,3个线程监控这个变量,当发现变量是自己要求的条件时就开始运行,运行完毕后就交出对象锁并通知其他线程,不是则阻塞线程并交出对象锁
public class ThreadDemo {
// 对象锁
static Object lock = new Object();
// 状态
static int status = 1;
// 次数
static AtomicInteger count = new AtomicInteger(1);
// 交替次数
static int max = 100;
public static void main(String[] args) {
create("线程1", 1, "A").start();
create("线程2", 2, "B").start();
create("线程3", 3, "C").start();
}
public static Thread create(String name, int st, String value) {
return new Thread(()-> {
while(count.get() <= max) {
synchronized(lock) {
if(status == st) {
System.out.println(count + ":" + name+ ":" + value);
if(status == 3) {
// 返回第一种状态
status = 1;
}else {
// 状态前进一步
status++;
}
count.incrementAndGet();
lock.notifyAll();
}else {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
}
}
还有一种方案2的奇偶数打印问题思路跟上面相似,就是改下条件,将状态条件改为了是奇数还是偶树,这种题型还可分为两种
一种是两个线程,一种是两个以上的线程。
两个线程下比较好写,仍然是一个对象锁+一个变量,一个线程拿到锁后判断是奇数就运行,完毕后变量加1并notify一下,另一个线程拿到锁后判断当前变量为偶数时就运行,完毕后加1并notify,逻辑比上面打印字母更简单一些,两个线程判断变量达到最大值就退出
两个以上的线程就跟打印字母有点像了,若有3个线程,可跟打印字母一样,用一个变量的3个值来标识每个线程是否该继续运行,运行后就将变量加1,这样就达到了目的。
关于notify/notifyAll与wait还有几点需要了解的,也是面试官看到并发编程题会追问的,他们必须在加锁范围内调用,wait方法调用时阻塞的是刺客调用它的线程,再次唤醒时从wait方法的下一行代码开始运行而不是直接返回上面synchronized处运行
notify/notifyAll方法调用时不会实际通知,要等到对象锁释放了才会发出实际通知,否则的话调用是对象锁还没释放,通知了也没用,其他线程只能干瞪眼。notify与notifyAll的区别是前者通知到了一个线程就结束了,不再通知其他的线程,而后者会通知所有的线程行动起来。