《并發(fā)編程的藝術(shù)》
并發(fā)編程的實(shí)現(xiàn)原理
目標(biāo)
- 上節(jié)課內(nèi)容回顧
-
synchronized原理分析 -
wait和notify -
Lock同步鎖
回顧
- 原子性
- 可見性
- 有序性
JMM
**JMM** 是 JAVA 里邊定義的內(nèi)存模型。定義了多線程和我們內(nèi)存交互的規(guī)范。屏蔽了硬件和操作系統(tǒng)訪問(wèn)內(nèi)存的差異。它類似于 JVM 的一個(gè)作用。提供了統(tǒng)一的規(guī)范。解決多核心 CPU 里邊的高速緩存和多線程并行訪問(wèn)內(nèi)存的原子性,可見性,有序性問(wèn)題。在不同的環(huán)境下的體現(xiàn)和解決辦法。定義了一套 JMM 規(guī)范,讓我們 JAVA 程序在不同的平臺(tái)下能夠達(dá)到一致的內(nèi)存訪問(wèn)效果。

- 在 JAVA 平臺(tái)里邊定義一套標(biāo)準(zhǔn)和規(guī)范。在 JAVA 里邊定義了 8 種操作。
- lock(鎖定):作用于主內(nèi)存的變量,把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占狀態(tài)。
- read(讀取):作用于主內(nèi)存變量,把一個(gè)變量值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動(dòng)作使用
- load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中。
- use(使用):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用變量的值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作。
- assign(賦值):作用于工作內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作。
- store(存儲(chǔ)):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量的值傳送到主內(nèi)存中,以便隨后的write的操作。
- write(寫入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中一個(gè)變量的值傳送到主內(nèi)存的變量中。
- unlock(解鎖):作用于主內(nèi)存變量,把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lái),釋放后的變量才可以被其他線程鎖定。
這 8 種操作都是原子性的。它定義了和我們內(nèi)存的交互的方式。
原子性、有序性、可見性。
解決的是 CPU 緩存、處理器優(yōu)化、指令重排序。
限制處理器的優(yōu)化以及使用內(nèi)存屏障
CPU 處理器在執(zhí)行的時(shí)候有一個(gè)優(yōu)化執(zhí)行的過(guò)程,使用內(nèi)存屏障來(lái)防止我們的編譯器和處理器的指令重排序。
線程在訪問(wèn)內(nèi)存的時(shí)候,有一個(gè)工作內(nèi)存。工作內(nèi)存對(duì)于每一個(gè) CPU 來(lái)說(shuō),是一塊完全獨(dú)立的緩存空間。如果一個(gè)線程去加載一個(gè)共享變量的話,它會(huì)首先去工作內(nèi)存中去加載,如果不存在的話,它會(huì)去主內(nèi)存去加載。`Load` 放到我們工作內(nèi)存里邊。 **JMM** 定義的一套抽象模型的統(tǒng)一規(guī)范。它是把底層的差異化通過(guò) JMM 來(lái)進(jìn)行規(guī)范。我們不管底層的 CPU 架構(gòu)是什么樣子,不管系統(tǒng)是什么樣子,它都能夠在 JMM 中做不同的處理。
- 處理器的優(yōu)化
- 指令重排序(編譯器的重排序和CPU 的內(nèi)存重排序)
JAVA 層面如何解決我們的問(wèn)題?
我們寫的代碼和編譯的代碼可能存在順序不一致的情況。重排序可以提升我們程序運(yùn)行的性能。以及合理地利用我們操作系統(tǒng)底層 CPU 資源的一種優(yōu)化手段,它的最終目的是提升性能。它有一個(gè)標(biāo)準(zhǔn):不會(huì)改變我們指令的語(yǔ)義。
我們定義了一個(gè)程序,不管我怎么改變,他的結(jié)果是不會(huì)去改變的。
- 編譯器的優(yōu)化重排序
- CPU 的指令重排序
- 內(nèi)存系統(tǒng)的重排序
‘編譯器的亂序
-
不改變單線程語(yǔ)義的前提下。
int a = 1; //(1) int b = a; //(2) // 是不會(huì)亂序的,倒敘之后會(huì)錯(cuò)誤
重排序可能造成可見性
在多線程中可以并行執(zhí)行多個(gè)線程。
CPU 的亂序是因?yàn)橛屑拇嫫?、高速緩存。寄存器是為了存?chǔ)我們本地的一些變量和一些本地的參數(shù)。CPU 里邊還有一個(gè)叫高速緩存(L1、L2、L3)。是為了縮短CPU和內(nèi)存訪問(wèn)速度的性能差異。
緩存離 CPU 越遠(yuǎn),性能越低。寄存器 > L1 > L2 > L3 。只有 L3 在多核心的情況下是共享的。
緩存一致性(基于 MESI 協(xié)議去解決)
如果多個(gè) CPU 都加載相同的數(shù)據(jù)加載到我們的緩存里邊,會(huì)造成我們的緩存一致性問(wèn)題。
CPU-0 S -> E -> M
CPU-2 S
如果其中一個(gè)CPU 讀取了緩存,就代表當(dāng)前的狀態(tài)是 S。對(duì)這個(gè)值做一個(gè)更改的話, 變成 E (獨(dú)占)。 M 更新 發(fā)出一個(gè) 失效的信號(hào) Invlid。等到其他 CPU 回復(fù)以后,就會(huì)進(jìn)行更新。把我們當(dāng)前的這個(gè)緩存更新到我們的主存里邊。當(dāng)我們的緩存的狀態(tài)發(fā)生切換的時(shí)候。其他的緩存收到消息,并且需要完成各自的狀態(tài)切換的時(shí)候。
這時(shí)候,就是一個(gè) CPU 等待的狀態(tài)。時(shí)間片等待就是一個(gè)性能問(wèn)題。
為了更進(jìn)一步地優(yōu)化,引入了 storebuffer / loadbuffer 減少阻塞。它是去減少阻塞。如說(shuō)說(shuō)我處理器要去寫入數(shù)據(jù)的時(shí)候。寫入到 storebuffer 里邊,對(duì)于 CPU-0 來(lái)說(shuō),它可以繼續(xù)去干其他事情,不會(huì)等待。由同步變成了異步。 **MESI** 協(xié)議里邊,如果寫入的話,必須等待其他 CPU 給你一個(gè)響應(yīng)。I 的狀態(tài),再去更新你的緩存,CPU 的阻塞是存在性能問(wèn)題的。引入 storebuffer 和 loadbuffer 不會(huì)去阻塞。當(dāng)收到所有的CPU 的回復(fù)以后就會(huì)提交。(相當(dāng)于 CPU 引入的一個(gè)異步機(jī)制。)
CPU-0 寫入到 storebuffer 里邊的時(shí)候,其他 CPU 是看不到的,它自己再去讀取的時(shí)候,是可以從 storebuffer 里邊去讀取的。這是它的一個(gè)規(guī)則。storebuffer 什么時(shí)候把這個(gè)東西寫入到主內(nèi)存,它是不確定的。
Loadbuffer ,等待同步去加載。它會(huì)導(dǎo)致我們完成的順序是不確定的。造成一些不同的結(jié)果。
x = 1; // 寫
y = x; // 讀 , 寫
CPU 層面上引入了內(nèi)存屏障的功能。
`store barrier` / `load barrier` / `full barrier`
內(nèi)存屏障的意思就是我們 CPU 或者編譯器在對(duì)我們內(nèi)存隨機(jī)訪問(wèn)的操作里邊,它在某個(gè)地方加入了同步的點(diǎn)。同步點(diǎn),使得我們屏障之前的讀寫操作全部執(zhí)行完才能執(zhí)行屏障之后的指令。**同步點(diǎn)**,CPU 寫入數(shù)據(jù)的時(shí)候是異步的。他必須等待其他 CPU 確認(rèn)以后才會(huì)去同步這個(gè)數(shù)據(jù)。不同 CPU 架構(gòu)的內(nèi)存屏障是不一樣的。(x86)
內(nèi)存屏障的作用
- 保證數(shù)據(jù)的可見性
- 防止指令之間的重排序
有些 CPU 架構(gòu)對(duì)內(nèi)存屏障的支持是不一樣的,有些 CPU 是支持強(qiáng)一致性的,就不需要內(nèi)存屏障了。
多線程在 JAVA 中的體現(xiàn)
我們關(guān)心在 JVM 層面如何解決這些問(wèn)題就好了
JVM 提供了四種內(nèi)存屏障來(lái)解決 CPU 的指令重排序和編譯器的指令重排序的問(wèn)題。
inline void OrderAccess::loadload() { compiler_barrier(); }
inline void OrderAccess::storestore() { compiler_barrier(); }
inline void OrderAccess::loadstore() { compiler_barrier(); }
inline void OrderAccess::storeload() { fence(); }
每一個(gè)屏障都定義了一個(gè)方法。功能最全,性能比較低。
inline void OrderAccess::fence() {
// always use locked addl since mfence is sometimes expensive
#ifdef AMD64
__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
compiler_barrier();
}
static inline void compiler_barrier() {
__asm__ volatile ("" : : : "memory");
}
template<>
struct OrderAccess::PlatformOrderedStore<1, RELEASE_X_FENCE>
{
template <typename T>
void operator()(T v, volatile T* p) const {
__asm__ volatile ( "xchgb (%2),%0"
: "=q" (v)
: "0" (v), "r" (p)
: "memory");
}
};
template<>
struct OrderAccess::PlatformOrderedStore<2, RELEASE_X_FENCE>
{
template <typename T>
void operator()(T v, volatile T* p) const {
__asm__ volatile ( "xchgw (%2),%0"
: "=r" (v)
: "0" (v), "r" (p)
: "memory");
}
};
template<>
struct OrderAccess::PlatformOrderedStore<4, RELEASE_X_FENCE>
{
template <typename T>
void operator()(T v, volatile T* p) const {
__asm__ volatile ( "xchgl (%2),%0"
: "=r" (v)
: "0" (v), "r" (p)
: "memory");
}
};
#ifdef AMD64
template<>
struct OrderAccess::PlatformOrderedStore<8, RELEASE_X_FENCE>
{
template <typename T>
void operator()(T v, volatile T* p) const {
__asm__ volatile ( "xchgq (%2), %0"
: "=r" (v)
: "0" (v), "r" (p)
: "memory");
}
};
個(gè)人理解它是一個(gè) 1 2 4 8 核 和 AMD 架構(gòu)的 不同策略。
volatile 是禁止編譯器對(duì)重排序的優(yōu)化。
Lock
- 總線鎖
- 緩存鎖
我們CPU 大部分都是用 緩存鎖。
- read -> 獲取
- modity-> 變化
- write -> 寫入
fence 就是用的內(nèi)存屏障,并沒有用到 CPU 層面的內(nèi)存屏障。不同的 CPU 架構(gòu),實(shí)現(xiàn)不太一樣。它是用 Lock 來(lái)實(shí)現(xiàn)我們內(nèi)存屏障的效果。
`x86` 是強(qiáng)一致性的。
Volatile 關(guān)鍵字
- 可見性
- 內(nèi)存屏障
- 編譯器的內(nèi)存屏障
- 處理器的內(nèi)存屏障
- 內(nèi)存屏障
int a = 1;
volatile int b = a;
// 對(duì)于 volatile 寫來(lái)說(shuō),
// storesotre() 指令(release)
// b = a;
// storeload 強(qiáng)制加入這個(gè)指令。固定的加了 storeload
`J.U.C` 里邊思路是一樣的。storestore 是讓寫全部同步到主內(nèi)存。storeload 是讓所有的讀和寫全部寫入到內(nèi)存。
public class SynchronizedDemo {
private static int count = 0;
public static void incr() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(() -> SynchronizedDemo.incr()).start();
}
Thread.sleep(4000);
out.println("result: " + SynchronizedDemo.count); // <= 1000
}
}
synchronized的使用
解決問(wèn)題
- 原子性
- 可見性
- 有序性
在多線程并發(fā)編程中 `synchronized` 一直是元老級(jí)角色,很多人都會(huì)稱呼它為重量級(jí)鎖。但是,隨著 **Java SE 1.6** 對(duì) `synchronized` 進(jìn)行了各種優(yōu)化之后,有些情況下它就并不那么重了,Java SE 1.6中為了減少獲得鎖和釋放鎖帶來(lái)的性能消耗而引入的偏向鎖和輕量級(jí)鎖,以及鎖的存儲(chǔ)結(jié)構(gòu)和升級(jí)過(guò)程。我們?nèi)匀谎赜们懊媸褂玫陌咐?,然后通過(guò) `synchronized` 關(guān)鍵字來(lái)修飾在inc的方法上。再看看執(zhí)行結(jié)果。
public class SynchronizedDemo {
private static int count = 0;
public synchronized static void incr() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(() -> SynchronizedDemo.incr()).start();
}
Thread.sleep(4000);
out.println("result: " + SynchronizedDemo.count);
}
}
synchronized的三種應(yīng)用方式
synchronized有三種方式來(lái)加鎖,分別是
修飾實(shí)例方法,作用于當(dāng)前實(shí)例加鎖,進(jìn)入同步代碼前要獲得當(dāng)前實(shí)例的鎖
靜態(tài)方法,作用于當(dāng)前類對(duì)象加鎖,進(jìn)入同步代碼前要獲得當(dāng)前類對(duì)象的鎖
修飾代碼塊,指定加鎖對(duì)象,對(duì)給定對(duì)象加鎖,進(jìn)入同步代碼庫(kù)前要獲得給定對(duì)象的鎖。
public void demo(){
// 全局鎖,多個(gè)對(duì)象是同一把鎖
synchronized (SynchronizedDemo.class){
//.......
}
}
public void demo1(){
// 每個(gè)實(shí)例是不同的鎖
synchronized (this){
//.......
}
}
public synchronized static void incr() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
問(wèn)題
- synchronized 是如何實(shí)現(xiàn)鎖的。
- 為什么任何一個(gè)對(duì)象都可以成為鎖
- 鎖存在哪個(gè)地方
synchronized括號(hào)后面的對(duì)象
`synchronized` 括號(hào)后面的對(duì)象是一把鎖,在 JAVA 中任意一個(gè)對(duì)象都可以成為鎖,簡(jiǎn)單來(lái)說(shuō),我們把 `Object` 比喻是一個(gè) key ,擁有這個(gè) key 的線程才能執(zhí)行這個(gè)方法,拿到這個(gè) key 以后在執(zhí)行方法過(guò)程中,這個(gè) key 是隨身攜帶的,并且只有一把。如果后續(xù)的線程想訪問(wèn)當(dāng)前方法,因?yàn)闆]有 key 所以不能訪問(wèn),只能在門口等著,等之前的線程把 key 放回去。所以, synchronized 鎖定的對(duì)象必須是同一個(gè),如果是不同對(duì)象,就意味著是不同的房間的鑰匙,對(duì)于訪問(wèn)者來(lái)說(shuō)是沒有任何影響的。
Object lock = new Object();
public void demo3(){
synchronized(lock){
}
}
synchronized的字節(jié)碼指令
通過(guò) `javap -v` 來(lái)查看對(duì)應(yīng)代碼的字節(jié)碼指令,對(duì)于同步塊的實(shí)現(xiàn)使用了 `monitorenter` 和 `monitorexit` 指令,前面我們?cè)谥v **JMM** 的時(shí)候,提到過(guò)這兩個(gè)指令,他們隱式地執(zhí)行了 `Lock` 和 `UnLock` 操作,用于提供原子性保證。 `monitorenter` 指令插入到同步代碼塊開始的位置、`monitorexit`指令插入到同步代碼塊結(jié)束位置,jvm需要保證每個(gè) `monitorenter` 都有一個(gè) `monitorexit` 對(duì)應(yīng)。
這兩個(gè)指令,本質(zhì)上都是對(duì)一個(gè)對(duì)象的監(jiān)視器 ( `monitor` ) 進(jìn)行獲取,
這個(gè)過(guò)程是排他的,也就是說(shuō)同一時(shí)刻只能有一個(gè)線程獲取到由 `synchronized` 所保護(hù)對(duì)象的監(jiān)視器線程執(zhí)行到 `monitorenter` 指令時(shí),會(huì)嘗試獲取對(duì)象所對(duì)應(yīng)的 monitor 所有權(quán),也就是嘗試獲取對(duì)象的鎖;而執(zhí)行 `monitorexit` ,就是釋放 `monitor` 的所有權(quán)。
所有的 JAVA 對(duì)象天生帶有 `monitor`
public void demo1();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_1
5: monitorexit
6: goto 14
9: astore_2
10: aload_1
11: monitorexit
12: aload_2
13: athrow
14: return
public static synchronized void incr();
descriptor: ()V
flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
獲得一把鎖,就需要去釋放
兩個(gè) moitorexit 一個(gè)是異常的時(shí)候,會(huì)釋放鎖。
synchronized的鎖的原理
**jdk1.6** 以后對(duì) `synchronized` 鎖進(jìn)行了優(yōu)化,包含偏向鎖、輕量級(jí)鎖、重量級(jí)鎖;在了解 `synchronized` 鎖之前,我們需要了解兩個(gè)重要的概念,一個(gè)是對(duì)象頭、另一個(gè)是 `monitor`
- 偏向鎖
- 輕量級(jí)鎖
- 重量級(jí)鎖
Java對(duì)象頭
對(duì)象在內(nèi)存三個(gè)區(qū)域
對(duì)象頭
實(shí)例數(shù)
-
對(duì)齊填充
在 Hotspot 虛擬機(jī)中,對(duì)象在內(nèi)存中的布局分為三塊區(qū)域:對(duì)象頭、實(shí)例數(shù)據(jù)和對(duì)齊填充;Java對(duì)象頭是實(shí)現(xiàn)synchronized的鎖對(duì)象的基礎(chǔ),一般而言,synchronized使用的鎖對(duì)象是存儲(chǔ)在Java對(duì)象頭里。它是輕量級(jí)鎖和偏向鎖的關(guān)鍵。
Mark Word
Mark Word用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程 ID、偏向時(shí)間戳等等。Java對(duì)象頭一般占有兩個(gè)機(jī)器碼(在32位虛擬機(jī)中,1個(gè)機(jī)器碼等于4字節(jié),也就是32bit)
32位的
64位的

