(ps:这道题是在马士兵老师的视频上看到的,觉得还不错,所以就单独写出来,据说是某宝曾经的面试题。)
实现一个容器,提供两个方法:add,size
写两个线程,线程1添加10个元素到容器中。线程2实现监控元素的个数。当数到5个时,线程2给出提示并结束。
是不是看这个题目觉得眼熟,好的吧,之前说volatile的时候用过这个demo,只不过这里打算单独好好讲讲。volatile只不过是一个做法,而且是不怎么好的一个做法。下面我一种一种实现方式说。
先列出基本代码:
package thread;
import java.util.ArrayList;
import java.util.List;
public class Test {
List<Integer> list = new ArrayList<Integer>();
public void add(Integer i) {
list.add(i);
}
public int size() {
return list.size();
}
public static void main(String[] args) {
Test test = new Test();
new Thread(()->{
System.out.println("t1 开始");
for(int i = 0;i<10;i++) {
test.add(i);
System.out.println("add +"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("t1 结束");
}).start();
new Thread(()->{
while(true) {
if(test.size() == 5) {
break;
}
}
System.out.println("t2 结束");
}).start();
}
}
上面列出的是最基本的代码,没有任何同步措施,所以是不对的,然后为什么不对说过了,咱们说怎么改对:
volatile解决
package thread;
import java.util.ArrayList;
import java.util.List;
public class Test {
volatile List<Integer> list = new ArrayList<Integer>();
public void add(Integer i) {
list.add(i);
}
public int size() {
return list.size();
}
public static void main(String[] args) {
Test test = new Test();
new Thread(()->{
System.out.println("t1 开始");
for(int i = 0;i<10;i++) {
test.add(i);
System.out.println("add +"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("t1 结束");
}).start();
new Thread(()->{
while(true) {
if(test.size() == 5) {
break;
}
}
System.out.println("t2 结束");
}).start();
}
}
如上代码,只不过list上添加了一个volatile就使得t2能够运行进if中,线程得已结束。

但是这有一个大问题,我这里之所以运行正常是因为是这每添加都睡了一秒。实际上不睡的话就会有问题,我直接上截图:

看出来了吧,这个问题就是因为时间问题,可能都加到10个元素了,t2这句话才打印出来。也就是说准确性不行。你就说图中这个情况,算是完成了这个需求还是每完成?标准意义上是每完成吧,但是volatile也就能保证这样啊,还想更精确就得换个思路。
wait/notify方法解决
wait方法是Object类的方法,其实就是让当前线程进入到等待阶段,同时释放锁和cpu。(这也是小知识点,sleep只会睡不会释放锁和cpu。不太了解的建议去补习这方面的基础)
notify是唤醒其中一条等待的线程,notifyAll是唤醒所有等待的线程。咱们这里用notify就行了。
然后其实这个复杂的地方是当size添加到五个,线程1就wait,然后线程2执行,执行完唤醒线程1是最直接的最简单的办法:
package thread;
import java.util.ArrayList;
import java.util.List;
public class Test {
List<Integer> list = new ArrayList<Integer>();
public void add(Integer i) {
list.add(i);
}
public int size() {
return list.size();
}
public static void main(String[] args) {
Test test = new Test();
final Object lock = new Object();
new Thread(()->{
System.out.println("t1 开始");
synchronized (lock) {
for(int i = 0;i<10;i++) {
test.add(i);
System.out.println("add +"+i);
if(i==4) {
try {
lock.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
}
}
System.out.println("t1 结束");
}).start();
new Thread(()->{
synchronized (lock) {
while(true) {
if(test.size() == 5) {
break;
}
}
System.out.println("t2 结束");
lock.notify();
}
}).start();
}
}
但是其实这个代码也有两点小问题:
- 这个代码必须保证线程1先进入锁。如果线程2先拿到锁就会死锁了。
- 这个代码中while(true)是个不合理的消耗。
然后改进的办法其实说简单也简单,就是两边再加个wait/notify。如果先进入线程2,size还不是5则wait,让锁给线程1,而线程1在到了5个元素的时候唤醒线程2并且自身wait(因为notify不释放锁,只是唤醒线程2是不能让线程2正常启动的。),剩下的和上面的代码一样。我去改动下:
package thread;
import java.util.ArrayList;
import java.util.List;
public class Test {
List<Integer> list = new ArrayList<Integer>();
public void add(Integer i) {
list.add(i);
}
public int size() {
return list.size();
}
public static void main(String[] args) {
Test test = new Test();
final Object lock = new Object();
new Thread(() -> {
synchronized (lock) {
if (test.size() != 5) {
try {
lock.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("t2 结束");
lock.notify();
}
}).start();
new Thread(() -> {
System.out.println("t1 开始");
synchronized (lock) {
for (int i = 0; i < 10; i++) {
test.add(i);
System.out.println("add +" + i);
if (i == 4) {
try {
lock.notify();
lock.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
System.out.println("t1 结束");
}).start();
}
}
CountDownLatch方法解决
其实上面的wait/notify方法实现是能实现了,问题是性能啊。各种唤醒等待,还有互斥锁来回来去互斥。是一个解决办法, 但是还有更好了,这里就用到了出入锁了!
这个锁就是java中的一个类CountDownLatch。这个类通俗讲就是一个计数器。可以选择计数器减值。和等待,等到计数器值为0才能进入。我记得以前专门写过这个类的一些原理和方法,有兴趣的可以自己去搜索试试。我这里就是简单的说两句。然后我字节贴代码:
package thread;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
public class Test {
List<Integer> list = new ArrayList<Integer>();
public void add(Integer i) {
list.add(i);
}
public int size() {
return list.size();
}
public static void main(String[] args) {
Test test = new Test();
CountDownLatch countDownLatch = new CountDownLatch(1);
new Thread(() -> {
if (test.size() != 5) {
try {
// 开始算这个计数器
countDownLatch.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("元素个数到5,t2 结束");
}).start();
new Thread(() -> {
System.out.println("t1 开始");
for (int i = 0; i < 10; i++) {
test.add(i);
System.out.println("add +" + i);
if (i == 4) {
countDownLatch.countDown();
}
}
}).start();
}
}
然后这个题从性能的角度来说是最好的解法,然后目前为止这个题我也就只有这三种解法。
今天的笔记就记到这里了,如果稍微帮到你了记得点个喜欢点个关注,另外这块内容大部分出自于马士兵老师的讲解,在此表示感谢。也祝大家工作顺顺利利,生活健健康康~!
网友评论