並發編程之面試題一


並發編程之面試題一

面試題

​ 創建一個容器,其中有兩個方法,一個方法是 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 .


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM