并发编程之面试题一
面试题
创建一个容器,其中有两个方法,一个方法是 add(),一个方法时size(),起两个线程,一个线程是往容器中添加1-10这是个数字,另外一个线程在数字添加到5的时候结束。
初始代码
该问题咋一看是一个很简单的面试题,创建两个线程,分别执行对应的任务即可。以下就是简单的代码:
public class Container {
private List<String> list = new ArrayList<>();
public void add(String str){
list.add(str);
}
public int size(){
return list.size();
}
public static void main(String[] args) {
Container container = new Container();
// 线程1:向容器添加元素
new Thread(()->{
for (int i = 1; i < 11; i++) {
container.add("hello"+i);
System.out.println("add"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 线程2:监测线程1追加的元素
new Thread(()->{
while (true){
if(container.size()==5){
break;
}
}
System.out.println("线程2结束");
}).start();
}
}
分析
但是,在执行以上代码的时候,可以发现线程2不能停止。原因很简单,这涉及了线程之间的通信。程序在启动时,JVM会给每个线程分配一个独立的内存空间(提高执行效率),每个线程独立的内存空间互不干扰,互不影响(即内存的不可见性)。
以上代码中,线程1在执行到添加第5个元素的时候,线程2并不知道容器中的元素已经有5个,故其不能停止。
解决方案
方案一
经过以上分析,可以想到使用 volatile 关键字,来实现内存的可见性。实现只需要将以上代码中的容器用 volitile 关键字修饰:
private volatile List<String> list = new ArrayList<>();
分析:
1)线程没有加锁,线程2取到的可能是6,才会停止;
2)线程2死循环浪费cpu资源。
解决二
public class Container2 {
private List list = new ArrayList();
public void add(String str){
list.add(str);
}
public int size(){
return list.size();
}
public static void main(String[] args) {
Container2 container2 = new Container2();
Object lock = new Object();
new Thread(()->{
synchronized (lock){
System.out.println("线程2启动");
if(container2.size()!=5){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程2结束");
lock.notify(); // wait会释放锁,notify不会释放锁
}
},"t2").start();
new Thread(()->{
synchronized (lock){
for (int i = 1; i < 11; i++) {
container2.add("hello"+i);
System.out.println("add"+i);
if(container2.size()==5){
// 这里不仅要唤醒线程2,还必须通过wait()释放锁
lock.notify();
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
},"t1").start();
}
}
分析:
这种解决方法,算是一个常见的解决方案了。这里我们要注意几个陷阱。
1)执行wait() 会立即释放锁资源;而执行notify()/notifyAll() 不会立即释放锁资源,要等执行完 synchronize 中的代码才释放资源;
2)wait()、notify()/notifyAll() 要放在 synchronize 代码块中执行。
3)synchronize 是非公平锁,也就是说,如果竞争激烈的话,可能有些线程一直得不到执行。
该方案是常见的解决方案,但是相对来说,代码比较复杂,也不是很好理解。下面出示另一种方案。
解决三
public class Container3 {
private volatile List list = new ArrayList();
public void add(String str) {
list.add(str);
}
public int size() {
return list.size();
}
public static void main(String[] args) {
Container3 container3 = new Container3();
// 1->0,门闩就打开
CountDownLatch latch = new CountDownLatch(1);
new Thread(() -> {
System.out.println("线程2启动");
if (container3.size() != 5) {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程2结束");
}, "t2").start();
new Thread(() -> {
for (int i = 1; i < 11; i++) {
container3.add("hello" + i);
System.out.println("add" + i);
if (container3.size() == 5) {
// latch-1
latch.countDown(); // 打开门闩后,并不影响他自己本身运行
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
分析:
CountDownLatch 是java1.5 引入的,是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
如果用实际的场景来类比,可以理解成,一扇门上加了N把门闩,一个人在外面等待,一个人在里面干活,每满足条件一次,就打开一把门闩,当所有的门闩全部打开,另外一个人就可以进去了。
后续思考
- 理解线程之间的通信以及其内存模型;
- 线程之间通信的几种实现方式;
- 通过源码分析 CountDownLatch .