目錄
- 第一章 并發編程執行緒基礎
- 什么是執行緒
- 執行緒的創建與運行
- 執行緒的等待與通知
- 執行緒中的方法
- 執行緒背景關系切換
- 執行緒死鎖
- 守護執行緒與用戶執行緒
- ThreadLocal
- InheritableThreadLocal
- 第二章 并發編程的其他基礎知識
- 什么是多執行緒并發編程
- Java 中的執行緒安全問題
- Java 中共享變數的記憶體可見性
- Java 中的 synchronized 關鍵字
- Java 中的 volatile 關鍵字
- Java 中的 CAS 操作
- Unsafe 類
- 指令重排序
- 偽共享
- 鎖的概述
閱讀 《Java并發編程之美》 - 翟陸續 (作者) 的筆記
第一章 并發編程執行緒基礎
什么是執行緒
行程和執行緒的關系:
- 執行緒是行程中的一個物體,執行緒本身是不會獨立存在的
- 行程是系統資源分配和調度的基本單位
- 一個行程中至少有一個執行緒,行程中的多個執行緒共享行程的資源
- CPU資源比較特殊,是分配到執行緒的
Java記憶體區域:一個行程中有多個執行緒,執行緒共享行程的堆和方法區,但執行緒有自己的程式計數器、虛擬機堆疊、本地方法堆疊

圖片來源:Java記憶體區域(運行時資料區域)詳解、JDK1.8與JDK1.7的區別 - 傑0327
執行緒的創建與運行
創建執行緒的三種方式:
- 繼承 Thread 類
- 實作 Runnable 介面
- 使用 FutureTask 類
直接繼承 Thread 并重寫 run() 方法:
public class MyThread extends Thread{
@Override
public void run() {
// code...
}
}
new MyThread().start();
實作 Runnable 介面:
public class Task implements Runnable{
@Override
public void run() {
// code...
}
}
new Thread(new Task()).start();
實作 Callable 介面,可以有回傳值:
public class Task implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return null;
}
}
FutureTask<Integer> futureTask = new FutureTask<>(new Task());
new Thread(futureTask).start();
futureTask.get();
總結:使用繼承 Thread 的方式并沒有將任務邏輯和執行緒機制分離,每次執行任務時都需要創建一個執行緒,而使用 Runnable 或 Callable 介面,可以使用一個執行緒執行多個任務,(最直接的方式就是將任務提交到執行緒池)
執行緒的等待與通知
// 阻塞執行緒的方法,直到:
// * 其他執行緒呼叫了 notify() 或 notifyAll()
// * 執行緒被中斷,則拋出中斷例外
// * 執行緒超時回傳
// * 虛假喚醒
obj.wait();
obj.wait(5000);
obj.notify();
obj.notifyAll();
呼叫以上方法需要獲取物件的監視器鎖,有兩種方式可以獲得物件的監視器鎖:
// 獲取物件本身的監視器鎖
public synchronized void method(){
while (!condition)
wait();
}
// 獲取 obj 物件的監視器鎖
synchronized(obj){
while (!condition)
obj.wait();
}
- 獲取監視器鎖后先檢查條件是否滿足,否則呼叫 wait() 將執行緒掛起并釋放鎖(使用 while回圈 是為了避免虛假喚醒)
- 當其他執行緒喚醒正在等待的執行緒時,被喚醒的執行緒會先競爭鎖,得到鎖的執行緒會從 wait() 方法回傳并再次檢查條件
執行緒中的方法
thread.join(); // 掛起呼叫執行緒,直到目標執行緒執行結束
Thread.sleep(1000); // 在指定時間內不參與CPU調度(靜態方法)
Thread.yield(); // 提示CPU執行緒希望提前廢棄CPU時間(靜態方法)
執行緒中斷:
thread.interrupt(); // 中斷執行緒物件
thread.isInterrupted(); // 判斷執行緒物件是中斷
Thread.interrupted(); // 判斷呼叫執行緒是否被中斷并清除中斷標志(靜態方法)
中斷執行緒只是將標志置位,具體如何回應取決于執行緒自身(可能拋出例外或繼續執行)
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock(); // 等待執行緒獲得鎖再拋出中斷例外
reentrantLock.lockInterruptibly(); // 立即拋出執行緒中斷
執行緒背景關系切換
系統的調度方式有兩種:搶占式和非搶占式
在搶占式的系統中,執行緒通過時間片輪詢的方式占用CPU,當時間片用完時讓出CPU,執行執行緒背景關系切換,
這種調度方式決定了當執行CPU密集型任務時,最多只能啟動和CPU數同等的執行緒數;而執行IO密集型任務時,一般可以啟動更多的執行緒,
執行緒死鎖
死鎖是指兩個或兩個以上的執行緒在執行程序中,因爭奪資源而造成的相互等待的現象,在無外力的作用下,這些執行緒會一直等待而無法繼續運行下去,
產生死鎖的四個必備條件:
- 互斥條件:資源只能被一個執行緒持有(具有排他性)
- 請求并持有:執行緒已持有資源,并請求其他被其他執行緒持有的資源
- 不可剝奪條件:被持有的資源只有持有它的執行緒可以釋放
- 環路等待條件:對資源的請求形成一個環形鏈
解決死鎖的方式:
- 資源申請的有序性原則
- (InnoDB)請求超時、圖演算法
守護執行緒與用戶執行緒
JVM 行程在用戶執行緒都結束后退出,而不管是否在有守護執行緒在執行,
Thread thread = new Thread();
thread.setDaemon(true);
thread.start();
ThreadLocal
下面代碼每個執行緒都會擁有自己的SimpleDateFormat物件:
// 從SimpleDateFormat 的注釋可以得知,SimpleDateFormat 不是執行緒安全的
private ThreadLocal<SimpleDateFormat> DateFormatContext = ThreadLocal.withInitial(()->{
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
});
ThreadLocal 實作原理:

