最近在梳理多线程的问题,遇到一个多线程的面试题,由此引发了一点思考:
自己与优秀的工程师差距在哪里,这个面试题让自己看到了差距。
希望自己以后在注重量的同时,能更多的关注质。
题目:
实现一个容器,提供两个方法:add,size。 写两个线程,线程1添加10个元素到容器中,线程2实现容器元素个数的监听,当个数达到5个时,线程2给出提示并结束。
答案:
方式一:
public class T12 {
volatile List list = new ArrayList();
// public <T extends Number> void add(T o){
// list.add(o);
// }
public void add(Object o){
list.add(o);
}
public int size(){
return list.size();
}
public static void main(String[] args) {
T12 t12 = new T12();
//first thread add element
new Thread(()->{
for (int i = 0; i < 10; i++) {
t12.add(i);
System.out.println("add " + i);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"thread1").start();
//second thread
new Thread(()->{
while(true){
if(t12.size() == 5){
break;
}
}
System.out.println("T2 end");
},"thread2").start();
}
}
方式一的thread2使用了while死循环,浪费cpu,改进?
方式二:
public class T13 {
volatile List<Integer> list = new ArrayList<>();
public void add(Integer integer){
list.add(integer);
}
public int size(){
return list.size();
}
public static void main(String[] args) {
T13 t13 = new T13();
final Object lock = new Object();
//开启thread2
new Thread(() -> {
synchronized (lock) {
System.out.println("thread2 begin");
if (t13.size() != 5) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("thread2 end");
}
}, "thread2").start();
//休眠1s
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//开启thread1
new Thread(() -> {
synchronized (lock) {
System.out.println("thread1 begin");
for (int i = 0; i < 10; i++) {
t13.add(i);
System.out.println("add" + i);
//达到条件时,唤醒线程
if (t13.size() == 5) {
lock.notify();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "thread1").start();
}
}
方式二改进了方式一的死循环问题,使用wait和notify。运用这种方式,必须保证thread2先运行,也就是让thread2先进入监听状态。
但是,运行结果thread2 end在最后打印出来,没有在size==5时打印出来
原因:
thread1 size=5时,notify会唤醒thread2,但是notify因为不会释放锁,导致即使唤醒thread2,但因为thread2拿不到lock锁对象,所以无法继续执行。
改进方式二:
public class T14 {
volatile List list = new ArrayList();
public void add(Object o){
list.add(o);
}
public int size(){
return list.size();
}
public static void main(String[] args) {
T14 t14 = new T14();
final Object lock = new Object();
new Thread(()->{
synchronized (lock){
System.out.println("thread2 begin");
if(t14.size() != 5){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("thread2 end");
lock.notify();//thread2执行完毕,唤醒thread1继续执行
}
},"thread2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
synchronized (lock){
System.out.println("thread1 begin");
for (int i = 0; i < 10; i++) {
t14.add(i);
System.out.println("add " + i);
if(t14.size() == 5){
lock.notify();//唤醒当前休眠的thread1,因notify不释放锁,所以需要wait方法,使thread2休眠,释放锁给thread1
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("thread1 end");
}
},"thread1").start();
}
}
改进后的方式二主要在:thread1中添加了wait方法,thread2中添加了notify方法。
但是通过程序也可以看到,改进后的方法,多次使用wait()\notify()方式过于繁琐。
方式三:
public class T15 {
volatile List list = new ArrayList();
public void add(Object o){
list.add(o);
}
public int size(){
return list.size();
}
public static void main(String[] args) {
T15 t15 = new T15();
//定义一个门闩,值为1;当值=0时,门闩解开
CountDownLatch countDownLatch = new CountDownLatch(1);
//开启thread2
new Thread(() -> {
System.out.println("thread2 begin");
if (t15.size() != 5) {
try {
countDownLatch.await();//休眠,释放资源
//可以指定等待时间
// countDownLatch.await(1000,TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("thread2 end");
}, "thread2").start();
//休眠1s
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//开启thread1
new Thread(()->{
System.out.println("thread1 begin");
for (int i = 0; i < 10; i++) {
t15.add(i);
System.out.println("add " + i);
if(t15.size() == 5){
//打开门闩,让thread2继续执行
//countdown使门闩值减1
countDownLatch.countDown();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"thread1").start();
}
}
方式三使用Latch(门闩)替代wait、notify进行通知。
优势:通信方式简单,可以设置等待时间
使用await和countdown替代wait、notify,CountDownLatch不涉及锁定,当count的值为0当前线程继续执行。
当不涉及到同步,只是线程通信的话,使用synchronized+wait+notify方式显得太重。这时应该考虑countdownlatch/cyclicbarrier/semaphore。
门闩latch使用方法:
- 初始化一个门闩countDownLatch,并赋初值;
- 开两个线程,当thread1执行到一定程度时,使用await方法休眠thread1;
- thread2开始执行,当thread2执行到一定程度,想唤醒thread1时,使用countDown方法进行countDownLatch减一操作;
- 当countDownLatch=0时,thread1线程被唤醒,继续执行。
这个面试题其实网上答案很多,但是解决问题的方式应该是我们需要学习的,各位平时看我博客的学弟、学妹,让我们一起提高自己的学习质量。
更多多线程源码整理:https://github.com/dhphust/JavaTrain/tree/master/src/test/java/MultiThread