前言
題目彙總來源 史上最全各類面試題彙總,沒有之一,不接受反駁
- 面試題彙總一 Java 語言基礎篇
- 面試題彙總二 Java 多線程篇
- 面試題彙總三 Java 集合篇
- 面試題彙總四 JVM 篇
- 面試題彙總五 Spring 篇
- 面試題彙總六 數據庫篇
- 面試題彙總七 計算機網絡篇
目錄
爲什麼wait, notify 和 notifyAll這些方法不在thread類裏面?
談談對Synchronized關鍵字,類鎖,方法鎖,重入鎖的理解
static synchronized 方法的多線程訪問和作用
同一個類裏面兩個synchronized方法,兩個線程同時訪問的問題
ReentrantLock 、synchronized和volatile比較
ThreadLocal、synchronized 和 volatile 關鍵字的區別
你如何確保main()方法所在的線程是Java程序最後結束的線程?
在Java中CycliBarriar和CountDownLatch有什麼區別?
CopyOnWriteArrayList可以用於什麼應用場景?
Java線程池中submit() 和 execute()方法有什麼區別?
Java中interrupted 和 isInterrupted方法的區別?
Java中ConcurrentHashMap的併發度是什麼?
什麼是Java Timer類?如何創建一個有特定時間間隔的任務?
多線程
開啓線程的三種方式?
- 繼承 Thread,覆蓋 run();
- Thread 構造器傳入 Runnable;
- Thread 構造器傳入用 FutureTask 包裝好的 Callable(FutureTask繼承了 Runnable 接口),可以從FutureTask中獲得返回值。
關於 FutureTask 的知識:
Java併發編程:Callable、Future和FutureTask
什麼是Callable和Future?
相關鏈接見上一題。
Callable
一個有一個帶有返回參數方法的接口。
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
Future
Future
接口表示異步計算的結果
public interface Future<V> {
// 取消線程,參數表示是否中斷線程
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
// 獲取結果,如果這個計算任務還沒有執行結束,該調用線程會進入阻塞狀態
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
說說進程,線程,協程之間的區別
- 進程是系統進行資源分配和調度的基本單位;
- 線程是程序執行流的最小單元;
- 線程切換由系統控制,而協程切換由協程主動讓渡。
進程持有哪些資源
1、虛擬地址空間
2、一個全局唯一的進程ID (PID)
3、一個可執行映像(image),也就是該進程的程序文件在內存中的表示
4、一個或多個線程
5、一個位於內核空間中的名爲EPROCESS(executive process block,進程執行塊)的數據結構,用以記錄該進程的關鍵信息,包括進程的創建時間、映像文件名稱等。
6、一個位於內核空間中的對象句柄表,用以記錄和索引該進程所創建/打開的內核對象。操作系統根據該表格將用戶模式下的句柄翻譯爲指向內核對象的指針。
7、一個位於描述內存目錄表起始位置的基地址,簡稱頁目錄基地址(DirBase),當CPU切換到該進程/任務時,會將該地址加載到CR3寄存器,這樣當前進程的虛擬地址纔會被翻譯爲正確的物理地址。
8、一個位於用戶空間中的進程環境塊(Process Environment Block, PEB)。
9、一個訪問權限令牌(access token),用於表示該進程的用戶、安全組,以及優先級別。
線程共享進程哪些資源
線程共享的資源包括:
- 進程代碼段;
- 進程的公有數據;
- 進程的所擁有資源;
線程獨立的資源包括:
- 線程ID;
- 寄存器組的值;
- 線程的堆棧;
- 錯誤返回碼;
- 線程優先級;
進程切換與線程切換
進程切換步驟:
- 切換虛擬地址,即切換頁目錄以使用新的地址空間;
- 切換內核棧和硬件上下文;
- 隱藏開銷:頁表緩衝TLB清空,導致虛擬地址轉換爲物理地址速度下降。
線程切換隻要切換內核棧和硬件上下文。
JVM和操作系統的線程狀態及對應關係
Java線程狀態:NEW, RUNNABLE(READY, RUNNING), BLOCKED, WAITING, TIMED_WATING, TERMINATED
操作系統線程(進程)狀態:
狀態的對應關係:
虛擬機中的線程狀態,不反應任何操作系統線程狀態。
java的線程和操作系統的線程有什麼關係
對於Sun JDK來說,它的Window版與Linux版都是使用一對一的線程模型實現的,一條Java線程就映射到一條輕量級進程中。
什麼是Daemon線程?它有什麼意義?
在java中守護線程和本地線程區別?
在Java中有兩類線程:用戶線程 (User Thread)、守護線程 (Daemon Thread)。
所謂守護 線程,是指在程序運行的時候在後臺提供一種通用服務的線程,比如垃圾回收線程就是一個很稱職的守護者,並且這種線程並不屬於程序中不可或缺的部分。因此,當所有的非守護線程結束時,程序也就終止了,同時會殺死進程中的所有守護線程。反過來說,只要任何非守護線程還在運行,程序就不會終止。
用戶線程和守護線程兩者幾乎沒有區別,唯一的不同之處就在於虛擬機的離開:如果用戶線程已經全部退出運行了,只剩下守護線程存在了,虛擬機也就退出了。 因爲沒有了被守護者,守護線程也就沒有工作可做了,也就沒有繼續運行程序的必要了。
爲什麼要有線程,而不是僅僅用進程?
我的理解:
進程的出現滿足了用戶同時運行多道程序的需求;
若沒有多線程,一個進程中的每個操作也只能順序進行,此時運行耗時操作比如 io,會導致整個進程掛起;
線程的出現可以滿足在一個進程中同時執行多個操作的需求。
什麼是線程組,爲什麼在Java中不推薦使用?
什麼是線程組? java線程組和線程
爲什麼不推薦使用? 2018-04-12-java-爲什麼不推薦使用線程組
- 線程組ThreadGroup對象中比較有用的方法是stop、resume、suspend等方法,由於這幾個方法會導致線程的安全問題(主要是死鎖問題),已經被官方廢棄掉了,所以線程組本身的應用價值就大打折扣了。
- 線程組ThreadGroup不是線程安全的,這在使用過程中獲取的信息並不全是及時有效的,這就降低了它的統計使用價值。
樂觀鎖和悲觀鎖的理解及如何實現,有哪些實現方式?
Java併發問題--樂觀鎖與悲觀鎖以及樂觀鎖的一種實現方式-CAS
悲觀鎖
假設衝突必然會發生,採取一切辦法保證數據一致性。Java 中採用 synchronized 關鍵字保證同步,但多線程環境下頻繁切換上下文會導致性能問題。
樂觀鎖
假設衝突不會發生,只在最後階段進行一致性驗證。兩個步驟:衝突檢測和數據更新。Java 中的一種實現方式是 CAS。
CAS
package sun.misc;
import ...
public final class Unsafe {
...
public final native boolean compareAndSwapObject(
Object o, long offset, Object expect, Object update);
public final native boolean compareAndSwapInt(
Object o, long offset, int expect, int update);
public final native boolean compareAndSwapLong(
Object o, long offset, long expect, long update);
...
}
可以看到,cas 可以操作 object、int 和 long,其中 o 和 offset 用於確定對象 o 的待修改成員變量 field,expect 爲期望值,update 爲更新值。當 field == expect 的時候,field = update 並返回 true;否則不更新,返回 false。
offset 的取值用到了 unsafe 和反射,應該在通常情況下不需要我們直接操作。
stateOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
CAS的應用:java.util.concurrent
volatile int i = 1;
i++;
i++ 並非原子操作,而是包含讀取、+1 和寫入三個步驟,因此在多線程環境下可能得不到期望的結果。可以使用 JUC 包下的 AtomicInteger。
AtomicInteger integer = new AtomicInteger(1);
integer.getAndIncrement();
內部實現:
// -------- AtomicInteger --------
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
// -------- Unsafe --------
public final int getAndAddInt(Object o, long offset, int delta) {
int num;
do {
num = this.getIntVolatile(o, offset);
} while(!this.compareAndSwapInt(o, offset, num, num + delta));
return num;
}
CAS的應用:AtomicStampedReference
ABA 問題:
- 一個變量 int state = 0;
- 線程 A 讀取到 state = 0;
- 線程 B 修改 state = 1 再改回 state = 0;
此時線程 A 再讀取 state,還是0,但實際上已經 state 已經被修改過。
對於對改動敏感的變量操作,可以使用 AtomicStampedReference,它會同時保存時間戳以確認是否發生改動。
// -------- AtomicStampedReference --------
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
Java中用到的線程調度算法是什麼?
JVM使用搶佔的、基於優先權的調度策略。
不過優先級策略不靠譜,因爲 JDK 1.2 之後,Java 使用了本地線程模型,線程調度由操作系統執行,優先級會受操作系統的影響。
同步方法和同步塊,哪個是更好的選擇?
同步代碼塊好。
- 加鎖範圍小於同步方法,因此性能更高;
- 可以任意指定加鎖對象,更靈活。
run()和start()方法區別
調用 run() 只是在當前線程運行方法,而 start() 纔是啓動新線程運行。
// -------- Thread --------
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
@Override
public void run() {
if (target != null) {
target.run();
}
}
Java中Semaphore是什麼?
如何控制某個方法允許併發訪問線程的個數?
使用方法↑
獲取信號量:
// -------- main(String[] args) --------
semaphore.acquire();
// -------- Semaphore --------
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// -------- AbstractQueuedSynchronizer --------
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 如果嘗試結果<0則阻塞
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
// -------- NonfairSync --------
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
// -------- Sync --------
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
// 信號量不足或者成功請求到信號量則返回
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
// 到這裏 tryAcquireShared(arg) 完成
// 接下來阻塞線程 doAcquireSharedInterruptibly(arg)
// -------- AbstractQueuedSynchronizer --------
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
// 自旋,直到請求到信號量退出阻塞狀態
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
釋放信號量:
// -------- main(String[] args) --------
semaphore.release();
// -------- Semaphore --------
public void release() {
sync.releaseShared(1);
}
// -------- AbstractQueuedSynchronizer --------
public final boolean releaseShared(int arg) {
// 嘗試釋放信號量
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
// -------- Sync --------
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
// 這個相比於請求信號量就簡單一些,cas成功就返回
if (compareAndSetState(current, next))
return true;
}
}
在Java中wait和seelp方法的不同
- wait 作用於對象,sleep 作用於 Thread
- wait 釋放對象鎖,sleep 不釋放對象鎖
- wait 運行於同步方法或同步代碼塊,sleep 可運行於任何地方
- wait 不需要捕獲異常,sleep 需要捕獲異常
Thread類中的yield方法有什麼作用?
暫停當前線程,將當前線程從 Running 狀態轉到 Ready 狀態,讓出控制權給其他線程。
什麼是不可變對象,它對寫併發應用有什麼幫助?
對象一旦被創建它的狀態(對象的數據,也即對象屬性值)就不能改變,任何對它的改變都應該產生一個新的對象。
不可變就不用擔心併發問題。
談談 wait / notify 關鍵字的理解
- wait / notify 存在於所有對象;
- 使用時需要 synchronized,否則 IllegalMonitorStateException;
- 調用 wait 方法會讓當前線程阻塞,讓出對象鎖;若 wait 有設置時間參數,到時間後自動喚醒;
- notify 一次喚醒一個等待的線程;notifyAll 一次性喚醒所有該對象上的等待線程。
爲什麼wait, notify 和 notifyAll這些方法不在thread類裏面?
wait,notify,notifyAll,sleep這些方法都跟線程的狀態變化有關,爲什麼jdk把前三個方法放在Object類裏面,而把sleep放在Thread類裏面?
- Java 內置鎖機制中,鎖是對象級而不是線程級,任何對象都能創建鎖;
- 一個線程可以有多個鎖,若跟線程綁定可能會不夠用。
什麼導致線程阻塞?
線程阻塞方法:
- object.wait()
- Thread.sleep()
- Thread.yield()
- thread.join()
- @Deprecated thread.suspend()
還有 synchronized 方法或代碼塊以及 IO 等。
線程之間是如何通信的?
講一下java中的同步的方法
- 關鍵字:synchronized,volatile
- wait() / notify()
- ThreadLocal
- JUC 下的一堆工具
lock原理
與 synchronized 不同,Lock 是基於JDK的一系列類。
public interface Lock {
// 上鎖,獲取不到鎖則阻塞
void lock();
// 上鎖,且若線程阻塞可中斷
void lockInterruptibly() throws InterruptedException;
// 上鎖,獲取不到鎖直接返回false
boolean tryLock();
// 上鎖,在規定時間內獲取不到鎖返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 解鎖
void unlock();
// 獲得新的 Condition,用於精準控制線程休眠與喚醒
Condition newCondition();
}
不像 synchronized,Lock 需要手動解鎖,因此使用時最好用 try ... catch ... finally 包圍,將解鎖寫入 finally。
Lock lock = ...;
lock.lock();
try{
//處理任務
}catch(Exception ex){
}finally{
lock.unlock(); //釋放鎖
}
Condition
Condition 中的 await() 方法相當於 Object 的 wait() 方法,Condition 中的 signal() 方法相當於 Object 的 notify() 方法,Condition 中的 signalAll() 相當於 Object 的 notifyAll() 方法。
Condition 可以進行更精細的線程休眠和喚醒控制。一個 Lock 可以針對不同條件,創建多個 Condition。
什麼是可重入鎖(ReentrantLock)?
ReentrantLock的內部實現
ReentrantLock重入鎖,是實現Lock接口的一個類,也是在實際編程中使用頻率很高的一個鎖,支持重入性,表示能夠對共享資源能夠重複加鎖,即當前線程獲取該鎖再次獲取不會被阻塞。在java關鍵字synchronized隱式支持重入性,synchronized通過獲取自增,釋放自減的方式實現重入。與此同時,ReentrantLock還支持公平鎖和非公平鎖兩種方式。
內部實現
簡易版看這篇:
詳細版看這篇:
談談對Synchronized關鍵字,類鎖,方法鎖,重入鎖的理解
Java的內置鎖
每個java對象都可以用做一個實現同步的鎖,這些鎖成爲內置鎖。線程進入同步代碼塊或方法的時候會自動獲得該鎖,在退出同步代碼塊或方法時會釋放該鎖。獲得內置鎖的唯一途徑就是進入這個鎖的保護的同步代碼塊或方法。若一個線程進入該方法或方法塊,此時鎖被其他線程佔用,則發生阻塞。
對象鎖
若 synchronized 關鍵詞修飾普通方法或方法中的代碼塊,則獲取的是一個對象的對象鎖。對於方法,獲取的是被調用對象的對象鎖,而方法塊可以獲取指定對象的對象鎖。
public class MyClass {
public synchronized void test1() {
// 獲取被調用對象的對象鎖
}
public void test2() {
synchronized (this) {
// 獲取指定對象的對象鎖
}
}
}
類鎖
若 synchronized 關鍵詞修飾 static 方法或方法中的代碼塊,則獲取的是一個類鎖。類鎖與對象鎖執行時不衝突。
public class MyClass {
public synchronized static void test3() {
// 獲取該類的類鎖
}
}
synchronized可重入性
同一線程在調用自己類中其他synchronized方法/塊或調用父類的synchronized方法/塊都不會阻礙該線程的執行,就是說同一線程對同一個對象鎖是可重入的,而且同一個線程可以獲取同一把鎖多次,也就是可以多次重入。
static synchronized 方法的多線程訪問和作用
見上題。對類施加的鎖,針對所有線程執行該類添加 synchronized 關鍵詞的靜態方法或方法塊。
同一個類裏面兩個synchronized方法,兩個線程同時訪問的問題
如果兩個方法都是非 static 或都是 static,其中一個線程會發生阻塞,等待另一個線程方法執行完畢。
如果一個 static 另一個非 static,則能並行執行。
對象鎖和類鎖是否會互相影響?
不會
談談volatile關鍵字的作用
當一個共享變量被volatile修飾時,它會保證修改的值立即被更新到主存
相關概念:
JMM(java內存模型)
如下圖,共享變量放在主內存中,但每個線程操作的都是相應本地內存的副本,因此若某個線程修改了共享變量,可能不能及時更新到主內存,其他線程無法及時讀到最新數據。
將一個共享變量聲明爲volatile後,會有以下效應:
- 當寫一個volatile變量時,JMM會把該線程對應的本地內存中的變量強制刷新到主內存中去;
- 這個寫會操作會導致其他線程中的緩存無效。
複合類操作
如 i++,包含了讀取、+1、寫入三個步驟,在多線程環境下不保證原子性。可改成相應的原子類進行操作。
禁止重排序
如 int a = 1; int b = 2; 在JVM優化時可能調換順序,若使用了 volatile 可屏蔽重排序。
volatile關鍵詞在單例模式的雙重校驗中的作用
public class Singleton {
private Singleton (){} //私有化構造方法
private static volatile Singleton singleton=null;
public static Singleton getInstance(){
//第一次校驗
if(singleton==null){
synchronized(Singleton.class){
//第二次校驗
if(singleton==null){
singleton = new Singleton();
}
}
return singleton;
}
}
singleTon = new Singleton()的執行過程如下:
- 獲取對象地址;
- 在對象地址上初始化一個新的Singleton;
- 將singleton引用指向Singleton對象;
若不加volatile,JVM的指令重排優化可能導致2和3執行順序對調,因此其他線程就會拿到非null的singleton引用,但此時Singleton對象還未初始化完畢。
synchronized與Lock的區別
ReentrantLock 、synchronized和volatile比較
那我自己簡單總結一下:
- volatile 是線程同步輕量級實現,性能較好;解決變量的線程可見性問題;修飾變量;不保證原子性。
- synchronized 相對於 volatile 更重量級;修飾方法和代碼塊;保證原子性。
- ReentrantLock 是JDK中的一個類;需要在方法中手動加減鎖;相對與 synchronized 可以進行更精細操作。
談談ThreadLocal關鍵字的作用
ThreadLocal,線程本地變量,爲變量在每個線程中都創建了一個副本,那麼每個線程可以訪問自己內部的副本變量。
我的理解:
每個 Thread 都帶有一個 ThreadLocalMap,可以理解爲是一個 Map<ThreadLocal, Object>;
ThreadLocal 在內部就是操作所在線程的 ThreadLocalMap;
當創建一個 ThreadLocal 時,就在當前線程的 ThreadLocalMap 中存入 key 爲自身,value 爲副本值的一個鍵值對;
同理,從 ThreadLocal 取出變量時,也是先獲取當前線程的 ThreadLocalMap,再以自身爲鍵值取出相應變量。
ThreadLocal、synchronized 和 volatile 關鍵字的區別
多線程之volatile、ThreadLocal、synchronized關鍵字區別
synchronized 和 volatile 的區別就不再說了,都是爲了實現線程間同步。而 ThreadLocal 是爲了實現每個線程內部的局部變量,和前兩者作用不一樣。
你如何確保main()方法所在的線程是Java程序最後結束的線程?
main 方法裏 join 子線程。
談談NIO的理解
在Java中CycliBarriar和CountDownLatch有什麼區別?
Java中CycliBarriar和CountdownLatch區別(附測試實例)
CycliBarriar
讓一組線程等待至某個狀態之後再全部同時執行。可以重複使用。
CyclicBarrier cb = new CyclicBarrier(3);
若線程1和線程2先後執行 cb.await(),將會被阻塞;直到線程3調用 cb.await(),三個線程將同時開始執行。
CountDownLatch
一個或多個線程,等待另外N個線程完成某個事情之後才能執行。不可重用。
CountDownLatch cdl = new CountDownLatch(3);
若線程1執行了 cdl.await(),將被阻塞;直到線程2和線程3分別執行 cdl.countDown(),將計數置爲0,線程1纔會繼續執行。
CopyOnWriteArrayList可以用於什麼應用場景?
讀寫分離,讀直接讀,寫或改時先複製一個副本,在副本上做修改,再將引用指向該副本。
讀不加鎖,寫要加鎖。
頻繁複制會有性能問題,因此適用於讀多寫少的情況。
多線程中的忙循環是什麼?
網上只找到了這個:
忙循環就是程序員用循環讓一個線程等待,不像傳統方法wait(), sleep() 或 yield() 它們都放棄了CPU控制,而忙循環不會放棄CPU,它就是在運行一個空循環。這麼做的目的是爲了保留CPU緩存,在多核系統中,一個等待線程醒來的時候可 能會在另一個內核運行,這樣會重建緩存。爲了避免重建緩存和減少等待重建的時間就可以使用它了。
怎麼檢測一個線程是否擁有鎖?
死鎖的四個必要條件?
1、互斥:某種資源一次只允許一個進程訪問,即該資源一旦分配給某個進程,其他進程就不能再訪問,直到該進程訪問結束。
2、佔有且等待:一個進程本身佔有資源(一種或多種),同時還有資源未得到滿足,正在等待其他進程釋放該資源。
3、不可搶佔:別人已經佔有了某項資源,你不能因爲自己也需要該資源,就去把別人的資源搶過來。
4、循環等待:存在一個進程鏈,使得每個進程都佔有下一個進程所需的至少一種資源。
什麼是線程池,如何使用?
這篇使用部分可以看一下,源碼部分和我電腦裏的(jdk 1.8)不太一樣。
首先,講講什麼是線程池?照筆者的簡單理解,其實就是一組線程實時處理休眠狀態,等待喚醒執行。那麼爲什麼要有線程池這個東西呢?可以從以下幾個方面來考慮:其一、減少在創建和銷燬線程上所花的時間以及系統資源的開銷 。其二、將當前任務與主線程隔離,能實現和主線程的異步執行,特別是很多可以分開重複執行的任務。但是,一味的開線程也不一定能帶來性能上的,線池休眠也是要佔用一定的內存空間,所以合理的選擇線程池的大小也是有一定的依據。
嘗試分析源碼
對於 ThreadPoolExecutor 的使用,比較關鍵的方法就是 execute() 和 submit(),而 submit() 內部也是調用 execute(),所以着重看一下 execute() 方法。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
// clt 是一個原子整數,代表什麼不重要,
// 知道 workerCountOf(c) 表示當前可以直接用的線程數,
// isRunning(c) 表示線程池當前是否是可用狀態就行
int c = ctl.get();
// 當前線程數 < corePoolSize 則創建新線程執行任務
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 當前線程數 >= corePoolSize,加入等待隊列
if (isRunning(c) && workQueue.offer(command)) {
// 由於是多線程環境,需要反覆確認線程池是不是被停了
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
// 當前線程數爲0,則一定創建一個線程執行任務
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 沒有成功加入等待隊列,嘗試在 maximumPoolSize 的範圍內創建線程,
// 如果創建失敗就說明線程數已經超過最大值。
// 如果等待隊列使用 LinkedBlockingQueue 這類沒有容量限制的隊列時,應該走不到這步
else if (!addWorker(command, false))
reject(command);
}
接下來看看 addWorker() 方法。
private boolean addWorker(Runnable firstTask, boolean core) {
// 參數 core 表示當前容量是參考 corePoolSize 還是 maximumPoolSize
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// 線程池狀態變了,不能創建新線程。具體判定條件沒看懂。
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// workerCount++
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// workerCount 導致的 cas 失敗,重試內部循環
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// Worder 實現了 Runnable 接口
// 所以獲得的線程 t 傳入的參數就是 Worker 自己
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// ...
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
// 到這一步終於開始執行線程了
// 這裏調用的 run() 方法是 Worker 的
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
// 包括 workers.remove(w) 和 workerCount-- 等操作
}
return workerStarted;
}
接下來簡單看看 Worker 的運行,瞭解一下線程池中的線程如何在執行完一個任務的情況下執行另一個。
// -------- Worker --------
public void run() {
runWorker(this);
}
// -------- ThreadPoolExecutor --------
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask; // 這個 firstTask 是 Worker 構造函數的參數
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
// 這個 while 裏的條件是關鍵
// 如果 task 非空直接執行,否則 getTask()
// getTask() 會從等待隊列裏取待執行任務,隊列空則阻塞,阻塞超時則返回空
while (task != null || (task = getTask()) != null) {
w.lock();
// 線程池被停止後要不要中斷線程的一系列判斷和操作
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task); // 方法裏面是空的
Throwable thrown = null;
try {
// 新任務執行
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown); // 方法裏面是空的
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
// 因爲超時等原因,需要結束該線程
// 下面的方法已看不懂了,比如爲什麼 workCount-- 不執行等
processWorkerExit(w, completedAbruptly);
}
}
Java線程池中submit() 和 execute()方法有什麼區別?
submit() 可以獲取返回值,execute() 不行。
四種ExecutorService線程池
newCachedThreadPool 創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,若無可回收,則新建線程。
newFixedThreadPool 創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。
newScheduledThreadPool 創建一個定長線程池,支持定時及週期性任務執行。
newSingleThreadExecutor 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。
核心線程數 | 最大線程數 | 持續時間 | 等待隊列 | 拒絕策略 | |
---|---|---|---|---|---|
CachedThreadPool | 0 | Integer.MAX_VALUE | 60s | SynchronousQueue | AbortPolicy |
FixedThreadPool | n | n | 0s | LinkedBlockQueue | AbortPolicy |
ScheduledThreadPool | n | Integer.MAX_VALUE | 0s | DelayedWorkQueue | AbortPolicy |
SingleThreadExecutor | 1 | 1 | 0s | LinkedBlockQueue | AbortPolicy |
線程池四種拒絕策略
ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。
ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。
ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程)。
ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務。
BlockingQueue介紹
用Java實現阻塞隊列
取操作 | 放操作 | 觀察操作 | |
---|---|---|---|
非阻塞異常 | remove() | add(E e) | |
非阻塞無異常 | poll() | offer(E e) | peek() |
延時 | poll(long timeout, TimeUnit unit) | offer(E e,long timeout, TimeUnit unit) | |
阻塞 | take() | put(E e) |
常見的阻塞隊列:
- ArrayBlockingQueue:基於數組實現的一個阻塞隊列,在創建ArrayBlockingQueue對象時必須制定容量大小。並且可以指定公平性與非公平性,默認情況下爲非公平的,即不保證等待時間最長的隊列最優先能夠訪問隊列。
- LinkedBlockingQueue:基於鏈表實現的一個阻塞隊列,在創建LinkedBlockingQueue對象時如果不指定容量大小,則默認大小爲Integer.MAX_VALUE。
- PriorityBlockingQueue:以上2種隊列都是先進先出隊列,而PriorityBlockingQueue卻不是,它會按照元素的優先級對元素進行排序,按照優先級順序出隊,每次出隊的元素都是優先級最高的元素。注意,此阻塞隊列爲無界阻塞隊列,即容量沒有上限(通過源碼就可以知道,它沒有容器滿的信號標誌),前面2種都是有界隊列。
- DelayQueue:基於PriorityQueue,一種延時阻塞隊列,DelayQueue中的元素只有當其指定的延遲時間到了,才能夠從隊列中獲取到該元素。DelayQueue也是一個無界隊列,因此往隊列中插入數據的操作(生產者)永遠不會被阻塞,而只有獲取數據的操作(消費者)纔會被阻塞。
Java中interrupted 和 isInterrupted方法的區別?
Thread的isInterrupted()和interrupted()的區別
Thread類中interrupt()、interrupted()和isInterrupted()方法詳解
// 針對當前線程,清除中斷位
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
// 針對被調用的線程,不清除中斷位
public boolean isInterrupted() {
return isInterrupted(false);
}
private native boolean isInterrupted(boolean ClearInterrupted);
// 針對被調用線程
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
多線程有什麼要注意的問題?
如何保證多線程讀寫文件的安全?
文件鎖,通過 channel 獲得。
// 阻塞
public final FileLock lock()
public abstract FileLock lock (long position, long size, boolean shared)
// 非阻塞
public final FileLock tryLock()
public abstract FileLock tryLock(long position, long size, boolean shared)
// 無參爲獨佔鎖,shared = true 爲共享鎖。
實現生產者消費者模式
Java中的ReadWriteLock是什麼?
ReadWriteLock 接口
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
實現:ReentrantReadWriteLock
- 寫鎖爲獨佔鎖,讀鎖爲共享鎖
- 讀鎖被獲得後不能獲得寫鎖;寫鎖被獲得後只有同一線程允許獲得讀鎖
用Java寫一個會導致死鎖的程序,你將怎麼解決?
讓線程A和線程B互相等待對方佔用的資源。
final Object obj1 = new Object();
final Object obj2 = new Object();
final BiConsumer consumer = (o1, o2) -> {
synchronized (o1) {
System.out.println(Thread.currentThread().getId() + " get " + o1);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println(Thread.currentThread().getId() + " get " + o2);
}
}
};
final Runnable r1 = () -> consumer.accept(obj1, obj2);
final Runnable r2 = () -> consumer.accept(obj2, obj1);
new Thread(r1).start();
new Thread(r2).start();
SimpleDateFormat是線程安全的嗎?
多個線程同時調用 parse 會在方法內部互相干擾,最好每個線程單獨使用 SimpleDateFormat。
Java中的同步集合與併發集合有什麼區別?
同步集合類
- Hashtable
- Vector
- 同步集合包裝類,Collections.synchronizedMap()和Collections.synchronizedList()
併發集合類
- ConcurrentHashMap
- CopyOnWriteArrayList
- CopyOnWriteHashSet
Java中ConcurrentHashMap的併發度是什麼?
什麼是Java Timer類?如何創建一個有特定時間間隔的任務?