圖片來源:ThreadLocal explained - DuyHai
Thread.set() 方法:
// 獲取當前執行緒的 threadLocals 變數并將鍵值對(theadLocal, value)放到里面
// 并且 threadLocals 是懶加載的
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
執行緒本地的變數:TheadLocalMap 是一個執行緒本地的散串列
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
懶加載的 threadLocals:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
ThreadLocal.get() 方法:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 以 TheadLocal 物件為鍵,從執行緒本地的散串列取值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
如果 Map 或對應的鍵還未初始化,則會回傳初始值:
private T setInitialValue() {
T value = https://www.cnblogs.com/unsafe01/p/initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
初始值默認是 null:上面的示例則將初始值設定為 SimpleDateFormat 物件
protected T initialValue() {
return null;
}
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
// 所以 supplier 本質是一個工廠,而 ThreadLocalMap 就是一個執行緒本地的容器
// supplier.get() 會回傳新創建的物件
@Override
protected T initialValue() {
return supplier.get();
}
}
ThreadLocal.remove():當本地物件不使用時要將其移除,防止記憶體溢位
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocal 總結:
- ThreadLocal 其實就是一個工具殼,它會操作執行緒本地的散串列 threadLocals,散串列以 ThreadLocal 物件為鍵
- 執行緒本地的 threadLocals 是懶加載的,初始容量是 16
- 在不使用物件后應呼叫 remove() 避免記憶體溢位
InheritableThreadLocal
使用 TheadLocal,子執行緒訪問不了父執行緒的本地變數,InheritableThreadLocal 解決了該問題,
原始碼分析:
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
// 根據父執行緒本地變數的值計算子執行緒本地變數的值(這里是直接回傳原值)
protected T childValue(T parentValue) {
return parentValue;
}
// 重寫父類 ThreadLocal 的方法,將 threadLocals 替換成 inheritableThreadLocals
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
在創建執行緒時,會呼叫 init() 方法:將父執行緒的本地變數淺拷貝到子執行緒
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
//...
// 如果父執行緒的 inheritableThreadLocals 不為 null,則執行以下代碼
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
//...
}
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
// 變數父執行緒散串列鍵值對
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
// 如果鍵不為 null
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
// 獲取鍵對應的值
Object value = https://www.cnblogs.com/unsafe01/p/key.childValue(e.value);
// 創建新的鍵值對
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
// 使用線性探測法解決散列沖突
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
InheritableThreadLocal 總結:InheritableThreadLocal 在創建執行緒時通過將父執行緒的本地變數復制到子執行緒以實作子執行緒可以訪問父執行緒本地變數的目的,
第二章 并發編程的其他基礎知識
什么是多執行緒并發編程
并發與并行的概念:
- 并發是指同一時間段內多個任務同時在執行
- 并行是指單位時間內多個任務同時在執行
時間段由多個單位時間組成
任務型別:
- IO 密集型任務:對于IO密集型任務,我們應該盡量減少執行緒阻塞時對CPU的占用,減少CPU空閑時間
- CPU密集型任務:對于CPU密集型任務,我們應該盡量減少執行緒背景關系切換的開銷
Java 中的執行緒安全問題
共享資源:可以被多個執行緒讀寫的資源
當多個執行緒同時對共享資源進行讀寫時,就必須進行同步,
Java 記憶體模型規定,將所有變數都存放在主記憶體中,當執行緒使用變數時,會把記憶體里面的變數復制到自己的作業記憶體,執行緒讀寫變數時操作的都是自己作業記憶體中的變數,
這時,就需要通過同步機制來保證對共享資源操作的原子性

