並發編程之面試題一
面試題
創建一個容器,其中有兩個方法,一個方法是 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把門閂,一個人在外面等待,一個人在里面干活,每滿足條件一次,就打開一把門閂,當所有的門閂全部打開,另外一個人就可以進去了。
后續思考
- 理解線程之間的通信以及其內存模型;
- 線程之間通信的幾種實現方式;
- 通過源碼分析 CountDownLatch .