在源碼中的體現(xiàn)
如果想更深入了解對(duì)象頭在JVM源碼中的定義,需要關(guān)心幾個(gè)文件`oop.hpp``/markOop.hpp`
`oop.hpp`,每個(gè) Java Object 在 JVM 內(nèi)部都有一個(gè) native 的 C++ 對(duì)象 `oop` / `oopDesc` 與之對(duì)應(yīng)。先在 `oop.hpp` 中看 **oopDesc** 的定義。

_mark 被聲明在 oopDesc 類的頂部,所以這個(gè) _mark 可以認(rèn)為是一個(gè) 頭部, 前面我們講過(guò)頭部保存了一些重要的狀態(tài)和標(biāo)識(shí)信息,在 markOop.hpp 文件中有一些注釋說(shuō)明 markOop的內(nèi)存布局。 age 分代年齡, epoch 偏向鎖的時(shí)間戳。

oop.hpp
class oopDesc {
friend class VMStructs;
friend class JVMCIVMStructs;
private:
volatile markOop _mark; // 就是對(duì)象頭
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
markOop.hpp

Monitor
什么是 **Monitor** ?我們可以把它理解為一個(gè)同步工具,也可以描述為一種同步機(jī)制。所有的Java對(duì)象是天生的 Monitor ,每個(gè)object的對(duì)象里 `markOop->monitor()` 里可以保存 **ObjectMonitor** 的對(duì)象。從源碼層面分析一下monitor對(duì)象。
-
oop.hpp下的oopDesc類是JVM對(duì)象的頂級(jí)基類,所以每個(gè)object對(duì)象都包含markOopoop.hpp

-
markOop.hpp中markOopDesc繼承自oopDesc,并擴(kuò)展了自己的monitor方法,這個(gè)方法返回一個(gè)ObjectMonitor 指針對(duì)象

-
objectMonitor.hpp,在hotspot虛擬機(jī)中,采用ObjectMonitor類來(lái)實(shí)現(xiàn)monitor,

總結(jié)
任何對(duì)象在我們 JVM 層面有一個(gè) oop 和 oopDesc 的對(duì)應(yīng)。`oop.hpp` 會(huì)存在一個(gè) mark 的對(duì)象頭,對(duì)象頭用來(lái)存儲(chǔ)鎖標(biāo)志的。這個(gè)鎖標(biāo)志,是用來(lái)存儲(chǔ)對(duì)應(yīng)的偏向鎖、輕量鎖等的標(biāo)志。
鎖是存在對(duì)象頭里邊的。
QA:
對(duì)象鎖之間不相互干擾。全局鎖意味著不管你多少個(gè)實(shí)例,我都能夠鎖定你。鎖的范圍,取決于你的對(duì)象的生命周期。這個(gè)對(duì)象的生命周期有多大,那你的鎖的作用域就有多大。
synchronized的鎖升級(jí)和獲取過(guò)程
了解了對(duì)象頭以及 `monitor` 以后,接下來(lái)去分析 `synchronized` 的鎖的實(shí)現(xiàn),就會(huì)非常簡(jiǎn)單了。前面講過(guò) `synchronized` 的鎖是進(jìn)行過(guò)優(yōu)化的,引入了偏向鎖、輕量級(jí)鎖;鎖的級(jí)別從低到高逐步升級(jí), 無(wú)鎖->偏向鎖->輕量級(jí)鎖->重量級(jí)鎖。
- 無(wú)鎖
- 偏向鎖
- 輕量級(jí)鎖
- 重量級(jí)鎖
- 輕量級(jí)鎖
- 偏向鎖
自旋鎖(CAS)
自旋鎖就是讓不滿足條件的線程等待一段時(shí)間,而不是立即掛起??闯钟墟i的線程是否能夠很快釋放鎖。怎么自旋?其實(shí)就是一段沒有任何意義的循環(huán)。雖然它通過(guò)占用處理器的時(shí)間來(lái)避免線程切換帶來(lái)的開銷,但是如果持有鎖的線程不能很快釋放鎖,那么自旋的線程就會(huì)浪費(fèi)處理器的資源,因?yàn)樗粫?huì)做任何有意義的工作。所以,自旋等待的時(shí)間或者次數(shù)是有一個(gè)限度的,如果自旋超過(guò)了定義的時(shí)間仍然沒有獲取到鎖,則該線程應(yīng)該被掛起。
-
// 自旋 for(;;){ // 不斷地獲取鎖 } // 1.7 之前可以自己配置 1.7 之后,JVM 去控制耗費(fèi)不耗費(fèi)CPU ,只是一個(gè)相對(duì)地概念。
偏向鎖
**大多數(shù)情況下,鎖不僅不存在多線程競(jìng)爭(zhēng),而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價(jià)更低而引入了偏向鎖。**當(dāng)一個(gè)線程訪問(wèn)同步塊并獲取鎖時(shí),會(huì)在對(duì)象頭和棧幀中的鎖記錄里存儲(chǔ)鎖偏向的線程 ID ,以后該線程在進(jìn)入和退出同步塊時(shí)不需要進(jìn)行 CAS 操作來(lái)加鎖和解鎖,只需簡(jiǎn)單地測(cè)試一下對(duì)象頭的 Mark Word 里是否存儲(chǔ)著指向當(dāng)前線程的偏向鎖。如果測(cè)試成功,表示線程已經(jīng)獲得了鎖。如果測(cè)試失敗,則需要再測(cè)試一下 Mark Word 中偏向鎖的標(biāo)識(shí)是否設(shè)置成 1 (表示當(dāng)前是偏向鎖):如果沒有設(shè)置,則使用 CAS 競(jìng)爭(zhēng)鎖;如果設(shè)置了,則嘗試使用 CAS 將對(duì)象頭的偏向鎖指向當(dāng)前線程。
( 就是頭里邊的 JavaThread. )