Java 中共享變數的記憶體可見性
Java 的記憶體模型是一個抽象的概念,作業記憶體對應到硬體架構就是CPU內的存盤器、一級快取、二級快取,
? 
快取導致的記憶體可見性問題:
- 執行緒A對共享變數1進行讀寫,并將結果同步到快取和主記憶體
- 執行緒B對共享變數1進行讀寫(從二級快取讀取),并將結果同步到快取和記憶體
- 此時,執行緒A在此對共享變數A進行讀寫,就會從一級快取讀到臟資料
Java 中的 synchronized 關鍵字
synchronized 是物件內部的一個監視器鎖,它是一個排他鎖,
synchronized 的記憶體語意:
- 進入 synchronized 塊的記憶體語意是把 synchronized 塊內使用到的變數從執行緒的作業記憶體中清除,這樣在 synchronized 塊內使用到該變數時就不會從執行緒的作業記憶體中獲取,而是直接從主記憶體中獲取;
- 退出 synchronized 的語意是把 synchronized 塊內對共享變數的修改重繪到主記憶體,
synchronized 可以解決共享變數記憶體的可見性問題,也可以用來實作原子性操作
Java 中的 volatile 關鍵字
當一個變數被宣告為 volatile 時,執行緒在寫入變數時不會把值快取在暫存器或其他地方,而是會把值重繪會主記憶體;當其他執行緒讀取該共享變數時,會從主記憶體重寫獲取最新值,而不是使用當前執行緒作業記憶體中的值,
volatile 可以解決共享變數記憶體的可見性問題和指令重排序問題,但不能保證操作的原子性
Java 中的 CAS 操作
CAS 通過硬體保證操作的原子性,
如果被操作的值存在環形轉換,使用CAS演算法就可能會出現ABA問題,解決的方式是增加一個遞增的版本號或時間戳,
Unsafe 類
Unsafe 類中的方法:
// 獲取變數的偏移值
public native long objectFieldOffset(Field f);
// 獲取陣列第一元素的地址
public native int arrayBaseOffset(Class<?> arrayClass);
// 獲取陣列一個元素的占用的位元組
public native int arrayIndexScale(Class<?> arrayClass);
// 原子性地更新
public final native boolean compareAndSwapLong(Object o, long offset,
long expected,
long x);
// 獲取 Long 型別的值(具有 volatile 語意)
public native long getLongVolatile(Object o, long offset);
// 設定 Long 型別的值(具有 volatile 語意)
public native void putLongVolatile(Object o, long offset, long x);
// 設定 Long 型別的值(不具有 volatile 語意)
public native void putOrderedLong(Object o, long offset, long x);
// 阻塞當前執行緒
public native void park(boolean isAbsolute, long time);
// 喚醒指定執行緒
public native void unpark(Object thread);
// 封裝 CAS 演算法的方法
public final long getAndSetLong(Object o, long offset, long newValue) {
long v;
do {
v = getLongVolatile(o, offset);
} while (!compareAndSwapLong(o, offset, v, newValue));
return v;
}
public final long getAndAddLong(Object o, long offset, long delta) {
long v;
do {
v = getLongVolatile(o, offset);
} while (!compareAndSwapLong(o, offset, v, v + delta));
return v;
}
獲取 Unsafe 物件:
public final class Unsafe {
private static native void registerNatives();
static {
registerNatives();
sun.reflect.Reflection.registerMethodsToFilter(Unsafe.class, "getUnsafe");
}
private Unsafe() {}
private static final Unsafe theUnsafe = new Unsafe();
@CallerSensitive
public static Unsafe getUnsafe() {
Class<?> caller = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(caller.getClassLoader()))
throw new SecurityException("Unsafe");
return theUnsafe;
}
}
由于 Unsafe 類做了限制,這里需要使用反射來獲取 Unsafe 物件:
public class UnsafeTest {
volatile long value;
static long valueOffset;
static Unsafe unsafe;
static {
try {
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
unsafe = (Unsafe) unsafeField.get(null);
valueOffset = unsafe.objectFieldOffset(UnsafeTest.class.getDeclaredField("value"));
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
UnsafeTest test = new UnsafeTest();
boolean success = UnsafeTest.unsafe.compareAndSwapLong(test, UnsafeTest.valueOffset, 0, 1);
System.out.println(success);
System.out.println(test.value);
}
}
指令重排序
Java 記憶體模型允許編譯器和處理器對指令重排序以提高運行性能,
在單執行緒下重排序可以保證最終執行的結果與程式順序執行的結果一致,但是在多執行緒下就會存在問題,
用volatile 可以解決指令重排序的導致的問題,
偽共享
偽共享出現的原因是因為快取和主記憶體進行資料交換的單位是快取行,多執行緒去修改同一快取行的不同變數時,只有一個執行緒可以去修改快取行的變數,因為快取一致性協議會使其他執行緒的同一快取行失效,使執行緒只能重新從二級快取或主記憶體讀取,從而造成性能下降,
在單執行緒下,快取行可以充分利用程式運行的區域性原理,從而提高程式性能,
多執行緒解決快取行的方法:
- 位元組填充
@sun.misc.Contended注解
使用 @Contended 注解使需要使用引數:-XX:-RestrictContended;
默認寬度是 128 位元組,可以使用 -XX:ContendedPaddingWidth 自定義寬度;
鎖的概述
- 悲觀鎖與樂觀鎖
- 獨占鎖與共享鎖
- 公平鎖與非公平鎖
- 可重入鎖
- 自旋鎖 (默認次數是10次,
-XX:PreBlockSpinsh)
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/201287.html
標籤:Java
上一篇:Java流程控制:選擇結構
