2020年,截止目前,我收到了阿里巴巴、騰訊、美團、京東、快手等互聯網大廠的面試邀請。求職是一場流程很長的拉鋸戰,涉及崗位選擇、簡歷投遞、簡歷評估、技術面試、HR面試等環節。
我發現在技術面試中多線程在面試中出現的次數非常非常多,幸好我面試之前也有所準備。今天結合面試經歷寫一篇面向面經的Java線程安全有關的最全知識彙總。
本文很乾貨,很乾!請自帶茶水
進程和線程
進程 | 線程 | |
---|---|---|
量級 | 重量級 | 輕量級 |
內存 | 私有內存 | 共享內存 |
同步機制 | 不需要 | 需要 |
處理安全性 | 殺死進程是安全的 | 殺死線程是不安全的 |
- 進程:擁有整臺計算機的資源。私有空間,彼此隔離。
- 多進程之間不共享內存
- 進程之間通過消息傳遞進行協作
- 一般來說,進程== 程序 ==應用,但一個應用中可能包含多個進程
- OS支持的IPC機制(pipe/socket)支持進程間通信
不僅是本機的多個進程之間,也可以是不同機器的多個進程之間。
- JVM通常運行單一進程,但也可以使用ProcessBuilder 創建新的進程。
- 線程
程序內部的控制機制。
- 進程=虛擬機;線程=虛擬CPU
- 程序共享、資源共享,共享內存
線程Thread
從Thread類派生子類
public class HelloThread extends Thread {
public void run() {
System.out.println("Hello from a thread!");
}
public static void main(String args[]) {
HelloThread p = new HelloThread();
p.start();
}
//----------啓動該線程的兩個方式
public static void main(String args[]) {
(new HelloThread()).start();
}
}
從Runnable接口構造Thread對象
public class HelloRunnable implements Runnable {
public void run() {
System.out.println("Hello from a thread!");
}
public static void main(String args[]) {
(new Thread(new HelloRunnable())).start();
}
}
常見創建方法:
new Thread(new Runnable() {
@Override
public void run() {
}
});
併發很難測試和調試因爲競爭條件導致的bug。因爲交錯interleaving的存在,導致很難復現bug
Thread.sleep():使得線程在一定時間內休眠,進入休眠的線程不會失去對現有monitor或鎖的所有權。
Thread.interrupt() 中斷
Thread.yield():使用該方法,線程告知調度器:我可以放棄CPU的佔用權,從而可能引起調度器喚醒其他線程(儘量避免在代碼中使用)。
public void run() {
...
for (int i = 0; i < 5; i++) {
if ((i % 5) == 0)
Thread.yield();
}
}
Thread.join():讓當前線程保持執行,直到其執行結束。
線程池
參考鏈接:https://www.cnblogs.com/cdf-opensource-007/p/8769777.html
Excutors創建線程池便捷方法如下:
Executors.newFixedThreadPool(100);//創建固定大小的線程池
Executors.newSingleThreadExecutor();//創建只有一個線程的線程池
Executors.newCachedThreadPool();//創建一個不限線程數上限的線程池,任何提交的任務都將立即執行
對於服務端需要長期運行的程序,創建線程池應該使用ThreadPoolExecutor
的構造方法
public ThreadPoolExecutor(
int corePoolPoolSize,//線程池長期維持的線程數
int maximumPoolSize, //線程數的上限
long keepAliveTime,//空閒線程存活時間
TimeUnit unit,//時間單位
BlockingQueue<Runnable> workQueue,//任務的排隊隊列
ThreadFactory threadFactory,//新線程的產生方式
RejectedExecutionHandler handler//拒絕策略
)
java線程池有7大參數,四大特性。
特性一:當池中正在運行的線程數(包括空閒線程)小於corePoolSize時,新建線程執行任務。
特性二:當池中正在運行的線程數大於等於corePoolSize時,新插入的任務進入workQueue排隊(如果workQueue長度允許),等待空閒線程來執行。
特性三:當隊列裏的任務數達到上限,並且池中正在運行的線程數小於maximumPoolSize,對於新加入的任務,新建線程。
特性四:當隊列裏的任務數達到上限,並且池中正在運行的線程數等於maximumPoolSize,對於新加入的任務,執行拒絕策略(線程池默認的拒絕策略是拋異常)。
線程安全的策略
-
限制數據共享
-
共享不可變數據
-
共享線程安全的可變數據
-
同步機制:通過鎖的機制共享線程不安全的可變數據,變並行爲串行
非同步機制
策略一:限制數據共享
線程之間不共享mutable數據類型
import java.math.BigInteger;
public class Main {
public static void computeFact(final int n){
BigInteger result = new BigInteger("1");
for(int i = 1;i<=n;i++) {
System.out.printf("Fact %d is working\n",i);
result = result.multiply(new BigInteger(String.valueOf(i)));
}
System.out.printf("Fact %d is %d\n",n,result);
}
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
computeFact(99);
}
});
thread1.start();
computeFact(100);
}
}
避免全局變量。例如下面是不安全的,存在多個線程,同時訪問getInstance()方法,創建出兩個PinballSimulator 對象。
策略二: Immutability
使用不可變數據類型和不可變引用,避免多線程之間的race condition
策略三:線程安全的數據類型
如果必須要用mutable的數據類型在多線程之間共享數據,要使用線程安全的數據類型。
同步機制
Lock鎖機制
可重入鎖:基於線程的分配。
讀寫鎖:對一個資源的訪問分成了2個鎖,比如文件,分爲讀鎖和寫鎖。例如ReadWriteLock()
可中斷鎖:可以中斷的鎖機制。例如Lock是可中斷鎖。
公平鎖: 以請求鎖的順序來獲取鎖。有多個線程在等待一個鎖,當鎖被釋放時,等待時間最久的線程會獲取該鎖,公平鎖。
syncronized
代碼塊
對某一代碼塊使用,sychronized後面的括號裏面是變量,一次只有一個線程進入該代碼塊。
public void synchroMethod(int m){
synchronized (m){
}
}
方法聲明時
方法聲明時使用,表示一次只能有一個線程進入該方法,其他線程要想在此時調用該方法,只能排隊。
public synchronized void synchroMethod(int m){
}
synchronized後面括號裏是對象
線程獲得的是對象鎖,synchronized後面括號裏是一個對象。
public void synchroMethod(int m){
synchronized(this){
}
}
注意,構造方法沒有必要使用synchronized方法,因爲構造方法的對象在從構造器返回之前一直被限制在單線程中。
synchronized method 與 synchronized(this) block的區別:
-
後者需要顯式的給出lock,且不一定非要是this
-
後者可停工更細粒度的併發控制
同步機制給性能帶來極大影響。除非必要,否則不要用。Java中很多mutable的類型都不是threadsafe就是這個原因。
儘可能減小lock的範圍。直接使用synchronized同步Method,說明沒有先思考清楚到底lock誰,然後再synchronized(…)。
public static synchronized boolean findReplace(EditBuffer buf, ...)
將獲得靜態鎖,在class層面上鎖。同一時間內只有一個線程能夠執行該方法,即使其他線程在不同的內存區取用數據,是安全的。這對性能帶來極大損耗。
-
Synchronized不是靈丹妙藥,你的程序需要嚴格遵守設計原則,先嚐試其他辦法,實在做不到再考慮lock。
-
所有關於threadsafe的設計決策也都要在ADT中記錄下來。
如果A線程在synchronized (list) { … }
ArraysList方法的add(),其中size是全局變量,沒有synchronization,存在線程不安全的風險
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
Collections.synchronizedList方法是線程安全的。
List<String> sharedList = Collections.synchronizedList(new ArrayList<String>());
在iterate部分需要使用synchronized(sharedList) { … } 來block
-
synchronized static是某個類的範圍,它可以對類的所有對象實例起作用。
-
synchronized 是某實例的範圍,只對一個實例起作用,synchronized isSync(){}防止多個線程同時訪問這個實例中的synchronized 方法。
Locking原則
-
任何共享的mutable變量/對象必須被lock所保護
-
如果一個不變量涉及到多個mutable變量的時候,它們必須被同一個lock所保護
-
monitor pattern中,ADT所有方法都被同一個synchronized(this)所保護
原子操作
不可被中斷的一個或一系列操作。減少內存一致性的錯誤風險。
-
輕量級的同步機制
-
原子變量的改變對於其他線程是可見的
private volatile int counter;
優點:比synchronized更加有效
缺點:需要更多地關注內存一致性
Liveness: deadlock, starvation and livelock
死鎖
多個線程競爭lock,相互等待對方釋放lock
死鎖的解決方案
- 設置鎖獲取的順序
缺點:
-
它不是模塊化的-代碼必須知道系統或至少子系統中的所有鎖。
-
在獲取第一個鎖之前,代碼可能很難知道它將需要哪些鎖。它可能需要做一些計算才能弄清楚。
2. coarser locking
使用單個鎖監管多個對象實例,甚至是程序的一個子系統。
例如,下列代碼使用Castle的對象鎖進行同步化
缺點:
-
用單個鎖監聽很多可變數據,不能實時獲取這些數據
-
在最壞的情況下,用一個鎖保護程序中所有的東西,程序變成單線程
Starvation
因爲其他線程lock時間太長,一個線程長時間無法獲取其所需的資源訪問權(lock),導致無法往下進行。
Livelock
一個線程經常會響應另一個線程的動作。
線程不會被阻斷,他們可能忙於響應其他線程而不能恢復工作。
線程間協作的方法
wait()
該操作使object所處的當前線程進入阻塞/等待狀態,直到其他線程調用該對象的notify()操作
public synchronized void guardedJoy() {
// This guard only loops once for each special event,
// which may not be the event we're waiting for.
while(!joy) {
try {
wait();
} catch (InterruptedException e) {}
}
System.out.println("Joy and efficiency have been achieved!");
}
notify()與notifyAll()
隨機選擇一個在該對象上調用wait方法的線程,解除其阻塞狀態
public synchronized notifyJoy() {
joy = true;
notifyAll();
}
總結
咱們玩歸玩,鬧歸鬧,別拿面試開玩笑。
線程安全在面試中出現的次數非常非常多,一旦問到了,大家一定要回答全面,不要丟三落四,回答到點上。大家面試前要把基礎打牢,多寫併發線程的程序代碼,多線程在筆試題中也很常見。