輕量級(jí)鎖
引入輕量級(jí)鎖的主要目的是在沒有多線程競(jìng)爭(zhēng)的前提下,減少傳統(tǒng)的重量級(jí)鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗。當(dāng)關(guān)閉偏向鎖功能或者多個(gè)線程競(jìng)爭(zhēng)偏向鎖導(dǎo)致偏向鎖升級(jí)為輕量級(jí)鎖,則會(huì)嘗試獲取輕量級(jí)鎖。

重量級(jí)鎖
重量級(jí)鎖通過(guò)對(duì)象內(nèi)部的監(jiān)視器(`monitor`)實(shí)現(xiàn),其 `monitor` 的本質(zhì)是依賴于底層操作系統(tǒng)的 **Mutex Lock** 實(shí)現(xiàn),操作系統(tǒng)實(shí)現(xiàn)線程之間的切換需要從用戶態(tài)到內(nèi)核態(tài)的切換,切換成本非常高。前面我們?cè)谥v **JAVA對(duì)象頭** 的時(shí)候,講到了`monitor` 這個(gè)對(duì)象,在 hotspot 虛擬機(jī)中,通過(guò) `ObjectMonitor` 類來(lái)實(shí)現(xiàn) `monitor` 。他的鎖的獲取過(guò)程會(huì)簡(jiǎn)單很多。

