并发编程之面试题一

并发编程之面试题一

面试题

​ 创建一个容器,其中有两个方法,一个方法是 add(),一个方法时size(),起两个线程,一个线程是往容器中添加1-10这是个数字,另外一个线程在数字添加到5的时候结束。

初始代码

该问题咋一看是一个很简单的面试题,创建两个线程,分别执行对应的任务即可。以下就是简单的代码:

public class Container {
    private List<String> list = new ArrayList<>();
    public void add(String str){
        list.add(str);
    }
    public int size(){
        return list.size();
    }

    public static void main(String[] args) {
        Container container = new Container();
				// 线程1:向容器添加元素
        new Thread(()->{
            for (int i = 1; i < 11; i++) {
                container.add("hello"+i);
                System.out.println("add"+i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        // 线程2:监测线程1追加的元素
        new Thread(()->{
            while (true){
                if(container.size()==5){
                    break;
                }
            }
            System.out.println("线程2结束");
        }).start();
    }
}

分析

​ 但是,在执行以上代码的时候,可以发现线程2不能停止。原因很简单,这涉及了线程之间的通信。程序在启动时,JVM会给每个线程分配一个独立的内存空间(提高执行效率),每个线程独立的内存空间互不干扰,互不影响(即内存的不可见性)。

​ 以上代码中,线程1在执行到添加第5个元素的时候,线程2并不知道容器中的元素已经有5个,故其不能停止。

解决方案

方案一

经过以上分析,可以想到使用 volatile 关键字,来实现内存的可见性。实现只需要将以上代码中的容器用 volitile 关键字修饰:

private volatile List<String> list = new ArrayList<>();

分析:

​ 1)线程没有加锁,线程2取到的可能是6,才会停止;

​ 2)线程2死循环浪费cpu资源。

解决二

public class Container2 {
    private List list = new ArrayList();
    public void add(String str){
        list.add(str);
    }
    public int size(){
        return list.size();
    }
    public static void main(String[] args) {
        Container2 container2 = new Container2();
        Object lock = new Object();
        new Thread(()->{
            synchronized (lock){
                System.out.println("线程2启动");
                if(container2.size()!=5){
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("线程2结束");
                lock.notify(); // wait会释放锁,notify不会释放锁
            }
        },"t2").start();
        new Thread(()->{
            synchronized (lock){
                for (int i = 1; i < 11; i++) {
                    container2.add("hello"+i);
                    System.out.println("add"+i);
                    if(container2.size()==5){
                        // 这里不仅要唤醒线程2,还必须通过wait()释放锁
                        lock.notify();
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        },"t1").start();
    }
}

分析:

​ 这种解决方法,算是一个常见的解决方案了。这里我们要注意几个陷阱。

​ 1)执行wait() 会立即释放锁资源;而执行notify()/notifyAll() 不会立即释放锁资源,要等执行完 synchronize 中的代码才释放资源;

​ 2)wait()、notify()/notifyAll() 要放在 synchronize 代码块中执行。

​ 3)synchronize 是非公平锁,也就是说,如果竞争激烈的话,可能有些线程一直得不到执行。

该方案是常见的解决方案,但是相对来说,代码比较复杂,也不是很好理解。下面出示另一种方案。

解决三

public class Container3 {
    private volatile List list = new ArrayList();
    public void add(String str) {
        list.add(str);
    }
    public int size() {
        return list.size();
    }
    public static void main(String[] args) {
        Container3 container3 = new Container3();
        // 1->0,门闩就打开
        CountDownLatch latch = new CountDownLatch(1);
        new Thread(() -> {
            System.out.println("线程2启动");
            if (container3.size() != 5) {
                try {
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程2结束");
        }, "t2").start();
        new Thread(() -> {
            for (int i = 1; i < 11; i++) {
                container3.add("hello" + i);
                System.out.println("add" + i);
                if (container3.size() == 5) {
                    // latch-1
                    latch.countDown(); // 打开门闩后,并不影响他自己本身运行
                }
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

分析:

​ CountDownLatch 是java1.5 引入的,是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。

​ 如果用实际的场景来类比,可以理解成,一扇门上加了N把门闩,一个人在外面等待,一个人在里面干活,每满足条件一次,就打开一把门闩,当所有的门闩全部打开,另外一个人就可以进去了。

后续思考

  1. 理解线程之间的通信以及其内存模型;
  2. 线程之间通信的几种实现方式;
  3. 通过源码分析 CountDownLatch .
posted @ 2019-04-12 17:04  追梦1819  阅读(642)  评论(6编辑  收藏  举报