1.为阅读体验,本站无任何广告,也无任何盈利方法,站长一直在用爱发电,现濒临倒闭,希望有能力的同学能帮忙分担服务器成本
2.捐助10元及以上同学,可添加站长微信lurenzhang888,备注捐助,网站倒闭后可联系站长领取本站pdf内容
3.若网站能存活下来,后续将会持续更新内容
这是一道面试中常考的并发编程的代码题,与它相似的问题有:
- 三个线程T1、T2、T3轮流打印ABC,打印n次,如ABCABCABCABC…….
- 两个线程交替打印1-100的奇偶数
- N个线程循环打印1-100
- ……
其实这类问题本质上都是线程通信问题,思路基本上都是一个线程执行完毕,阻塞该线程,唤醒其他线程,按顺序执行下一个线程。下面先来看最简单的,如何按顺序执行三个线程。
- synchronized+wait/notify
基本思路就是线程A、线程B、线程C三个线程同时启动,因为变量num
的初始值为0
,所以线程B或线程C拿到锁后,进入while()
循环,然后执行wait()
方法,线程线程阻塞,释放锁。只有线程A拿到锁后,不进入while()
循环,执行num++
,打印字符A
,最后唤醒线程B和线程C。此时num
值为1
,只有线程B拿到锁后,不被阻塞,执行num++
,打印字符B
,最后唤醒线程A和线程C,后面以此类推。
class Wait_Notify_ACB {
private int num;
private static final Object LOCK = new Object();
private void printABC(String name, int targetNum) {
synchronized (LOCK) {
while (num % 3 != targetNum) { //想想这里为什么不能用if代替while,想不起来可以看上一篇文章
try {
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
System.out.print(name);
LOCK.notifyAll();
}
}
public static void main(String[] args) {
Wait_Notify_ACB wait_notify_acb = new Wait_Notify_ACB ();
new Thread(() -> {
wait_notify_acb.printABC("A", 0);
}, "A").start();
new Thread(() -> {
wait_notify_acb.printABC("B", 1);
}, "B").start();
new Thread(() -> {
wait_notify_acb.printABC("C", 2);
}, "C").start();
}
}
输入结果:
ABC
Process finished with exit code 0
接下来看看第一个问题,三个线程T1、T2、T3轮流打印ABC,打印n次。其实只需要将上述代码加一个循环即可,这里假设n=10。
class Wait_Notify_ACB {
private int num;
private static final Object LOCK = new Object();
private void printABC(String name, int targetNum) {
for (int i = 0; i < 10; i++) {
synchronized (LOCK) {
while (num % 3 != targetNum) { //想想这里为什么不能用if代替,想不起来可以看上一篇文章
try {
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
System.out.print(name);
LOCK.notifyAll();
}
}
}
public static void main(String[] args) {
Wait_Notify_ACB wait_notify_acb = new Wait_Notify_ACB ();
new Thread(() -> {
wait_notify_acb.printABC("A", 0);
}, "A").start();
new Thread(() -> {
wait_notify_acb.printABC("B", 1);
}, "B").start();
new Thread(() -> {
wait_notify_acb.printABC("C", 2);
}, "C").start();
}
}
输出结果:
ABCABCABCABCABCABCABCABCABCABC
Process finished with exit code 0
下面看第二个问题,两个线程交替打印1-100的奇偶数,为了减少输入所占篇幅,这里将100 改成了10。基本思路上面类似,线程odd先拿到锁——打印数字——唤醒线程even——阻塞线程odd,以此循环。
class Wait_Notify_Odd_Even{
private Object monitor = new Object();
private volatile int count;
Wait_Notify_Odd_Even(int initCount) {
this.count = initCount;
}
private void printOddEven() {
synchronized (monitor) {
while (count < 10) {
try {
System.out.print( Thread.currentThread().getName() + ":");
System.out.println(++count);
monitor.notifyAll();
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//防止count=10后,while()循环不再执行,有子线程被阻塞未被唤醒,导致主线程不能退出
monitor.notifyAll();
}
}
public static void main(String[] args) throws InterruptedException {
Wait_Notify_Odd_Even waitNotifyOddEven = new Wait_Notify_Odd_Even(0);
new Thread(waitNotifyOddEven::printOddEven, "odd").start();
Thread.sleep(10);
new Thread(waitNotifyOddEven::printOddEven, "even").start();
}
}
运行结果:
odd:1
even:2
odd:3
even:4
odd:5
even:6
odd:7
even:8
odd:9
even:10
再看第三个问题,N个线程循环打印1-100,其实仔细想想这个和三个线程循环打印ABC并没有什么本质区别,只需要加上判断是否到了打印数字的最大值的语句即可。假设N=3,为了能把输出结果完全显示,打印1-10,代码如下:
class Wait_Notify_ACB {
private int num;
private static final Object LOCK = new Object();
private int maxnum = 10;
private void printABC(String name, int targetNum) {
while (true) {
synchronized (LOCK) {
while (num % 3 != targetNum) { //想想这里为什么不能用if代替,想不起来可以看上一篇文章
if(num >= maxnum){
break;
}
try {
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(num >= maxnum){
break;
}
num++;
System.out.println(Thread.currentThread().getName() + ": " + num);
LOCK.notifyAll();
}
}
}
public static void main(String[] args) {
Wait_Notify_ACB wait_notify_acb = new Wait_Notify_ACB ();
new Thread(() -> {
wait_notify_acb.printABC("thread1", 0);
}, "thread1").start();
new Thread(() -> {
wait_notify_acb.printABC("thread2", 1);
}, "thread2").start();
new Thread(() -> {
wait_notify_acb.printABC("thread3", 2);
}, "thread3").start();
}
}
输出结果:
thread1: 1
thread2: 2
thread3: 3
thread1: 4
thread2: 5
thread3: 6
thread1: 7
thread2: 8
thread3: 9
thread1: 10
面试官:大家都是用的synchronized+wait/notify,你能不能换个方法解决该问题?
我:好的,我还会用join方法
下面介绍的方法只给出第一道题的代码了,否则太长了,相信大家可以举一反三
- join()
join()
方法:在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行。基于这个原理,我们使得三个线程按顺序执行,然后循环多次即可。无论线程1、线程2、线程3哪个先执行,最后执行的顺序都是线程1——>线程2——>线程3。代码如下:
class Join_ABC {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
Thread t1 = new Thread(new printABC(null),"A");
Thread t2 = new Thread(new printABC(t1),"B");
Thread t3 = new Thread(new printABC(t2),"C");
t0.start();
t1.start();
t2.start();
Thread.sleep(10); //这里是要保证只有t1、t2、t3为一组,进行执行才能保证t1->t2->t3的执行顺序。
}
}
static class printABC implements Runnable{
private Thread beforeThread;
public printABC(Thread beforeThread) {
this.beforeThread = beforeThread;
}
@Override
public void run() {
if(beforeThread!=null) {
try {
beforeThread.join();
System.out.print(Thread.currentThread().getName());
}catch(Exception e){
e.printStackTrace();
}
}else {
System.out.print(Thread.currentThread().getName());
}
}
}
}
输出结果:
ABCABCABCABCABCABCABCABCABCABC
面试官:还会其他方法吗?
我:还会Lock。
- Lock
该方法很容易理解,其实现代码和synchronized+wait/notify方法的很像。不管哪个线程拿到锁,只有符合条件的才能打印。代码如下:
class Lock_ABC {
private int num; // 当前状态值:保证三个线程之间交替打印
private Lock lock = new ReentrantLock();
private void printABC(String name, int targetNum) {
for (int i = 0; i < 10; ) {
lock.lock();
if (num % 3 == targetNum) {
num++;
i++;
System.out.print(name);
}
lock.unlock();
}
}
public static void main(String[] args) {
Lock_ABC lockABC = new Lock_ABC();
new Thread(() -> {
lockABC.printABC("A", 0);
}, "A").start();
new Thread(() -> {
lockABC.printABC("B", 1);
}, "B").start();
new Thread(() -> {
lockABC.printABC("C", 2);
}, "C").start();
}
}
输出结果:
ABCABCABCABCABCABCABCABCABCABC
面试官:该方法存在什么问题,可以进一步优化吗
我:可以使用Lock+Condition实现对线程的精准唤醒,减少对其他线程无意义地唤醒,浪费资源。
- Lock+Condition
该思路和synchronized+wait/notify方法的更像了,synchronized对应lock,await/signal方法对应wait/notify方法。下面的代码为了能精准地唤醒下一个线程,创建了多个Condition对象。
class LockConditionABC {
private int num;
private static Lock lock = new ReentrantLock();
private static Condition c1 = lock.newCondition();
private static Condition c2 = lock.newCondition();
private static Condition c3 = lock.newCondition();
private void printABC(String name, int targetNum, Condition currentThread, Condition nextThread) {
for (int i = 0; i < 10; ) {
lock.lock();
try {
while (num % 3 != targetNum) {
currentThread.await();
}
num++;
i++;
System.out.print(name);
nextThread.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
LockConditionABC print = new LockConditionABC();
new Thread(() -> {
print.printABC("A", 0, c1, c2);
}, "A").start();
new Thread(() -> {
print.printABC("B", 1, c2, c3);
}, "B").start();
new Thread(() -> {
print.printABC("C", 2, c3, c1);
}, "C").start();
}
}
输出结果:
ABCABCABCABCABCABCABCABCABCABC
面试官:除了该方法,还有什么方法可以避免唤醒其他无意义的线程?
我:可以通过使用信号量来实现。
- Semaphore
Semaphore:用来控制同时访问某个特定资源的操作数量,或者同时执行某个制定操作的数量。Semaphore内部维护了一个计数器,其值为可以访问的共享资源的个数。
一个线程要访问共享资源,先使用acquire()
方法获得信号量,如果信号量的计数器值大于等于1,意味着有共享资源可以访问,则使其计数器值减去1,再访问共享资源。如果计数器值为0,线程进入休眠。
当某个线程使用完共享资源后,使用release()
释放信号量,并将信号量内部的计数器加1,之前进入休眠的线程将被唤醒并再次试图获得信号量。
代码如下:
class SemaphoreABC {
private static Semaphore s1 = new Semaphore(1); //先打印A,所以设s1中的计数器值为1
private static Semaphore s2 = new Semaphore(0);
private static Semaphore s3 = new Semaphore(0);
private void printABC(String name, Semaphore currentThread, Semaphore nextThread) {
for (int i = 0; i < 10; i++) {
try {
currentThread.acquire(); //阻塞当前线程,即调用当前线程acquire(),计数器减1为0
System.out.print(name);
nextThread.release(); //唤醒下一个线程,即调用下一个线程线程release(),计数器加1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
SemaphoreABC printer = new SemaphoreABC();
new Thread(() -> {
printer.printABC("A", s1, s2);
}, "A").start();
Thread.sleep(10);
new Thread(() -> {
printer.printABC("B", s2, s3);
}, "B").start();
Thread.sleep(10);
new Thread(() -> {
printer.printABC("C", s3, s1);
}, "C").start();
}
}
输出结果:
ABCABCABCABCABCABCABCABCABCABC
面试官:除了上述五种方法,还有其他方法吗
我:还有LockSupport、CountDownLatch、AtomicInteger等等。
面试官:那如何实现三个线程循环打印ACB,其中A打印两次,B打印三次,C打印四次呢?
我:……
面试官:如何用两个线程交叉打印数字和字符呢?例如A1B2C3……Z26
我:……
大家可以思考下后面两个问题,原理都是相通的。
本站链接:https://www.mianshi.online,如需勘误或投稿,请联系微信:lurenzhang888
点击面试手册,获取本站面试手册PDF完整版