文章目录
- 一、题目要求
- 二、用wait-notify优化前后的两版代码
- 1.优化前
- 2.优化后
- 三、使用park和unpark实现
- 四、Condition接口
一、题目要求
给定两个List(或者数组),一个List存储"ABCDEFG",一个List存储"1234567",两个线程分别打印两个List,打印的结果是 “A1B2C3D4E5F6G7”
面试官当时说,这是一道学并发编程必写的一道题,我心里想:嗯?我就没写过。然后他接着说:打开你的IDEA共享屏幕,开始写吧(视频面试)。行我就开始写了。
这道题考察的是线程通信的方式,就是线程A打印A阻塞,线程B打印1阻塞,以此类推。实现的方式有很多种,比如wait-notify,park-unpark,Contidition等等,上面说的三种是比较常见的方式了。
我选用的是wait-notify,后面两种也会写到。
二、用wait-notify优化前后的两版代码
1.优化前
public static void main(String[] args){
//定义对象锁和两个List
final Object o = new Object();
final List<Integer> integers = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7);
final List<String> strings = Lists.newArrayList("A", "B", "C", "D", "E", "F", "G");
//打印数字
new Thread(() -> {
integers.forEach(item -> {
synchronized (o) {
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(item);
o.notify();
}
});
}, "t1").start();
//打印字母
new Thread(() -> {
strings.forEach(item -> {
synchronized (o) {
System.out.print(item);
o.notify();
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}, "t2").start();
}
//输出结果
A1B2C3D4E5F6G7
解释下上面实现的过程
两个线程使用同一把对象锁,Object类型的对象o,synchronized大家比较熟悉了,实现了锁机制,线程间的同步,原理就是多个线程同时修改对象头中的ThreadId,修改成功了就是抢到锁了,进入同步代码块,修改失败会进入等待队列中(后续也会写相关的文章)。
假如线程t1抢到锁了,调用o.wait()时,线程t1会释放锁资源,jvm会将线程t1暂时放到对象锁o的等待队列中。当线程t2调用o.notify()时会从对象锁o的等待队列中随机唤醒一个,因为只有一个t1,所以唤醒t1继续打印输出。换成t2亦然。
我写完之后,面试官接着说:给你1min分钟的时间,想一下有没有可以优化的地方。大家也可以想下,我当时说的是代码重复。
2.优化后
面试官:你这个过程中每循环一次就会加一次锁,会有额外的性能损耗。听完豁然开朗,开始优化第二版
优化过程比较简单就是将synchronized同步代码块放到循环的外面,这样只有线程启动时加锁,只有一次加锁的过程。代码就不放了,可以自己实现下
三、使用park和unpark实现
park和unpark是LockSupport类中的两个方法,LockSupport.park()使当前线程处于等待状态,而LockSupport.unpark()表示唤醒一个线程,unpark方法需要一个参数,指定唤醒哪个线程。理解起来比较简单。
直接上代码吧
static Thread t1=null;
static Thread t2=null;
public static void main(String[] args) {
final List<Integer> integers = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7);
final List<String> strings = Lists.newArrayList("A", "B", "C", "D", "E", "F", "G");
//打印字母
t1=new Thread(()->{
strings.forEach(item->{
System.out.print(item);
LockSupport.unpark(t2);
LockSupport.park();
});
},"t1");
//打印数字
t2=new Thread(()->{
integers.forEach(item->{
LockSupport.park();
System.out.print(item);
LockSupport.unpark(t1);
});
},"t2");
t1.start();
t2.start();
}
四、Condition接口
先上代码,再解释
public static void main(String[] args) {
final List<Integer> integers = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7);
final List<String> strings = Lists.newArrayList("A", "B", "C", "D", "E", "F", "G");
ReentrantLock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
//打印数字
new Thread(()->{
lock.lock();
try {
integers.forEach(item->{
try {
condition1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(item);
condition2.signal();
});
}finally {
lock.unlock();
}
},"t1").start();
//打印字母
new Thread(()->{
lock.lock();
try {
strings.forEach(item->{
System.out.print(item);
condition1.signal();
try {
condition2.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}finally {
lock.unlock();
}
},"t2").start();
}
看的出来condition.await()和signal()实现了o.wait和notify的作用。await使当前线程阻塞,signal唤醒其他线程。大致原理是AQS除了维护一个同步队列外,还维护了一个等待队列,该等待队列实现了condition,具体原理后续博客也回写。
不过要注意一个点,await和signal在阻塞和唤醒时如果顺序不当,可能导致当前程序无法执行完,wait和notify也有一样的问题。当然上面的程序是没有问题。
因为当主程序运行时t1先获取锁,然后立刻阻塞。此时t2获取锁先打印字母A,再唤醒t1打印1,阻塞t2。整好可以打印一组A1,循环7次。最后t2打印完G处于等待状态,t1打印完成后唤醒t2,程序结束。两个线程都不会处于等待状态。