有一道面试题是这样的:实现一个容器,提供两个方法,add和size。写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5时,线程2给出提示并结束。
咋一看,似乎并不能,首先,实现一个容器如下:
public class MyContainer1 {
List<Object> lists = new ArrayList<>();
public void add(Object o) {
lists.add(o);
}
public int size() {
return lists.size();
}
}
然后,在main方法中写两个线程,第一个线程,往容易中添加10个元素,第二个线程,使用while循环,当发现容器的元素个数为5时,跳出while循环, 并结束线程,代码如下:
public static void main(String[] args) {
MyContainer1 t1 = new MyContainer1();
new Thread(()->{
for(int i = 0; i < 10; i++) {
try {
t1.add("");
System.out.println(t1.size());
Thread.sleep(1000);
} catch (Exception e){
e.printStackTrace();
}
}
}, "t1").start();
new Thread(()->{
while (true) {
if(t1.size() == 5) {
System.out.println("list size is 5, break");
break;
}
}
}, "t2").start();
}
看起来是没问题,然而事情并不会这么简单,问题出在哪呢?忘记加volatile了!给lists加上volatile修饰:volatile List<Object> lists = new ArrayList<>();
再运行程序,便能得到想要的效果。
但是这个程序就是完善的吗?当然不,其问题在于,线程2中的while循环,非常浪费CPU,另外就是,由于没有加同步,即使是while循环,也不能非常精准地在判断size为5的时候break,有可能在执行判断的时候,size又改了。所以,我们还需要做到更好!
这里使用wait和notify可以做到更好,wait会释放锁,而notify不会释放锁。
首先使用synchronized某个锁定对象,然后调用该对象的wait方法,线程进入等待状态,同时释放锁;只有在该对象的notify方法被调用的时候,等待状态的线程才会被唤醒继续执行。
事不宜迟,写下代码:
public class MyContainer2 {
private List<Integer> lists = new ArrayList<>();
public void add(Integer a) {
lists.add(a);
}
public Integer size() {
return lists.size();
}
public static void main(String[] args) {
MyContainer2 c = new MyContainer2();
final Object lock = new Object();
//线程2:监控线程
new Thread(()->{
synchronized (lock) {
System.out.println("thread 2 start...");
if(c.size() != 5) {
try {
lock.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("thread 2 end.");
}
}, "t2").start();
new Thread(()->{
synchronized (lock) {
for(int i = 0; i < 10; i++) {
System.out.println("thread 1, add " + i);
c.add(i);
if(c.size() == 5) {
lock.notify();
}
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}, "t1").start();
}
}
在上面的代码中,线程2先启动,它首先获得lock对象的锁,判断容器size不为5时,调用lock对象的wait方法开始等待,同时释放lock对象的锁;接着线程1启动,同样首先获取lock对象的锁,然后for循环每次像容器中添加一个元素,当判断容器size为5的时候,调用lock的notify方法,理论上这个时候会唤醒处于wait状态t2线程,然后t2应该输出"thread 2 end."
运行代码,发现size为5的时候,t2并没有继续执行!问题在于,wait会释放锁,而notify不会释放锁,尽管notify方法被调用了,但是t2并没有获得锁,因此不会继续执行。t2要想重新获得锁,当然要t1释放才行,所以,在t1中,调用了lock对象的notify方法之后,再调用lock的wait方法释放锁,而在t2被唤醒之后,继续执行,最后还要调用lock对象的notify方法去唤醒此时处在wait状态的t1,于是代码优化如下:
public class MyContainer2 {
private List<Integer> lists = new ArrayList<>();
public void add(Integer a) {
lists.add(a);
}
public Integer size() {
return lists.size();
}
public static void main(String[] args) {
MyContainer2 c = new MyContainer2();
final Object lock = new Object();
//监控线程
new Thread(()->{
synchronized (lock) {
System.out.println("thread 2 start...");
if(c.size() != 5) {
try {
lock.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("thread 2 end.");
lock.notify();
}
}, "t2").start();
new Thread(()->{
synchronized (lock) {
for(int i = 0; i < 10; i++) {
System.out.println("thread 1, add " + i);
c.add(i);
if(c.size() == 5) {
lock.notify();
try {
lock.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}, "t1").start();
}
}
使用synchronized、wait和notify,终于实现了想要的效果,相比第一种实现方式,已经有了极大的进步,至少没有硬伤了,但这依然不是最好的解决方案,下一篇文章继续讲解。