先来看一道淘宝面试题
实现一个容器, 提供两个方法add() , size(), 需要写两个线程
- 线程1: 添加10个元素到容器中, 得全部执行完
- 线程2: 实现监控元素的个数, 当个数到5个时, 打印"监控结束" , 线程2结束
普通人的错误解法 ( 主要用于深入表明题意 )
public class T01_WithoutVolatile {
private List lists = new LinkedList();
public void add(Object o) {
lists.add(o);
}
public int size() {
return lists.size();
}
public static void main(String[] args) {
T01_WithoutVolatile c = new T01_WithoutVolatile();
Thread addThread = new Thread() {
public void run() {
for (int i = 1; i <= 10; i++) {
c.add(new Object());
System.out.println("已添加第" + i + "个");
c.sleep(300); // 只有sleep了, 才能保证在第5个之后, 第6个之前, 线程2打印结束
}
}
};
Thread sizeThread = new Thread() {
public void run() {
while (c.size() != 5) {
c.sleep(10); // 只有sleep了, 才能保证在第5个之后, 第6个之前, 线程2打印结束
}
System.out.println("size==5, 监控结束");
}
};
addThread.start();
sizeThread.start();
}
private static void sleep(long time) {
try {
Thread.sleep(time);
} catch (Exception e) {
}
}
}
运行结果有可能正确, 但也有可能是下面的结果, 第6个打印完, 才打印结束
已添加第1个
已添加第2个
已添加第3个
已添加第4个
已添加第5个
已添加第6个
size==5, 监控结束
已添加第7个
已添加第8个
已添加第9个
已添加第10个
这个解法主要是讲明题意, sleep严重影响性能,肯定不能作为答案
wait / notify 的错误解法
public class T03_NotifyHoldingLock {
private List lists = new LinkedList();
public void add(Object o) {
lists.add(o);
}
public int size() {
return lists.size();
}
public static void main(String[] args) {
T03_NotifyHoldingLock c = new T03_NotifyHoldingLock();
Object lock = new Object();
Thread addThread = new Thread() {
public void run() {
synchronized (lock) { // 获取lock锁
for (int i = 1; i <= 10; i++) {
c.add(new Object());
System.out.println("已添加第" + i + "个");
if (i == 5) {
lock.notify(); // 只做到通知, 其他线程想要开始做, 得先等我全部做完
}
}
// 释放lock锁
}
}
};
Thread sizeThread = new Thread() {
public void run() {
synchronized (lock) { // 获取lock锁
try {
lock.wait(); // 释放lock锁
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("size==5, 监控结束");
}
}
};
// 必须保证sizeThread先跑起来, 否则程序会因为sizeThread没有被notify, 永远也退出不了
sizeThread.start();
addThread.start();
}
}
Object.wait / notify / notifyAll, 很重要的容易被忽略的点
- wait()调用完了会释放当时synchronized获得的锁
- notify调用完了, 不会释放锁, 其他的线程虽然被notify了, 但是无法立即拿到锁
示例中, 我们先让sizeThread跑起来, 获得lock锁之后, 调了wait方法又立即释放了lock
接下来, addThread跑起来了, 当执行到第5次循环的时候, 调用了notify, 但是这个时候并没有释放锁,
只是让sizeThread从对象池的阻塞队列中, 转移到了等待队列, 而等待队列中的线程想要继续执行, 得先获取到锁才行
但是addThread没有释放锁, 执行完所有10次循环, 才退出了synchronized同步代码块, 才释放了锁
wait / notify 的正确解法
public class T04_NotifyFreeLock {
private List lists = new LinkedList();
public void add(Object o) {
lists.add(o);
}
public static void main(String[] args) {
T04_NotifyFreeLock c = new T04_NotifyFreeLock();
Object lock = new Object();
Thread addThread = new Thread() {
public void run() {
synchronized (lock) {
for (int i = 1; i <= 10; i++) {
c.add(new Object());
System.out.println("已添加第" + i + "个");
if (i == 5) {
lock.notify(); // 通知sizeThread到等待队列
try {
lock.wait(); // 自己立即阻塞, 并释放锁, 让sizeThread先执行
} catch (InterruptedException e) {
}
}
}
}
}
};
Thread sizeThread = new Thread() {
public void run() {
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
}
System.out.println("size==5, 监控结束");
lock.notify(); // 通知addThread恢复
}
}
};
// 必须保证sizeThread先跑起来, 否则程序会因为sizeThread没有被notify, 永远也退出不了
sizeThread.start();
addThread.start();
}
}
这里的关键点是, addThread notify了之后, 自己立即阻塞, 并释放锁, 给sizeThread一次执行的机会
sizeThread在打印结束之后, 要记得再notify一下addThread, 让addThread继续把后面5次循环执行完, 否则addThread会一直等在那里
差不多的解决思路, CountDownLatch的解法
public class T05_CountDownLatch {
private List lists = new LinkedList();
public void add(Object o) {
lists.add(o);
}
public static void main(String[] args) {
T05_CountDownLatch c = new T05_CountDownLatch();
CountDownLatch cdl = new CountDownLatch(1);
CountDownLatch cdl2 = new CountDownLatch(1);
Thread addThread = new Thread() {
public void run() {
try {
for (int i = 1; i <= 10; i++) {
c.add(new Object());
System.out.println("已添加第" + i + "个");
if (i == 5) {
cdl.countDown();
cdl2.await();
}
}
} catch (Exception e) {
}
}
};
Thread sizeThread = new Thread() {
public void run() {
try {
cdl.await();
System.out.println("size==5, 监控结束");
cdl2.countDown();
} catch (Exception e) {
}
}
};
addThread.start();
sizeThread.start();
}
}
一样的思路, addThread通知sizeThread之后, 自己先趴着(阻塞), 等待sizeThread执行完后notify自己
类似的思路, LockSupport的解法
public class T06_LockSupport {
private List lists = new LinkedList();
public void add(Object o) {
lists.add(o);
}
static Thread addThread = null, sizeThread = null;
public static void main(String[] args) {
T06_LockSupport c = new T06_LockSupport();
addThread = new Thread() {
public void run() {
for (int i = 1; i <= 10; i++) {
c.add(new Object());
System.out.println("已添加第" + i + "个");
if (i == 5) {
LockSupport.unpark(sizeThread);
LockSupport.park();
}
}
}
};
sizeThread = new Thread() {
public void run() {
LockSupport.park();
System.out.println("size==5, 监控结束");
LockSupport.unpark(addThread);
}
};
// 必须保证sizeThread先跑起来, 否则程序会因为sizeThread没有被notify, 永远也退出不了
sizeThread.start();
addThread.start();
}
}
.
LockSupport.park / unpark
LockSupport.park() 使当前线程阻塞
LockSupport.unpark(thread) 叫醒某个线程
演示: 在第5秒阻塞, 在第8秒叫醒
在park()被调用之前 , unpark可以先调用
在上面的示例中, sleepSeconds(8) 和 unpark(t) 两行互换位置, 运行结果显示unpark先调用的话, park()方法会不起作用
更进一步, 我们来测验一下分别调用两次park和unpark
其效果和分别调用一次一模一样
所以,可以证明LockSupport内部是根据count来判断是否要继续执行线程的, count初始为0, park一下, count++, unpark一下count–, 当count>=0的时候, 线程可以继续往下执行, 否则, 就阻塞住
本质上park和unpark都是native方法
public native void park(boolean var1, long var2);
public native void unpark(Object var1);
区别
如果用其他的锁方式, 你得有一把锁, LockSupport是不需要的
- 比如wait, 你得有个object
- synchronized你也得有个object, 比如XXX.class或new XXX()