Java多執行緒volatile、ThreadLocal、執行緒池、atomic
volatile
CPU Cache 快取的是記憶體資料用於解決 CPU 處理速度和記憶體不匹配的問題,記憶體快取的是硬碟資料用於解決硬碟訪問速度過慢的問題。
volatile
指示 JVM ,這個變數是共享不穩定的,每次使用都從記憶體中讀取。
併發:
原子性(synchronized
)、可見性(volatile
,修改後其他執行緒可以立刻看到最新值)、有序性(volatile
禁止指令重排序)。
synchronized
關鍵字和 volatile
關鍵字是兩個互補的存在,而不是對立的存在!
volatile
關鍵字是執行緒同步的輕量級實現,所以volatile
synchronized
關鍵字要好 。但是volatile
關鍵字只能用於變數而synchronized
關鍵字可以修飾方法以及程式碼塊 。volatile
關鍵字能保證資料的可見性,但不能保證資料的原子性。synchronized
關鍵字兩者都能保證。volatile
關鍵字主要用於解決變數在多個執行緒之間的可見性,而synchronized
關鍵字解決的是多個執行緒之間訪問資源的同步性。
ThreadLocal
每個執行緒的專屬本地變數,每個執行緒繫結自己的值。
每個Thread
中都具備一個ThreadLocalMap
,而ThreadLocalMap
可以儲存以ThreadLocal
如例:
package ThreadLocal; import java.text.SimpleDateFormat; import java.util.Random; public class ThreadLocalExample implements Runnable{ private static final ThreadLocal<simpledateformat> formatter = ThreadLocal.withInitial(()->new SimpleDateFormat("yyyyMMdd HHmm")); public static void main(String[] args) throws InterruptedException { ThreadLocalExample obj = new ThreadLocalExample(); for(int i=0;i<10;i++) { Thread t = new Thread(obj,""+i); Thread.sleep(new Random().nextInt(1000)); t.start(); } } @Override public void run() { System.out.println("Thread name="+Thread.currentThread().getName()+" default formatter = "+formatter.get().toPattern()); try { Thread.sleep(new Random().nextInt(1000)); } catch (InterruptedException e) { e.printStackTrace(); } formatter.set(new SimpleDateFormat()); System.out.println("Thread name="+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern()); } }
ThreadLocalMap
中使用的 key 為 ThreadLocal
的弱引用,而 value 是強引用。所以,如果 ThreadLocal
沒有被外部強引用的情況下,在垃圾回收的時候,key 會被清理掉,而 value 不會被清理掉。這樣一來,ThreadLocalMap
中就會出現 key 為 null 的 Entry。假如我們不做任何措施的話,value 永遠無法被 GC 回收,這個時候就可能會產生記憶體洩露。ThreadLocalMap
實現中已經考慮了這種情況,在呼叫 set()
、get()
、remove()
方法的時候,會清理掉 key 為 null
的記錄。使用完 ThreadLocal
方法後,最好手動呼叫remove()
方法。
弱引用
Java 中存在四種引用,它們由強到弱依次是:強引用、軟引用、弱引用、虛引用。下面我們簡單介紹下除弱引用外的其他三種引用:
- 強引用(Strong Reference):通常我們通過new來建立一個新物件時返回的引用就是一個強引用,若一個物件通過一系列強引用可到達,它就是強可達的(strongly reachable),那麼它就不被回收。
- 軟引用(Soft Reference):軟引用和弱引用的區別在於,若一個物件是弱引用可達,無論當前記憶體是否充足它都會被回收,而軟引用可達的物件在記憶體不充足時才會被回收,因此軟引用要比弱引用“強”一些。
- 弱引用物件的存在不會阻止它所指向的物件變被垃圾回收器回收。弱引用最常見的用途是實現規範對映(canonicalizing mappings,比如雜湊表)。假設垃圾收集器在某個時間點決定一個物件是弱可達的(weakly reachable)(也就是說當前指向它的全都是弱引用),這時垃圾收集器會清除所有指向該物件的弱引用,然後垃圾收集器會把這個弱可達物件標記為可終結(finalizable)的,這樣它們隨後就會被回收。與此同時或稍後,垃圾收集器會把那些剛清除的弱引用放入建立弱引用物件時所登記到的引用佇列(Reference Queue)中。
- 虛引用(Phantom Reference):虛引用是Java中最弱的引用,那麼它弱到什麼程度呢?它是如此脆弱以至於我們通過虛引用甚至無法獲取到被引用的物件,虛引用存在的唯一作用就是當它指向的物件被回收後,虛引用本身會被加入到引用佇列中,用作記錄它指向的物件已被銷燬。
執行緒池
uuid
: UUID 含義是通用唯一識別碼 (Universally Unique Identifier),這是一個軟體建構的標準。UUID由以下幾部分的組合:
當前日期和時間,UUID的第一個部分與時間有關,如果你在生成一個UUID之後,過幾秒又生成一個UUID,則第一個部分不同,其餘相同。
時鐘序列。
全域性唯一的IEEE機器識別號,如果有網絡卡,從網絡卡MAC地址獲得,沒有網絡卡以其他方式獲得。
UUID 的唯一缺陷在於生成的結果串會比較長。關於UUID這個標準使用最普遍的是微軟的GUID(Globals Unique Identifiers)。標準的UUID格式為:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12)。
例子:
public class ThreadPoolExample {
//建立一個不限制執行緒個數的執行緒池
// private static ExecutorService exec = Executors.newCachedThreadPool(new ThreadFactory() {
// @Override
// public Thread newThread(Runnable r) {
// Thread t = new Thread(r);
// t.setName("worker-thread-"+ UUID.randomUUID().toString());
// return t;
// }
// });
// lambda形式
private static final ExecutorService exec2 = Executors.newCachedThreadPool(r -> {
Thread t = new Thread(r);
t.setName("worker-thread-"+ UUID.randomUUID().toString());
return t;
});
private static final ExecutorService exec = Executors.newFixedThreadPool(5,r -> {
Thread t = new Thread(r);
t.setName("worker-thread-"+ UUID.randomUUID().toString());
return t;
});
public static void main(String[] args) {
for(int i=0;i<10;i++) {
exec.submit(()->{
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
}
CachedThreadPool
主要被應用在響應時間要求高、資料量可控的場景,由於其不限制建立執行緒的個數,故若資料量不可控,會造成程式 OOM。
FixedThreadPool 主要被應用線上程資源有限,資料量較小或不可控場景。
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<runnable>(),
threadFactory);
}
// ThreadPoolExecutor 建構函式
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime, // keepAliveTime:超過corePoolSize數的空閒執行緒在被銷燬之前等待新任務到達的最長時間
TimeUnit unit,
BlockingQueue<runnable> workQueue, // 預設無限大
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
ThreadPoolExecutor
3 個最重要的引數:
corePoolSize
: 核心執行緒數執行緒數定義了最小可以同時執行的執行緒數量。maximumPoolSize
: 當佇列中存放的任務達到佇列容量的時候,當前可以同時執行的執行緒數量變為最大執行緒數。workQueue
: 當新任務來的時候會先判斷當前執行的執行緒數量是否達到核心執行緒數,如果達到的話,新任務就會被存放在佇列中。
ThreadPoolExecutor
其他常見引數:
keepAliveTime
: 當執行緒池中的執行緒數量大於corePoolSize
的時候,如果這時沒有新的任務提交,核心執行緒外的執行緒不會立即銷燬,而是會等待,直到等待的時間超過了keepAliveTime
才會被回收銷燬;unit
:keepAliveTime
引數的時間單位。threadFactory
: executor 建立新執行緒的時候會用到。handler
: 飽和策略。
任務佇列預設無限大,在我們處理的資料量較大或者併發量很大時,應避免直接使用 Executors
提供的 FixedThreadPool
。
但是由於 ThreadPoolExecutor 在等待佇列滿時,會拒絕任務插入並直接丟棄,所以針對於不可以丟棄的任務,就不能簡單的採用這種方式。可以變更拒絕策略。最簡單的方式就是通過增加一個拒絕策略,該策略中做的便是對等待佇列進行阻塞寫入,也就實現了執行緒池提交任務的阻塞等待。
public class ThreadLocalExample{
private static ExecutorService exec = new ThreadPoolExecutor(10,10,0L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),r -> {
Thread t = new Thread(r);
t.setName("worker-thread-" + UUID.randomUUID().toString());
return t;
},(r,executor)->{
if(!executor.isShutdown()) {
try {
//阻塞等待put操作
System.err.println("waiting queue is full, putting...");
executor.getQueue().put(r);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
private static AtomicInteger at = new AtomicInteger(0);
public static void main(String[] args) {
while (true) {
exec.submit(() -> {
System.err.println("Worker" + at.getAndIncrement() + " start.");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.err.println("Worker end.");
});
}
}
}
Atomic
原子類
JUC (java.util.concurrency) 4大原子類:
基本型別
使用原子的方式更新基本型別
AtomicInteger
:整形原子類AtomicLong
:長整型原子類AtomicBoolean
:布林型原子類
陣列型別
使用原子的方式更新數組裡的某個元素
AtomicIntegerArray
:整形陣列原子類AtomicLongArray
:長整形陣列原子類AtomicReferenceArray
:引用型別陣列原子類
引用型別
AtomicReference
:引用型別原子類AtomicStampedReference
:原子更新帶有版本號的引用型別。該類將整數值與引用關聯起來,可用於解決原子的更新資料和資料的版本號,可以解決使用 CAS 進行原子更新時可能出現的 ABA 問題。AtomicMarkableReference
:原子更新帶有標記位的引用型別
物件的屬性修改型別
AtomicIntegerFieldUpdater
:原子更新整形欄位的更新器AtomicLongFieldUpdater
:原子更新長整形欄位的更新器AtomicReferenceFieldUpdater
:原子更新引用型別欄位的更新器
AtomicInteger的本質:自旋鎖+CAS原子操作
樂觀鎖,效能較強,利用CPU自身的特性保證原子性,即CPU的指令集封裝compare and swap兩個操作為一個指令來保證原子性。適合讀多寫少模式。
但是缺點明顯:自旋,消耗 CPU 效能,所以寫的操作較多推薦sync
。僅適合簡單的運算,否則會產生ABA
問題,自旋的時候,別的執行緒可能更改 value
,然後又改回來,此時需要加版本號解決,JDK提供了AtomicStampedReference
和 AtomicMarkableReference
解決ABA
問題,提供基本資料型別和引用資料型別版本號支援。
AQS
全稱為AbstractQueuedSynchronizer,抽象佇列同步器。
ReentrantLock這種東西只是一個外層的API,核心中的鎖機制實現都是依賴AQS元件的。它包含了state變數、加鎖執行緒、等待佇列等併發中的核心元件。
參考
本文同步釋出於orzlinux.cn