多執行緒併發程式設計面試常考
阿新 • • 發佈:2020-09-12
物件在記憶體中的記憶體佈局
用sychronized
鎖住物件後該物件的鎖狀態升級過程:new
- 無鎖態 - 偏向鎖 - 輕量級鎖/自旋鎖/無鎖 (CAS)- 重量級鎖 - GC標記資訊
執行緒的幾個狀態
- NEW(新建狀態)
- Runnable
- Ready(就緒狀態,執行緒被放在等待佇列中,等著被CPU執行)
- Running(執行狀態,被扔到CPU中執行)
- Blocked
- Waiting
- TimedWaiting
- Terminated(終止態)
三種新建執行緒的方法
- 實現
Thread
類 - 實現
Runnable
介面 - 執行緒池
執行緒的常用方法:
sleep()
,沉睡一段時間(當前執行緒回到就緒狀態),這段時間CPU執行其它執行緒yield()
,和sleep()
類似,讓出CPU,當前執行緒回到就緒狀態。使用很少見join()
,通知其它執行緒獲得CPU執行,比如在t1
執行緒內執行t2.join()
,意思就是t1
執行緒通知t2
執行緒執行,自己回到就緒狀態。
Synchronized
講解
synchronized
實現過程:(不能禁止指令重排)
- Java程式碼:
synchronized
monitorenter
、moniterexit
- 執行過程中自動升級(偏向鎖、自旋鎖、重量級鎖)
- 更底層的實現
lock comxchg
volatile
講解:
- 保證變數的各執行緒可見性/資料一致性 (多個執行緒要用到變數時,重新去記憶體拿)
- 禁止CPU指令重排(在單執行緒沒問題,多執行緒就會出現問題。為什麼要指令重排,其實就是因為CPU太快了,而訪問記憶體比訪問快取又慢了太多)
- 舉個例子:物件的初始化三個步驟
Person p = new Person("zeng", 24);
- 申請物件
Person
的記憶體,這個時候給例項變數設定了預設值,比如name = null; age = 0;
- 呼叫該物件的建構函式進行真正的初始化例項變數
name = "zeng"; age = 24;
- 返回物件
Person
給p
- 申請物件
- 舉個例子:物件的初始化三個步驟
volatile
不能實現synchronized
的原子性操作- 比如定義一個變數
volatile int count = 0;
count++
加1000次,最終的count
不一定會是10000,因為這裡的count++
並不是一個原子性操作,它包含好幾個指令,所以為了要實現整個的count++
原子性操作,也就是必須要使用sychronized
對count++
加鎖。
- 比如定義一個變數
再注意一些問題:
- 在用
synchronized
鎖住一個物件時,這個時候不能將這個引用去指向另一個物件 - 不要用
synchronized
去鎖一個String、Integer
等基本資料型別的封裝類的物件
CAS(無鎖優化/自旋):
CompareAndSwap
Java
裡面java.util.concurrnet.atomic.AtomicXXX
開頭的類都是使用CAS自旋鎖實現的。內部都是使用UnSafe
這個類的compareAndSet
等操作實現執行緒安全地修改值- 舉個例子:
AtomicInteger count = new AtomicInteger(0);
在上面的volatile
的討論中,count++
如果不加sychronized
鎖會導致非原子性操作,但這裡直接使用AtomicInteger
即可實現執行緒可見、原子性操作,將count++
到10000。並且不需要volatile、synchronized
。
- 舉個例子:
- ABA問題(1變為2又被變為1),加版本號
version
- 所有的
Java
中CAS
的操作基本上都是用的UnSafe
這個類,這個UnSafe
使Java
語言有了像C++
的直接操作JVM
記憶體的能力。
ReentrantLock(可重入鎖,公平鎖(預設是非公平鎖))本身底層也是CAS
- 可以替代
synchronized
,替換方法:lock.lock();
- 可以通過
lock.interupt
的方法將該鎖設定為可以通過interup
方法喚醒正在wait
的執行緒 - 相比上個特點,
synchronized
的執行緒,wait
之後必須通過其它執行緒的notify()
才能喚醒 - 如果設定為公平鎖,那麼執行緒在搶一個資源時,會進入優先佇列排隊按先後順序等待
synchronized
是非公平鎖synchronized
自動加鎖解鎖,ReentrantLock
手動加鎖解鎖lock.lock()
- 底層實現:
ReentrantLock
是CAS的實現,synchronized
底層是有鎖的升級過程(4種)
CountDownLatch鎖(倒計時完了繼續執行(門栓))
CyclicBarrier鎖(當執行緒數目到達某個數目(柵欄值)時,繼續執行後面的事物)
Phase鎖(階段鎖,CyclicBarrier的升級版本,有多個階段,比如結婚現場有7個人,先7人到達現場,再7人吃完飯,再xxxxx)
ReadWriteLock(共享鎖、排他鎖、多個執行緒可以一起執行)
Semaphore(訊號量,用於限流(僅允許幾個執行緒同時工作))
Exchanger(兩個執行緒執行時交換值)
LockSupport(可以通過park()
方法隨時將執行緒停止,並通過unpark()
方法隨時讓某執行緒就緒)
面試題1:定義兩個執行緒,A執行緒往容器裡放資料,B執行緒監測容器容量為5時,停止執行
- 有3種方法
- 使用
wait()
與notify()
方法的組合。這個很重要 - 使用門栓鎖
CountDownLatch
- 使用
LockSupport
直接park()
與unpark()
面試題2:順序列印A1B2C3……
面試題3:生產者消費者問題
版本1 通過synchronized、wait()、notify()
實現
package zr.thread;
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
/*
生產者與消費者實現1
寫一個固定容量同步容器,擁有put和get方法, 以及getCount方法
能夠支援2個生產者執行緒以及10個消費者執行緒的阻塞呼叫
使用wait()和notifyAll()來實現
這個方法是有瑕疵的,因為使用notifyAll()會喚醒所有的其它等待佇列的執行緒,包括生產者、消費者
有沒有辦法只喚醒生產者,或者只喚醒消費者?
*/
/**
* @author ZR
* @Classname MyContainer1
* @Description 生產者消費者最簡單寫法
* @Date 2020/9/12 21:02
*/
public class MyContainer1<T> {
final private LinkedList<T> lists = new LinkedList<>();
// 最多10個元素
final private int MAX = 10;
private int count = 0;
// 因為++count所以要加synchronized
public synchronized void put(T t){
// 想想為什麼用while而不是if
while(lists.size() == MAX){
try{
this.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
lists.add(t);
++count;
// 通知所有消費者執行緒消費
// 這個方法其實是有點小瑕疵的,因為notifyAll()會叫醒所有的其它wait()執行緒,也包括了另一個生產者
this.notifyAll();
}
// 因為--count所以要加synchronized
public synchronized T get(){
T t = null;
while(lists.size() == 0){
try{
this.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
t = lists.removeFirst();
--count;
// 通知生產者進行生產
// 這個方法其實是有點小瑕疵的,因為notifyAll()會叫醒所有的其它wait()執行緒,也包括了其它消費者
this.notifyAll();
return t;
}
public static void main(String[] args){
MyContainer1<String> c = new MyContainer1<>();
// 啟動消費者執行緒
for(int i = 0; i < 10; i++){
new Thread(()->{
for(int j = 0; j < 5; j++)
System.out.println(c.get());
}, "customer" + i).start();
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 啟動生產者執行緒
for(int i = 0; i < 2; i++){
new Thread(()->{
for(int j = 0; j < 25; j++)
c.put(Thread.currentThread().getName() + " " + j);
}, "producer" + i).start();
}
}
}
版本2 通過ReentrantLock
實現
package zr.thread;
import com.sun.org.glassfish.external.statistics.CountStatistic;
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author ZR
* @Classname MyContainer2
* @Description TODO
* @Date 2020/9/12 21:27
*/
public class MyContainer2<T> {
final private LinkedList<T> lists = new LinkedList<>();
// 最多10個元素
final private int MAX = 10;
private int count = 0;
private Lock lock = new ReentrantLock();
// Condition的本質就是等待佇列,在這裡生產者在生產者的佇列,消費者在消費者的佇列
// 在Container1例中,等待佇列只有一個,生產者和消費者都在裡邊兒
private Condition producer = lock.newCondition();
private Condition customer = lock.newCondition();
public void put(T t){
try {
// 需要手動加鎖
lock.lock();
while(lists.size() == MAX)
producer.await();
lists.add(t);
++count;
// 通知消費者執行緒進行消費
customer.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 手動解鎖
lock.unlock();
}
}
public T get(){
T t = null;
try {
lock.lock();
while(lists.size() == 0)
customer.await();
t = lists.removeFirst();
--count;
// 通知生產者執行緒生產
producer.signalAll();
} catch (InterruptedException e){
e.printStackTrace();
} finally {
lock.unlock();
}
return t;
}
public static void main(String[] args){
MyContainer2<String> c = new MyContainer2<>();
// 啟動消費者執行緒
for(int i = 0; i < 10; i++){
new Thread(()->{
for(int j = 0; j < 5; j++)
System.out.println(c.get());
}, "customer" + i).start();
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 啟動生產者執行緒
for(int i = 0; i < 2; i++){
new Thread(()->{
for(int j = 0; j < 25; j++)
c.put(Thread.currentThread().getName() + " " + j);
}, "producer" + i).start();
}
}
}