• 并发编程之面试题一


    并发编程之面试题一

    面试题

    ​ 创建一个容器,其中有两个方法,一个方法是 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 .
  • 相关阅读:
    黎活明给程序员的忠告
    servlet单实例多线程模式
    Servlet 获取多个参数
    Java Servlet学习笔记(四)Servlet客户端Http请求
    JavaWeb 后端 <二> 之 Servlet 学习笔记
    Servlet 规范笔记—基于http协议的servlet
    Servlet 规范笔记—servlet概念及结构
    看懂UML类图和时序图
    hibernate中的事务管理是怎么概念?
    Jquery中$.get(),$.post(),$.ajax(),$.getJSON()的用法总结
  • 原文地址:https://www.cnblogs.com/yanfei1819/p/10697156.html
Copyright © 2020-2023  润新知