1. 進程和線程的區別
答:區別總結如下:
- 進程是程序的一次執行過程,是系統進行資源調度分配的基本單位;
- 線程是進程的更小的運行單位,一個進程擁有多個線程,線程間共享地址空間和資源;
- 線程上下文切換比進程上下文切換要快;
- 線程一般沒有系統資源,但也有一些必不可少的資源,如ThreadLocal。
1.1 切換速度的差異原因
答:總結爲:
- 進程切換時,涉及當前進程cpu環境的保存和新調度進程cpu環境的設置;
- 線程切換時,僅需要保存和設置少量寄存器內容。
1.2 進程間的通信方式
答:Socket套接字,共享內存,管道通信。
1.3 線程能擁有自己的資源嗎?
答:能,通過ThreadLocal存儲線程特有對象。
1.4 進程給了線程什麼資源
答:線程共享進程的方法區、堆。線程獨立資源有程序計數器、虛擬機棧和本地方法棧。
1.5 程序計數器爲什麼私有
答:主要爲了線程切換後能恢復到正確的執行位置。
作用:
- 字節碼解釋器通過改變程序計數器讀取指令;
- 多線程模式下,用來記錄當前線程的執行位置。
1.6 虛擬機棧和本地方法棧爲什麼私有
答:主要是爲了保證線程中的局部變量不被別的線程訪問到。
2. 多線程和單線程的關係
答:概括爲:
- 多進程指os中同時運行多個程序
- 多線程是指一個進程,併發執行多個線程,每個線程有自己的功能;
- 多線程是CPU輪詢時間片模式,提高資源利用率;
- 多線程會降低程序執行速度,因爲存在線程上下文切換,但能減少用戶的等待響應時間,還會帶來線程死鎖等安全問題。
2.1 併發和並行的關係
答:併發指同一時間段,多個任務都在執行。並行指單位時間內,多個任務同時執行。
eg. 8-9點,我洗臉刷牙吃飯->併發,我左手洗臉右手刷牙->並行。
2.2 Java中的多線程
答:Java中,運行程序就相當於啓動JVM的進程,main函數相當於主線程,main函數調用的A,B,C方法相當於多個線程,四者同時執行競爭CPU。
3. 線程的生命週期和狀態
答:線程狀態包括 新建、運行、阻塞等待和消亡。阻塞等待分爲Blocked、Waiting和Time Waiting。
我們可以參照源碼中Thread狀態的枚舉定義。
- New新建:創建後尚未調用start方法
- Runnable可運行:可能是正在運行或者正在等待CPU資源
- Blocked阻塞:線程進入同步塊中,需要申請一個同步鎖而進行的等待
- Waiting無限期等待:調用了Object.wait()或Object.notify()或LockSupport.park()方法,無限期等待其他線程來喚醒
- Time Waiting有限期等待:調用Thread.sleep()等方法,區別是等待時間是明確的
- Terminated消亡:線程執行結束或產生異常提前結束
4. 多線程編程中常用函數
答:常用函數的比較總結如下:
- sleep方法:Thread類的方法,讓線程進入有限期等待休眠,之後自動甦醒。休眠不釋放鎖。常用於暫停執行。
- wait方法:Object類的方法,與synchronized一起使用,線程進入有限期或無限期等待,被notify方法調用才能解除阻塞,只有重新佔用互斥鎖才能進入Runnable。休眠釋放互斥鎖。常用於線程間通信交互。wait(long timeout)超時後也會自動甦醒。
- join方法:當前線程調用,其他線程全部停止,等待當前線程執行結束再執行。Stop the world
- yield方法:讓線程放棄當前獲得的CPU時間,使線程仍處於Runnable,隨時可以再獲得CPU時間。
5. 線程死鎖
答:定義爲多個線程之間相互等待對方而被無限期阻塞。
5.1 四個必要條件
答:OS的基礎知識:
- 資源互斥:一個資源任意時刻只能被一個線程使用
- 請求和保持:一個線程因請求資源而阻塞時,對已獲得的資源保持不放
- 不剝奪:線程已獲得的資源,再未使用完之前,不能強行剝奪
- 循環等待:若干線程間形成頭尾相接的循環等待資源狀態
5.2 避免死鎖的方法
答:破壞死鎖產生的四個條件中的任一:
- 破壞資源互斥:做不到
- 破壞請求和保持:一次性申請全部資源
- 破壞不剝奪:佔用部分資源的線程再申請其他資源時,若申請不到就主動釋放其佔有的資源
- 破環循環等待:鎖排序法,指定獲取鎖的順序(也可認爲指定獲取資源的順序),比如:只有獲得A鎖的線程才能獲得B鎖,只有AB鎖都獲得的才能操作資源C。
6. 線程鎖死
答:定義爲等待線程由於喚醒條件無法成立或其他線程無法喚醒這個線程,讓此線程一直處於非運行狀態。
6.1 線程鎖死分類
- 信號丟失鎖死:沒有對應的線程來喚醒等待線程,導致一直等待。
- 嵌套監視器鎖死:由於嵌套鎖導致等待線程永遠無法被喚醒的故障。比如,線程只釋放了內層鎖Y.wait(),沒有釋放外層鎖X;但通知線程必須獲得外層鎖X,才能通過Y.notify()喚醒,這就出現嵌套等待現象。
7. 線程其他故障
7.1 線程活鎖
答:定義爲線程一直處於運行狀態,但其執行的任務沒有任何進展。比如,線程一直在請求其需要的資源,但無法申請成功。
7.2 線程飢餓
答:線程一直無法獲得其所需的資源致使任務無法運行的情況。
7.3活性故障間轉換
答:線程飢餓發生時,如果線程處於Runnable狀態,就轉變爲活鎖。線程死鎖也是線程飢餓。
8. Java實現線程的方法
答:java中有三種方法實現線程。實現Runnable接口,實現Callable接口,繼承Thread類。
建議採用實現接口的方式,因爲繼承整個Thread類開銷過大且Java不支持多重繼承,但支持多接口繼承。
8.1 實現Runnable接口
繼承Runnable接口,實現run方法,通過Thread調用start()啓動線程。
public class MyRunnable implements Runnable {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyRunnable instance = new MyRunnable();
Thread thread = new Thread(instance);
thread.start();
}
8.2 實現Callable接口
以Callable做參數創建FutureTask類,通過Thread調用start()啓動線程。
public class MyCallable implements Callable<Integer> {
public Integer call() {
return 123;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
8.3 繼承Thread類
通過start()啓動,因爲Thread類也是實現Runnable接口,所以需要重寫run()。
public class MyThread extends Thread {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
9. 原子性、可見性、有序性
答:線程安全體現在這三個方面。
9.1 原子性
定義:一組操作要麼完全發生,要麼沒有發生,其餘線程不會看到中間過程的存在。
實現方式:排他鎖;CAS(Compare And Swap);volatile關鍵字修飾
注意問題:原子性針對多個線程的共享變量,對於局部變量來說無意義;volatile關鍵字只能保證寫操作的原子性。
9.2 可見性
定義:一個線程對共享變量的更新對於另外一個線程是否可見的問題。
實現方式:刷新處理器緩存,讓其他處理器的更關心同步到當前處理器緩存中;當前處理器更新共享變量後,沖刷處理器緩存,讓更新寫入緩存。
9.3 有序性
定義:一個線程對共享變量的更新在其餘線程看起來是按照什麼順序執行的問題
10. synchronized關鍵字
10.1 說說對synchronized關鍵字的理解
答:synchronized是Java的一個關鍵字,是一個內部鎖,保證其修飾的方法或代碼塊在任意時刻只有一個線程執行。解決的是多線程間訪問資源的同步性。優化方法是縮短獲取鎖的時間。
10.2 底層原理
答:底層原理屬於JVM層面。
修飾同步語句塊:
- 進入時,執行monitorenter,將計數器+1,釋放鎖monitorexit時,計數器-1
- 當一個線程判斷到計數器爲0時,則當前鎖空閒,可以佔用;反之,當前線程進入等待狀態
修飾方法:
使用ACC_SYNCHRONIZED標識指明方法是一個同步方法,JVM從而執行相應的同步調用。
10.3 JVM資源調度
答:分爲公平調度和非公平調度。
- 公平調度:按照申請的先後順序授予資源的獨佔權。缺點是吞吐率小。
- 非公平調度:新來的線程能先被授予資源的獨佔權。缺點是會產生飢餓現象。
10.4 synchronized和ReentrantLock的區別
答:總結爲:
- 二者都是可重入鎖。可重入鎖就是自己可以再次獲取自己的內部鎖。同一線程每次獲取鎖,鎖的計數器++,計數器爲0時再釋放鎖。
- synchronized依賴於JVM,ReentrantLock依賴於API。
- ReentrantLock比synchronized增加了一些高級功能。
- 等待可中斷。即正在等待的線程可以選擇放棄等待,執行其他任務。
- ReentrantLock支持公平和非公平調度。synchronized只支持非公平鎖。
- 支持選擇性通知。synchronized相當於整個Lock只有一個Condition,所有線程都註冊在一個上面,notifyAll()通知所有等待狀態線程,效率不高。ReentrantLock的線程對象能註冊在指定的Condition中,signalAll()只會喚醒該Condition實例中的等待線程。
11. 鎖優化
答:鎖優化主要是JVM對synchronized的優化。
11.1 自旋鎖
主要思想是讓一個線程在請求一個共享數據鎖時忙循環(自旋)一段時間,若這段時間內能獲得鎖,則避免進入阻塞狀態。缺點是自旋操作佔用CPU時間。
總結:請求鎖時先忙循環
11.2 鎖粗化
若JVM探測到一串操作都對同一個對象加鎖,就會把加鎖範圍擴展到整個操作的外部(粗化),以避免頻繁加鎖引起性能損耗。
11.3 偏向鎖
主要思想是先來就是你的,有競爭就釋放鎖。
- 第一個獲取鎖對象的線程,在之後獲得該鎖就不再進行同步操作。
- 當鎖對象第一次被線程獲得的時候,進入偏向狀態,標記爲 1。
- 使用 CAS 操作將線程 ID 記錄到 Mark Word 中,如果 CAS 操作成功,這個線程以後每次進入這個鎖相關的同步塊就不需要再進行任何同步操作。
- 當有另外一個線程去嘗試獲取這個鎖對象時,偏向狀態就宣告結束,此時撤銷偏向(Revoke Bias)後恢復到未鎖定狀態或者輕量級鎖狀態。
12. Volatile關鍵字
答:Volatile關鍵字的主要作用就是保證變量的可見性和有序性,不能保證原子性。每次把變量寫入/讀取到主存中。
12.1 synchronized和volatile的區別
答:比較總結如下:
- volatile關鍵字是輕量級的鎖,性能比synchronized好;
- volatile只能修飾變量,synchronized能修飾方法和代碼塊;
- volatile保證數據可見性和有序性,不保證原子性;synchronized都保證。
12.2 實現原理
答:
- 生成彙編代碼時,Lock前綴會將處理器緩存寫回內存;
- 寫回內存會使其他CPU的緩存失效;
- CPU發現本地緩存失效時,會從內存重讀該變量數據,從而實現獲得新值。
12.3 volatile在什麼情況下能代替鎖
答:volatile是輕量級的鎖,適合多個線程共享一個或一組狀態變量,來替代鎖。
12.4 JMM內存屏障
答:JMM通過在適當位置插入內存屏障阻止重排序。
volatile寫:在前和後插入屏障(先禁上面普通寫,再禁止下面可能的volatile讀寫);
volatile讀:在後插入兩個屏障(禁止普通讀寫+volatile讀)。
- StoreStore屏障:禁止上面的普通寫和下面的volatile寫重排序;
- StoreLoad屏障:禁止上面的volatile寫和下面的volatile讀/寫重排序;
- LoadLoad屏障:禁止上面的volatile讀重排序和下面的普通讀操作;
- LoadStore屏障:禁止上面的volatile讀重排序和下面的普通寫操作。
13. ThreadLocal
答:ThreadLocal變量,爲每個使用該變量的線程提供獨立的變量本地副本,通過get/set方法獨立獲取/改變自己的副本值,避免線程安全問題。(理解爲100個學生給了100只筆,互不影響)
13.1 內部實現機制
答:總結爲。
- 每個線程內都有一個類HashMap的對象,稱爲ThreadLocalMap,存放以ThreadLocal爲key的鍵值對,get/set/remove基於此實現。
- key是弱引用,value是強引用,在gc時value不會被清理,長期不清楚會造成內存泄漏。
13.2 內存泄漏解決方案
答:源碼中已經有了改進。
- 針對key爲null的entry,先查有沒有哈希衝突,沒有就調用cleanSomeSlots檢測清楚髒數據;有就向後環形查找,過程中有髒數據就replaceStaleEntry。
13.3 爲什麼使用弱引用
答:如果使用強引用,業務處理置null,但threadlocalmap的鍵值對依舊強引用threadlocal,始終可達,JVM不會自動gc。弱引用即使會出現內存泄漏問題,但在生命週期內只要保證對髒數據處理,就能保證安全。
13.4 怎麼用
答:使用完ThreadLocal後,必須手動調用remove方法,進行清理,因爲官方在set/get/remove方法中內置了清理key爲null的記錄。(跟用鎖一樣,加鎖完要解鎖)
14. 線程池
14.1 爲什麼要用線程池
答:池化思想有利於降低資源銷燬,節省創建資源的時間,提高線程的可管理性。
14.2 execute()和submit()區別
答:總結如下:
- execute()用於提交不需要返回值的任務,所以無法判斷任務是否被線程池執行成功;
- submit()用於提交需要返回值的任務,線程池返回Future類型對象,通過get()獲得返回值。
14.3 線程池參數
答:使用ThreadPoolExecutor創建線程池,客戶端調用submit(Runnable task)提交任務。
/**
* 用給定的初始參數創建一個新的ThreadPoolExecutor。
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
具體參數爲:
- corePoolSize:核心線程數,即最小可同時運行的線程數量
- maximumPoolSize:最大線程數,即最大可同時運行的線程數量
- keepAliveTime :線程空閒但是保持不被回收的時間
- unit:時間單位
- workQueue:阻塞隊列,存儲線程的隊列
- threadFactory:創建線程的工廠
- handler:拒絕策略
推薦配置:
- corePoolSize: 核心線程數爲 5。
- maximumPoolSize :最大線程數 10
- keepAliveTime : 等待時間爲 1L。
- unit: 等待時間的單位爲 TimeUnit.SECONDS。
- workQueue:任務隊列爲 ArrayBlockingQueue,並且容量爲 100;
- handler:拒絕策略爲 CallerRunsPolicy。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExecutorDemo {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;
public static void main(String[] args) {
//使用阿里巴巴推薦的創建線程池的方式
//通過ThreadPoolExecutor構造函數自定義參數創建
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 10; i++) {
//創建WorkerThread對象(WorkerThread類實現了Runnable 接口)
Runnable worker = new MyRunnable("" + i);
//執行Runnable
executor.execute(worker);
}
//終止線程池
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
14.4 排隊策略
答:向線程池提交任務時,需要遵循排隊策略。
- 若運行的線程 < corePoolSize,Executor首選添加線程,不排隊;
- 若運行的線程 >= corePoolSize,且隊列未滿,Executor首選將請求加入隊列,不加新線程;
- 若隊列已滿,創建新線程,若超出maximumPoolSize ,拒絕此任務。
14.5 拒絕策略
答:線程達到max,隊列也放滿時,使用拒絕策略。
- AbortPolicy:中斷,拋出異常 RejectedExecutionException來拒絕新任務的處理。
- CallerRunsPolicy:調用自己所在線程運行任務。會降低對於新任務提交速度,影響程序的整體性能。
- DiscardPolicy:直接丟棄,不處理。
- DiscardOldestPolicy:捨棄最舊任務,丟棄最早的未處理的任務請求。
14.6 常見線程池類型
答:四種。
- newCachedThreadPool():可緩存線程池,核心線程池大小爲0,最大線程池大小無限,來一個創建一個線程。
- newFixedThreadPool():固定大小的線程池。
- newScheduledThreadPool:定時線程池,週期執行或者定時執行。
- newSingleThreadExecutor():單線程化的線程池,保證所有任務按指定順序執行,如FIFO、LRU。不會發生併發執行。
14.7 常見阻塞隊列
答:三種。
- ArrayBlockingQueue:基於預先分配的數組實現的有界阻塞隊列。
優點:put 和 take操作不會增加GC的負擔(因爲空間是預先分配的);
缺點:put 和 take操作使用同一個鎖,可能導致鎖爭用,導致較多的上下文切換。
適合在生產者線程和消費者線程之間的併發程序較低的情況下使用。
- LinkedBlockingQueue:
基於鏈表實現的是無界阻塞隊列。(上限是Integer.MAX_VALUE)
優點:put 和 take 操作使用兩個顯式鎖(putLock和takeLock)
缺點:增加GC的負擔,因爲空間是動態分配的。
適合在生產者線程和消費者線程之間的併發程序較高的情況下使用。
- SynchronousQueue:
不存儲元素的有界阻塞隊列。
生產者線程生產一個產品之後,會等待消費者線程來取走這個產品,纔會接着生產下一個產品。(put必須有take)
適合在生產者線程和消費者線程之間的處理能力相差不大的情況下使用。
15. Atomic原子類
答:簡單來說,原子類就是具有原子操作特徵的類,即這個類中的操作不可中斷。原子類都方法JUC(java.util.concurrent)併發包.atomic下。
15.1 CAS
答:atomic包下都是採用CAS的樂觀鎖實現。樂觀鎖即假設所有線程訪問資源不會出現衝突阻塞情況,如果出現使用CAS處理。
15.2 CAS過程
答:總結一下就是,拿期望值和原本值比較,相同就更新爲新值。
- CAS包含V:內存中的實際值,O:預期值,N:新值。
- V=O,說明值沒有更改,O爲當前最新值,可以進行N賦值給V。
- V!=O,說明值已經改變,O不爲當前最新值,不能賦值,直接返回V。
15.3 CAS問題
15.3.1 ABA
如果一個值初次讀取爲A,而後被改成B,後來又被改回A,那CAS操作會誤認爲其從未改變過。解決方案是增加變量值的版本號以保證CAS的正確性。
15.3.2 自旋時間過長
CAS是非阻塞同步,不會掛起線程,而是自旋一段時間進行嘗試。自旋時間過長會對性能造成很大的消耗。
15.4 synchronized和CAS的區別
答:區別在於Synchronized是互斥同步,存在線程阻塞和喚醒鎖的性能問題。CAS是非阻塞同步,進行自旋後嘗試。
15.5 以AtomicInteger爲例
常用方法有:
public final int get() //獲取當前的值
public final int getAndSet(int newValue)//獲取當前的值,並設置新的值
public final int getAndIncrement()//獲取當前的值,並自增
public final int getAndDecrement() //獲取當前的值,並自減
public final int getAndAdd(int delta) //獲取當前的值,並加上預期的值
boolean compareAndSet(int expect, int update) //如果輸入的數值等於預期值,則以原子方式將該值設置爲輸入值(update)
public final void lazySet(int newValue)//最終設置爲newValue,使用 lazySet 設置之後可能導致其他線程在之後的一小段時間內還是可以讀到舊的值。
16. AQS
16.1 AQS介紹
答:AQS(AbstractQueuedSynchronizer)是用來構建鎖和同步器的框架。
16.2 對原理的理解
答:AQS的核心思想就是,若被請求的共享資源空閒,則將當前請求資源的線程設置爲有效的工作線程,資源分配後將共享資源設爲鎖定狀態。若被請求的共享資源被佔用,則需要一套線程阻塞等待和喚醒鎖分配的機制,這個機制AQS通過CLH隊列實現,即將暫時獲取不到鎖的線程封裝爲一個結點加入到隊列中。
CLH隊列:是一個虛擬雙向隊列,僅存在結點間的關聯聯繫。
內部使用int變量的state標識同步狀態,FIFO的排隊策略,CAS實現值的修改。
16.3 資源共享方式
答:兩種。
- Exclusive(獨佔):只有一個線程能執行,如ReentrantLock。又可分爲公平鎖和非公平鎖:
公平鎖:按照線程在隊列中的排隊順序,先到者先拿到鎖
非公平鎖:當線程要獲取鎖時,無視隊列順序直接去搶鎖,誰搶到就是誰的 - Share(共享):多個線程可同時執行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock等等。
16.4 組件總結
- Semaphore(信號量):允許多個線程同時訪問某個資源,synchronized 和 ReentrantLock 都是一次只允許一個線程訪問某個資源,Semaphore(信號量)可以指定多個線程同時訪問某個資源。
- CountDownLatch:倒計時器協調器,用來實現線程等待其餘線程完成某一特定操作後,再開始執行。內部維護一個計數器,爲0則喚醒等待線程,不爲0則暫停。
CyclicBarrier:循環柵欄,可以讓一組線程到達一個同步點時被柵欄阻塞,直到最後一個線程到達柵欄時,所有被攔截的線程再繼續執行。內部維護一個計數器 = 參與方個數,每個線程到達同步點調用await()方法使count-1,當判斷到是最後一個參與方時,調用singalAll喚醒所有線程。到地一攔,人齊再走。