類對(duì)象里邊也有一個(gè) `ObjectMonitor` , _owner // 指向獲得 ObjectMonitor 的線程。
它的鎖是一個(gè)全局的鎖。多個(gè)線程同時(shí)去訪問(wèn) `ObjectMonitor` 的時(shí)候,這個(gè)時(shí)候,它只會(huì)有一個(gè)線程來(lái)獲得,但是對(duì)于多個(gè)實(shí)例來(lái)說(shuō),他只有一個(gè)實(shí)例來(lái)獲得。每一個(gè)線程去獲得一個(gè)對(duì)象都不一樣。所以它能夠?qū)崿F(xiàn)鎖的作用域。
自旋是一定的時(shí)間,不斷地自旋反而耗費(fèi)了 CPU 資源,自旋的目的,(概率的說(shuō)法)很多時(shí)候獲取鎖的時(shí)間比較短,自旋很短的時(shí)間就可以獲得鎖了。為什么要讓線程掛起再去獲得鎖。雖然,自旋消耗了一點(diǎn) CPU 的資源,但是相比于后者,它的性能反而是提升了。但是它不可能一直持續(xù)下去。
ObjectWaiter * volatile _next;
ObjectWaiter * volatile _prev;
JVM 為每一個(gè)嘗試進(jìn)入 `synchronized` 代碼塊的 JavaThread 創(chuàng)建一個(gè) `ObjectWaiter` 并添加到 _cxq 隊(duì)列中。__next _prev 這是一個(gè)虛擬的隊(duì)列,并沒有存在個(gè)真正的數(shù)據(jù)結(jié)構(gòu),它是通過(guò)節(jié)點(diǎn)的方式去維護(hù)的。
_EntryList : 處于等待鎖 block 狀態(tài)的線程,由 ObjectWaiter 組成的雙向量表, JVM 會(huì)從該鏈表中取出一個(gè) ObjectWaiter 并喚醒對(duì)應(yīng)的 JavaThread
_waitSet: 調(diào)用 wait 狀態(tài)的線程的時(shí)候,會(huì)被加入到 waitSet
CXQ隊(duì)列 : LIFO 后進(jìn)先出的隊(duì)列。
每一個(gè)線程進(jìn)入以后都會(huì)有一個(gè)自旋,嘗試去獲得鎖。自旋失敗,`#park` , `#park` 是掛起一個(gè)線程。如果沒有自旋直接掛起的時(shí)候,從掛起到喚醒從用戶態(tài)和內(nèi)核態(tài)的切換,消耗會(huì)比較高。
出隊(duì)列會(huì)用指針移動(dòng)的操作,操作的時(shí)間會(huì)變得很少。
EnterList 隊(duì)列 2Q ,兩個(gè)隊(duì)列的方式,用來(lái)減少競(jìng)爭(zhēng)的頻率。當(dāng)我們?cè)?CXQ 里邊,可以移入 EntryList 里邊,如果 EntryList 是空,CXQ 不為空的情況下。從 CXQ 末尾取出一個(gè)線程放到 EntryList 里邊。
ownerThread 釋放鎖以后,讓出隊(duì)列,有資格去競(jìng)爭(zhēng)鎖。當(dāng)競(jìng)爭(zhēng)鎖成功,就會(huì)被設(shè)置成 owner 。
wait 和 notify
Synchronized 支持重入,非公平鎖。
#wait 和 #notify 是用來(lái)讓線程進(jìn)入等待狀態(tài)以及使得線程喚醒的兩個(gè)操作
public class ThreadWait extends Thread {
private Object lock;
public ThreadWait(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock){
System.out.println("開始執(zhí)行 ThreadWait ");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執(zhí)行結(jié)束 ThreadWait ");
}
}
}
public class ThreadNotify extends Thread {
private Object lock;
public ThreadNotify(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
System.out.println("開始執(zhí)行 ThreadNotify ");
lock.notify();
System.out.println("執(zhí)行結(jié)束 ThreadNotify ");
}
}
}
public class Demo {
public static void main(String[] args) {
// 我將鎖傳進(jìn)去,就可以實(shí)現(xiàn)對(duì)象同用一把鎖。
// 還可以控制鎖的范圍
Object lock = new Object();
ThreadWait threadWait = new ThreadWait(lock);
ThreadNotify threadNotify = new ThreadNotify(lock);
threadWait.start();
threadNotify.start();
}
}
#wait 和 #notify 的原理
-
Wait和notify為什么要先獲取鎖? -
wait和sleep的區(qū)別?
調(diào)用 `#wait` 方法,首先會(huì)獲取監(jiān)視器鎖,獲得成功以后,會(huì)讓當(dāng)前線程進(jìn)入等待狀態(tài)進(jìn)入等待隊(duì)列并且釋放鎖;然后當(dāng)其他線程調(diào)用 `#notify` 或者 `notifyall` 以后,會(huì)選擇從等待隊(duì)列中喚醒任意一個(gè)線程,而執(zhí)行完 `#notify` 方法以后,并不會(huì)立馬喚醒線程,原因是當(dāng)前的線程仍然持有這把鎖,處于等待狀態(tài)的線程無(wú)法獲得鎖。必須要等到當(dāng)前的線程執(zhí)行完按 `monitorexit` 指令以后,也就是鎖被釋放以后,處于等待隊(duì)列中的線程就可以開始競(jìng)爭(zhēng)鎖了。

