引言
前面的学习中已经解除了关于加锁,共享变量可见性,原子性,synchronized,volatile等知识。在多线程中,还有很多需要掌握的小知识点。
避免字符串常量作为锁对象
在下面的例子中,m1和m2其实锁定的是同一个对象这种情况下还会发生比较诡异的现象,比如你用到一个类库,在该类库中代码锁定了字符串"Hello",但是你读不到源码,所以你在自己的代码中也锁定了"Hello",这时候就有可能发生非常诡异的死锁阻塞,因为你的程序和你用到的类库不经意间使用了同一把锁。
public class T {
String s1 = "Hello";
String s2 = "Hello";
void m1() {
synchronized (s1) {
}
}
void m2() {
synchronized (s2) {
}
}
}
避免将锁定对象的引用变成另一个对象
锁定某个对象,如果o的属性发生改变,不影响锁的使用,但是如果o变成另外一个对象,则锁定的对象发生改变,应该避免将锁定对象的引用变成另一个对象。
public class T {
Object o = new Object();
void m() {
synchronized (o) {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
}
public static void main(String[] args) {
T t = new T();
new Thread(t::m, "t1").start();// 启动第一个线程
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 创建第二个线程
Thread t2 = new Thread(t::m, "t2");
// 锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,t2 将永远得不到执行机会。
// 这是因为锁定的是堆内存中实际的对象,而不是栈内存中的引用,如果引用指向的对象发生了改变,则新对象不会有锁。
t.o = new Object();
t2.start();
}
}
面试题
曾经的面试题(淘宝?)
实现一个容器,提供两个方法:add()、size()
写两个线程,线程1 为容器添加10个元素,线程2实时监控容器中元素的个数,当个数为5时,线程2给出提示并结束。
分析下面这个程序,能完成这个功能吗?
程序一
public class MyContainer1 {
// 如果不加volatile,lists的变化无法及时被其他线程感知,因此可能导致不可见的问题
volatile List<Object> lists = new ArrayList<>();
public void add(Object o) {
lists.add(o);
}
public int size() {
return lists.size();
}
public static void main(String[] args) {
MyContainer1 c = new MyContainer1();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
c.add(new Object());
System.out.println("add" + i);
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
}
}, "t1").start();
new Thread(() -> {
/* t2循环检测c中的大小是否已经等于5*/
while(true) {
if (c.size() == 5) {
break;
}
}
System.out.println("t2 结束");
}, "t2").start();
}
}
执行结果:
程序二
/**
* 曾经的面试题(淘宝?) 实现一个容器,提供两个方法:add , size 写两个线程,线程1 为容器添加10个元素,线程2实时监控容器中元素的个数,
* 当个数为5时,线程2给出提示并结束 <br>
*
* 给lists添加volatile之后,t2能够接到通知,但是,t2线程的死循环很浪费cpu,如果不用死循环,该怎么做?
* =======================================================================
* 这里使用wait和notify 做到,wait会释放锁,而notify不会释放锁。
* 需要注意的是,运用这种方法,必须要保证t2先执行,也就是先让t2监控才可以。
*
* 阅读下面程序,并分析输出结果
* 可以读到输出结果并不是size==5时t2退出,而是t1结束时t2才接收到通知而退出
* 想想这是为什么?
*
* 类名:MyContainer2<br>
* 作者: mht<br>
* 日期: 2018年9月1日-上午11:04:54<br>
*/
public class MyContainer2 {
// 如果不加volatile,lists的变化无法及时被其他线程感知,因此可能导致不可见的问题
volatile List<Object> lists = new ArrayList<>();
public void add(Object o) {
lists.add(o);
}
public int size() {
return lists.size();
}
/**
* 使用wait和notify必须锁定同一个Object,否则将不能使用wait和notify,虽然size=5时线程1发出了notify的命令
* 唤醒了t2,但是notify不会释放lock,t2依然需要阻塞,等待t1执行完后释放锁后才可以执行。另外sleep()也是不释放锁的。
*/
public static void main(String[] args) {
MyContainer2 c = new MyContainer2();
// 锁对象,可以是任意的一个对象,使用wait和notify必须在同一个对象锁上
final Object lock = new Object();
new Thread(() -> {
System.out.println("t2启动");
synchronized (lock) {
// 这里判断size是否等于5,如果不满足条件,则t2进入等待状态
if (c.size() != 5) {
try {
System.out.println("size = " + c.size() + ",t2 wait...");
// wait会让当前线程进入阻塞状态,并释放锁
lock.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("t2结束");
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
System.out.println("t1启动");
synchronized (lock) {
for (int i = 0; i < 10; i++) {
c.add(new Object());
System.out.println("add" + i);
// t1线程内部发出一个满足条件时的唤醒消息,notify代表唤醒一个线程,这个唤醒是随机的
if (c.size() == 5) {
System.out.println("size = 5, 叫醒其他线程!");
lock.notify();// notifyAll()可以唤醒全部等待中的线程,随机抢得
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}).start();
}
}
执行结果:
程序三
/**
* 使用wait和notify 完成最终的目标MyContainer2.java最终解决方法
* 使用wait和notify在线程之间来回等待和叫醒,这样做的确可以完成要求,但是
* 这样做非常的麻烦
* 类名:MyContainer2<br>
* 作者: mht<br>
* 日期: 2018年9月1日-上午11:04:54<br>
*/
public class MyContainer3 {
// 如果不加volatile,lists的变化无法及时被其他线程感知,因此可能导致不可见的问题
volatile List<Object> lists = new ArrayList<>();
public void add(Object o) {
lists.add(o);
}
public int size() {
return lists.size();
}
/**
* 使用wait和notify必须锁定同一个Object,否则将不能使用wait和notify,虽然size=5时线程1发出了notify的命令
* 唤醒了t2,但是notify不会释放lock,t2依然需要阻塞,等待t1执行完后释放锁后才可以执行。另外sleep()也是不释放锁的。
*/
public static void main(String[] args) {
MyContainer3 c = new MyContainer3();
// 锁对象,可以是任意的一个对象,使用wait和notify必须在同一个对象锁上
final Object lock = new Object();
new Thread(() -> {
System.out.println("t2启动");
synchronized (lock) {
// 这里判断size是否等于5,如果不满足条件,则t2进入等待状态
if (c.size() != 5) {
try {
System.out.println("size = " + c.size() + ",t2 wait...");
// wait会让当前线程进入阻塞状态,并释放锁
lock.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("t2结束");
// 注意!!wait状态的线程不会因为其他线程的结束而自动执行,必须叫醒线程1
lock.notify();
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
System.out.println("t1启动");
synchronized (lock) {
for (int i = 0; i < 10; i++) {
c.add(new Object());
System.out.println("add" + i);
// t1线程内部发出一个满足条件时的唤醒消息,notify代表唤醒一个线程,这个唤醒是随机的
if (c.size() == 5) {
System.out.println("size = 5, 叫醒其他线程!");
lock.notify();// notifyAll()可以唤醒全部等待中的线程,随机抢得
try {
lock.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}).start();
}
}
程序四
/**
* 曾经的面试题(淘宝?) 实现一个容器,提供两个方法:add , size 写两个线程,线程1 为容器添加10个元素,线程2实时监控容器中元素的个数,
* 当个数为5时,线程2给出提示并结束 <br>
*
* 给lists添加volatile之后,t2能够接到通知,但是,t2线程的死循环很浪费cpu,如果不用死循环,该怎么做?
* =======================================================================
* 使用Latch(门闩) 代替wait notify来进行通知
* 好处是通信方式简单,同时也可以指定等待时间
* 使用 await和countdown方法来替代wait 和 notify
* CountDownLatch不涉及锁定,当count的值为零时当前线程继续执行
* 当不涉及同步,只是涉及通信的时候,使用synchronized + wait/notify就显得太重了
* 这时应该考虑CountDownLatch/CyclicBarrier/Semaphore
*
* 类名:MyContainer2<br>
* 作者: mht<br>
* 日期: 2018年9月1日-上午11:04:54<br>
*/
public class MyContainer4 {
// 如果不加volatile,lists的变化无法及时被其他线程感知,因此可能导致不可见的问题
volatile List<Object> lists = new ArrayList<>();
public void add(Object o) {
lists.add(o);
}
public int size() {
return lists.size();
}
/**
* 使用wait和notify必须锁定同一个Object,否则将不能使用wait和notify,虽然size=5时线程1发出了notify的命令
* 唤醒了t2,但是notify不会释放lock,t2依然需要阻塞,等待t1执行完后释放锁后才可以执行。另外sleep()也是不释放锁的。
*/
public static void main(String[] args) {
MyContainer4 c = new MyContainer4();
CountDownLatch latch = new CountDownLatch(1);
new Thread(() -> {
System.out.println("t2 启动");
if (c.size() != 5) {
try {
latch.await();// 和o.wait()效果类似
// 也可以指定时间
// latch.await(5, TimeUnit.SECONDS);
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("t2 结束");
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
System.out.println("t1 启动");
for (int i = 0; i < 10; i++) {
c.add(new Object());
System.out.println("add" + i);
if (c.size() == 5) {
// 打开门闩使得t2得以执行
latch.countDown();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
推荐第四种写法,效率高,可读性好。
NTES部门同步CountDownLatch模拟
/**
* 通过CountDownLatch模拟网易邮箱请求多线程
* <br>类名:NtesDemo<br>
* 作者: mht<br>
* 日期: 2018年9月2日-下午12:01:36<br>
*/
public class NtesDemo {
private volatile static Integer apiResult;
private volatile static Integer mysqlResult;
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(2);
new Thread(() -> {
if (apiResult == null || mysqlResult == null) {
System.out.println("apiResult = " + apiResult + ", mysqlResult = " + mysqlResult);
try {
latch.await(5, TimeUnit.SECONDS);
// 两个结果依然有null值,则提示超时了
if (apiResult == null || mysqlResult == null) {
System.out.println("超时了");
} else {
System.out.println("mysqlResult(" + mysqlResult + ") + apiResult(" + apiResult + ") = " + (mysqlResult + apiResult));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 数据库查询
new Thread(() -> {
int count = 0;
for (int i = 0; i < Integer.MAX_VALUE; i++) {
count++;
}
mysqlResult = count;
latch.countDown();
}).start();
// 网易邮箱查询
new Thread(() -> {
int count = 0;
for (int i = 0; i < Integer.MAX_VALUE; i++) {
count++;
}
apiResult = count;
latch.countDown();
}).start();
}
}
执行结果:
总结
使用wait和notify来等待和唤醒线程是一种粒度非常细的操作,使用者需要非常了解两者的特性和使用方法才能够熟练运用。
wait和notify使用时必须将对象锁定,否则无法使用,线程在使用对象的wait方法后会进入等待状态(类似于阻塞,但wait必须由其他线程使用notify才可以唤醒,而阻塞不需要任何唤醒的机制,一直处于竞锁状态),notify()和notifyAll()可以唤醒其他线程,前者为随机唤醒一个线程。
wait和notify的操作是相对复杂的,虽然强大,但是在处理复杂的业务逻辑中书写较麻烦,相当于多线程中的汇编语言。
使用CountDownLatch可以有效的替代wait和notify的使用场景,而且不受锁的限制,书写简便且易于理解。