目錄
多線程原理分析(一)
一、包含的知識點
- 多線程帶來的問題(好處、壞處)
- synchronized加鎖方式
- 鎖信息是如何存儲的(Header、Mark Word)
- synchronized鎖升級流程(偏向鎖、輕量級鎖、重量級鎖)
二、多線程帶來的問題
2.1 多線程帶來的好處、壞處
- 能夠充分利用多核CPU, 實現線程並行執行
- 多線程對於共享變量訪問帶來安全性問題
2.2 線程安全問題的本質
線程安全性本質,是對於數據狀態的訪問, 這個狀態通常是共享的、可變的, 而問題的本質是對共享變量的訪問
- 共享的, 指數據變量可以被多個線程訪問
- 可變的, 指數據變量在生命週期內是可改變的
2.3 解決線程安全問題方式
- synchronized
三、Synchronized基本認識
3.1 synchronized 加鎖方式
- 修飾實例方法, 對當前實例加鎖,進入同步代碼前要獲得當前**實例的鎖**
- 修飾代碼塊, 對當前類對象加鎖,進入同步代碼前要獲得當前**類對象的鎖**
- 修飾類方法, 指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得**給定對象的鎖**
這裏以代碼塊加鎖進行舉例
public class SynchronizedThread {
private static int count = 0 ;
public static void main(String[] args) throws InterruptedException {
for (int i = 0 ; i < 2000; i++) {
new Thread(() -> incr()).start();
}
Thread.sleep(3000);
System.out.println("result -> " + count);
}
public static void incr() {
synchronized (SynchronizedThread.class) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++ ;
}
}
}
3.2 鎖是如何存儲的
3.2.1 鎖包含的數據內容
這裏以 synchronized(lock) 爲例來說明
同步代碼塊以lock的生命週期來控制鎖的粒度, 對象在內存中的佈局主要包含三部分: 對象頭(Header)、實例數據(InstanceData)、對其填充(Padding)
下面是數據結構關係圖, 鎖的信息就維護在對象頭(Header)中
3.2.2 JVM創建對象實現
創建Java對象時, JVM使用 InstanceOopDesc和arrayOopDesc 來描述對象頭, 下面是InstanceOop.hpp文件的內容
#ifndef SHARE_VM_OOPS_INSTANCEOOP_HPP
#define SHARE_VM_OOPS_INSTANCEOOP_HPP
#include "oops/oop.hpp"
// An instanceOop is an instance of a Java Class
// Evaluating "new HashTable()" will create an instanceOop.
class instanceOopDesc : public oopDesc {
public:
// aligned header size.
static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; }
// If compressed, the offset of the fields of the instance may not be aligned.
static int base_offset_in_bytes() {
// offset computation code breaks if UseCompressedClassPointers
// only is true
return (UseCompressedOops && UseCompressedClassPointers) ?
klass_gap_offset_in_bytes() :
sizeof(instanceOopDesc);
}
static bool contains_field_offset(int offset, int nonstatic_field_size) {
int base_in_bytes = base_offset_in_bytes();
return (offset >= base_in_bytes &&
(offset-base_in_bytes) < nonstatic_field_size * heapOopSize);
}
};
#endif // SHARE_VM_OOPS_INSTANCEOOP_HPP
文中沒有我們想了解的_mark、_medaData信息, 而InstanceOopDesc 繼承了oopDesc , 查看oop.hpp文件內容, 包含下面的內容
class oopDesc {
private:
volatile markOop _mark; //對象標記信息
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata; //類元信息
}
對象標記信息類型屬於markOop, 查看markOop.hpp文件, 包含下面的內容
// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ------------------------------------------>| (CMS free block)
// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
enum { age_bits = 4, // 分代年齡
lock_bits = 2, // 鎖標誌 0-false, 1-true
biased_lock_bits = 1, // 是否是偏向鎖標誌
//hashcode
max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits,
cms_bits = LP64_ONLY(1) NOT_LP64(0),
epoch_bits = 2 //偏向鎖時間戳
};
markOpp存儲的數據會隨着鎖標誌位的變化而變化, 包含的數據對象存在下面的情況
鎖狀態 | 25bit | 4bit | 1bit | 2bit | |
---|---|---|---|---|---|
23bit | 2bit | 是否偏向鎖 | 鎖標誌位 | ||
無鎖 | 對象的hashcode | 分代年齡 | 0 | 01 | |
偏向鎖 | 線程ID | Epoch | 分代年齡 | 1 | 01 |
輕量級鎖 | 指向棧中鎖記錄的指針 | 00 | |||
重量級鎖 | 指向互斥量(重量級鎖)的指針 | 10 | |||
GC標誌 | 空 | 11 |
3.2.3 爲什麼任何對象都可以實現鎖
-
Java中每個對象都隱式的繼承Object, 而Object對象在JVM中都有一個native對象oop/oopDesc與之對應
-
線程獲取鎖, 實際獲取的是監視器對象(monitor), 多線程訪問同步代碼, 相當於爭搶監視器對象, 修改對象鎖標誌
而Java對象天生帶有monitor對象,下面是markOop.hpp文件monitor
ObjectMonitor* monitor() const { assert(has_monitor(), "check"); // Use xor instead of &~ to provide one extra tag-bit check. return (ObjectMonitor*) (value() ^ monitor_value); }
3.3 synchronized鎖升級
在分析鎖如何存儲一節中, markOop對鎖進行了分類, 按照鎖的狀態分爲: 無鎖、偏向鎖、輕量級鎖、重量級鎖
在無鎖的情況下, 對共享變量訪問性能比較高, 但是高併發情況下存在安全問題 ; 加鎖情況下, 可以保證共享變量安全問題, 但是性能比較低, 爲了在數據訪問安全性和數據訪問性能之間有個折中, 提出了偏向鎖、輕量級鎖的概念(JDK6提供)
3.3.1 偏向鎖原理
當一個線程訪問加了同步鎖的代碼塊時, 會在對象頭(Header)記錄下這個線程Id(ThreadId), 後續線程進入代碼時, 會比較線程ID是否相同, 如果相同, 說明是同一線程, 進入或者退出代碼塊時不用再次獲取/釋放鎖
偏向, 表示偏向當前線程, 不用再次獲取線程鎖。
3.3.2 偏向鎖獲取/釋放邏輯
在進行偏向鎖獲取/釋放邏輯講解前, 請先看下下面流程圖, 我們將以下面的流程圖來進行講解
偏向鎖獲取流程
- 首相獲取鎖對象的Mark Word, 判斷是否處於可偏向狀態(biased_lock=1, 且ThreadId爲空)
- 如果是可偏向狀態, 通過CAS方式將ThreadId寫如鎖對象頭(Header)中
- 如果CAS成功, 表示成功獲取了鎖對象的偏向所, 接着執行同步代碼塊
- 如果CAS失敗, 表示有其它線程獲取了鎖對象的偏向鎖, 說明存在鎖衝突現象, 需要撤銷偏向鎖, 將鎖升級爲輕量級鎖(這個操作需要等到全局安全點)
- 如果是已偏向狀態, 需要檢測Mark Word中存儲的線程id(ThreadId)是否和當前ThreadId一致
- 如果相同, 不需要再次獲取鎖, 可以進入同步代碼塊進行邏輯操作
- 如果不相同, 說明存在其它線程獲取了偏向鎖, 需要撤銷偏向鎖, 進行鎖升級到輕量級鎖
偏向鎖釋放流程
因爲鎖只能單向升級, 不能降級, 這裏偏向鎖的釋放不是將對象恢復爲無鎖狀態, 而是CAS失敗時, 將鎖升級爲輕量級鎖, 對原持有偏向鎖的線程進行撤銷時, 存在下面的情況
- 原持有偏向鎖的線程離開了臨界區, 這個時候會將對象頭(Header) 設置爲無鎖狀態,其它線程可以通過CAS進行鎖的獲取
- 原持有偏向鎖的線程還在臨界區內, 即代碼塊還沒有執行完, 會將原持有偏向鎖的線程升級爲輕量級鎖繼續執行代碼塊
3.3.3 輕量級鎖獲取/釋放邏輯
在進行輕量級鎖獲取/釋放邏輯講解前, 請先看下下面流程圖, 我們將以下面的流程圖來進行講解
輕量級鎖獲取流程
輕量級鎖獲取是通過自旋鎖方式實現的。自旋 , 是指當其它線程進行鎖競爭時, 這個線程不會立即進入阻塞狀態, 而是原地循環等待, 在原持有鎖的線程釋放鎖之後,這個線程會立即獲取鎖。
自旋的過程會消耗CPU, 自旋需要考慮下面情況
-
輕量級鎖適合同步代碼塊處理時間很短的場景
-
需要控制自旋次數, 避免自旋時間過長, JDK6引入了自適應自旋鎖概念, 它會根據前一次自旋時間來決定本次是否需要自旋, 避免資源浪費
升級流程如下:
-
線程在自己的棧針中創建鎖記錄對象(LockRecord)
-
將鎖對象頭中MarkWord記錄,複製到創建的LockRecord中
-
將鎖對象的Owner指針指向鎖對象
-
將鎖對象的對象頭Mark Word 替換爲指向鎖記錄的指針
輕量級鎖釋放流程
輕量級鎖釋放邏輯是獲取鎖的逆邏輯, 通過CAS操作把線程棧中的Lock Record替換到Mark Word中, 如果替換成功,表示沒有競爭, 如果失敗表示存在競爭, 需要對鎖進行升級操作, 將鎖升級爲重量級鎖 。
3.3.4 重量級鎖獲取/釋放邏輯
進行重量級鎖講解前, 請先看下下面代碼及其執行情況(javap -v SynchronizedMonitor)
public class SynchronizedMonitor {
public static void main(String[] args) {
synchronized (SynchronizedMonitor.class) {
}
test() ;
}
public static void test() {
System.out.println("Good");
}
}
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class com/ityongman/thread/SynchronizedThread
2: dup
3: astore_1
4: monitorenter
5: aload_1
6: monitorexit
7: goto 15
10: astore_2
11: aload_1
12: monitorexit
13: aload_2
14: athrow
15: invokestatic #3 // Method test:()V
18: return
從字節碼可以看出, 線程進入/退出同步代碼塊是通過 monitorenter/monitorexit 控制, 線程想要執行被synchronized修飾的同步方法或者同步代碼塊, 必須要先獲得監視器對象(Monitor), 而每個Java對象都有一個Monitor和它關聯(詳細內容可以查看3.2節)
- monitorenter, 表示獲取一個對象的監視器(monitor)
- monitorexit, 表示釋放監視器對象(monitor)的所有權, 讓處於等待隊列中的線程可以重新競爭監視器鎖
monitor依賴操作系統的MutexLock(互斥鎖)來實現的, 線程被阻塞後會進入內核調用狀態,這個操作會導致用戶態和內核態相互切換, 而這個操作很影響性能。下面是重量級鎖加鎖流程
任意線程對臨界資源的訪問, 首先要獲取lock的監視器對象, 如果獲取對象失敗, 線程會進入同步隊列, 線程狀態變爲(BLOCKED), 當持有監視器鎖的線程釋放了鎖, 會喚醒同步隊列中阻塞的線程, 重新嘗試獲取鎖資源。
NOTE: 線程獲取鎖實際獲取的是臨界資源監視器(monitor)鎖, Object中有wait() 、notify()、notifyAll()方法釋放鎖資源或通知其它線程重新競爭鎖資源, 這些方法的調用必須子synchronized修改的同步範圍內, 不然會拋下異常IllegalMonitorStateException, 意思是因爲沒有同步,線程對對象鎖的狀態是不確定的,不能調用這些方法 。
//一、拋出異常
public class SynchronizedThread extends Thread{
public static void main(String[] args) throws InterruptedException {
synchronized (SynchronizedThread.class) {
}
SynchronizedThread.class.wait();
}
}
//二、無異常, 正常執行
public class SynchronizedThread extends Thread{
public static void main(String[] args) throws InterruptedException {
synchronized (SynchronizedThread.class) {
SynchronizedThread.class.wait();
}
}
}
Exception in thread "main" java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at com.ityongman.thread.SynchronizedThread.main(SynchronizedThread.java:13)
3.4 鎖的優缺點對比
鎖 | 優點 | 缺點 | 適用場景 |
---|---|---|---|
偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執行非同步方法比僅存在納秒級的差距 | 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 | 適用於只有一個線程訪問同步塊場景 |
輕量級鎖 | 競爭的線程不會阻塞,提高了程序的響應速度 | 如果始終得不到鎖競爭的線程使用自旋會消耗CPU | 追求響應時間,同步塊執行速度非常快 |
重量級鎖 | 線程競爭不使用自旋,不會消耗CPU | 線程阻塞,響應時間緩慢 | 追求吞吐量, 同步塊執行速度較長 |
參考信息:
- http://ifeve.com/java-synchronized/