wait 方法的時(shí)候 ObjectMonitor::wait(jlong milllis, boo...){}
- 會(huì)把當(dāng)前的線程包裝成 ObjectWaiter對(duì)象,
- 并設(shè)置成 TS_WAIT 狀態(tài),就是一個(gè) waiting 的
- Self _ParkEvent->park(); 掛起!
notify
- unpark
- 獲取 ObjectWaiter
- 喚醒
wait 和 notify 為什么需要在 synchronized 里面
`#wait`方法的語(yǔ)義有兩個(gè),一個(gè)是釋放當(dāng)前的對(duì)象鎖、另一個(gè)是使得當(dāng)前線程進(jìn)入阻塞隊(duì)列, 而這些操作都和監(jiān)視器是相關(guān)的,所以 `#wait` 必須要獲得一個(gè)監(jiān)視器鎖而對(duì)于 `#notify` 來(lái)說(shuō)也是一樣,它是喚醒一個(gè)線程,既然要去喚醒,首先得知道它在哪里?所以就必須要找到這個(gè)對(duì)象獲取到這個(gè)對(duì)象的鎖,然后到這個(gè)對(duì)象的等待隊(duì)列中去喚醒一個(gè)線程。
public ThreadNotify(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock){
System.out.println("開始執(zhí)行 thread notify");
lock.notify();
System.out.println("執(zhí)行結(jié)束 thread notify");
}
}
java.lang.Thread#join() join 就是調(diào)用的 wait 方法。
public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
`wait` 我們?cè)趯?shí)際過(guò)程中用得很少,我們可能不會(huì)徹底地掌控到線程的狀態(tài)。線程一般都是用線程池,用 `Thread` 去實(shí)現(xiàn) `wait` 用得比較少。
有 AQS ,就不用 wait notify 了
synchronized 是如何實(shí)現(xiàn)鎖的?
輕量級(jí)鎖到偏向鎖到重量級(jí)鎖。
為什么任何一個(gè)任何對(duì)象都可以成為鎖?
每個(gè) JAVA 對(duì)象在我們 JVM 里邊都會(huì)有一個(gè)對(duì)象頭,對(duì)象頭會(huì)存儲(chǔ)鎖的一些標(biāo)志,當(dāng)前是輕量級(jí)鎖,還是重量級(jí)鎖。ObjectMonitor 做一個(gè)監(jiān)視器,去實(shí)現(xiàn)鎖競(jìng)爭(zhēng)一種機(jī)制。
來(lái)源于: https://javaguide.net