Synchronized詳解、同步互斥自旋鎖分析及MonitorJVM底層實現(xiàn)原理
狀態(tài)對象
如果一個對象有被修改的成員變量 被稱為有狀態(tài)的對象相反如果沒有可被修改的成員變量 稱為無狀態(tài)的對象。
示例:
public class MyThreadTest {
public static void main(String[] args) {
Runnable r = new MyThread();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
}
}
class MyThread implements Runnable {
/**
* 如果一個對象有被修改的成員變量 被稱為有狀態(tài)的對象
* 相反如果沒有可被修改的成員變量 稱為無狀態(tài)的對象
*
* 由于兩個線程同時訪問有狀態(tài)的對象 當一個線程x++完
* 此時另外一個線程又將x變成0 此時就會輸出兩次0
*/
int x;
@SneakyThrows
@Override
public void run() {
x = 0;
while (true) {
System.out.println("result: " + x++);
Thread.sleep((long) Math.random() * 1000);
if (x == 30) {
break;
}
}
}
}
/*
result: 0
result: 0
result: 1
result: 2
result: 3
result: 4
result: 5
result: 6
result: 7
result: 8
result: 9
....
*/
示例2:
/**
* 類鎖和對象鎖互相不干擾 線程可以獲取對象鎖不影響其他的線程獲取類鎖 反之亦然
*/
public class MyThreadTest2 {
public static void main(String[] args) {
MyClass myClass = new MyClass();
MyClass myClass2 = new MyClass();
Thread t1 = new Thread1(myClass);
Thread t2 = new Thread2(myClass);
t1.start();
try {
System.out.println("name: "+Thread.currentThread().getName());
Thread.sleep(700);//睡眠main線程
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
class MyClass {
public synchronized void hello() {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello");
}
public synchronized void world() {
System.out.println("world");
}
}
class Thread1 extends Thread {
private MyClass myClass;
public Thread1(MyClass myClass) {
this.myClass = myClass;
}
@Override
public void run() {
myClass.hello();
}
}
class Thread2 extends Thread {
private MyClass myClass;
public Thread2(MyClass myClass) {
this.myClass = myClass;
}
@Override
public void run() {
myClass.world();
}
}
結論:每個實例對象都有一個唯一的Monitor(鎖)。
synchronized修飾代碼塊
當我們用synchronized修飾代碼塊時字節(jié)碼層面上是通過monitorenter和monitorexit指令來實現(xiàn)的鎖的獲取與釋放動作。
/**
* 當我們使用synchronized關鍵字來修飾代碼塊時,
* 字節(jié)碼層面上是通過monitorenter和monitorexit指令來實現(xiàn)的鎖的獲取與釋放動作。
* monitorenter跟monitorexit 是一對多的關系
*
* 當線程進入到monitorenter指令后,線程將會持有Monitor對象,執(zhí)行monitorexit指令,線程將會釋放Monitor對象
*/
public class MyTest1 {
private Object object = new Object();
public void method() {
int i = 1;
/*
此處的不是只能鎖object 所有的都可以
此處synchronized 它會嘗試去獲取該對象的鎖 有執(zhí)行無則阻塞
*/
synchronized (object) {
System.out.println("hello world!");
//當應用主動拋出異常此時字節(jié)碼 會直接執(zhí)行并且直接執(zhí)行monitorexit解鎖
throw new RuntimeException();
}
}
public void method2() {
synchronized (object) {
System.out.println("welcome");
}
}
}
synchronized代碼塊修飾多個成員對象和this對象
結論:synchronized代碼塊鎖定多個成員對象 和this對象 此時成員對象和this對象之間是互不影響的,只有當前代碼塊鎖定的是同一個對象時才會等待。
注意: 成員屬性對象加鎖,若該類屬于單例,那么該屬性值全局并發(fā)修改始終以最新的值為主(volatile 關鍵字就是用來輔助線程讀取最新的值 ),例如 A B C線程 線程修改(每次+1)某類的 sum =0 屬性值 A最先修改為0+1 = 1 后續(xù)B接著修改就會是1+1 =2 以此類推 如果想讓每個線程訪問都是默認值0 需要使用Spring 的scope 的protype作用域 或者ThreadLocal 或者將其放置在方法中。
/**
* synchronized代碼塊 鎖定多個成員對象 和this對象 此時成員對象和this對象互不影響
*/
public class Test {
public static void main(String[] args) {
MyClass myClass = new MyClass();
Thread t1 = new Thread1(myClass);
Thread t2 = new Thread2(myClass);
Thread t3 = new Thread3(myClass);
t3.start();//5000
t1.start();//4000
try {
System.out.println("name: " + Thread.currentThread().getName());
Thread.sleep(700);//睡眠main線程
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start(); //t1 t2 t3
}
}
class MyClass {
final Object o1 = new Object();
final Object o2 = new Object();
public void hello() {
//只鎖o1的對象 由于o1和o2 是不同的對象兩個方法互不影響
synchronized (o1) {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("hello");
}
public void world() {
//只鎖o2的對象
synchronized (o2) {
System.out.println("world");
}
}
public synchronized void test() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test");
}
}
class Thread1 extends Thread {
private MyClass myClass;
public Thread1(MyClass myClass) {
this.myClass = myClass;
}
@Override
public void run() {
myClass.hello();
}
}
class Thread2 extends Thread {
private MyClass myClass;
public Thread2(MyClass myClass) {
this.myClass = myClass;
}
@Override
public void run() {
myClass.world();
}
}
class Thread3 extends Thread {
private MyClass myClass;
public Thread3(MyClass myClass) {
this.myClass = myClass;
}
@Override
public void run() {
myClass.test();
}
}
輸出:
name: main
world
hello
test
/*
0 iconst_1
1 istore_1
2 aload_0
3 getfield #3 <com/example/demo/com/concurrecy/concurrency3/MyTest1.object>
6 dup
7 astore_2
8 monitorenter
9 getstatic #4 <java/lang/System.out>
12 ldc #5 <hello world!>
14 invokevirtual #6 <java/io/PrintStream.println>
17 aload_2
18 monitorexit
19 goto 27 (+8)
22 astore_3
23 aload_2
24 monitorexit //22-26為了保證拋出異常也能釋放鎖
25 aload_3
26 athrow //IO流輸出 存在這種異常情況發(fā)生
27 return
*/
/*
當加了throw new RuntimeException();
code 字節(jié)碼生成的助記符 只生成一個monitorexit
0 iconst_1
1 istore_1
2 aload_0
3 getfield #3 <com/example/demo/com/concurrecy/concurrency3/MyTest1.object>
6 dup
7 astore_2
8 monitorenter
9 getstatic #4 <java/lang/System.out>
12 ldc #5 <hello world!>
14 invokevirtual #6 <java/io/PrintStream.println>
17 new #7 <java/lang/RuntimeException>
20 dup
21 invokespecial #8 <java/lang/RuntimeException.<init>>
24 athrow
25 astore_3
26 aload_2
27 monitorexit//此時在方法體中拋出了異常 異常結束 直接釋放了鎖
28 aload_3
29 athrow //表示該方法一定會以異常結束
*/
而其對應的標識符如下:
此時就沒有通過monitorebter和moniterexit 來獲取鎖而是通過ACC_SYNCHRONIZED標識符來嘗試獲取鎖synchronized修飾靜態(tài)方法。
當synchronized修飾靜態(tài)方法其實跟修飾成員方法一樣 只不過方法標識符多了個ACC_STATIC,并且其鎖的是類鎖。
/**
* 當synchronized修飾靜態(tài)方法其實跟修飾成員方法一樣 只不過方法標識符多了個ACC_STATIC
* 其次鎖的是 類鎖
*/
public class MyTest3 {
/**
* static靜態(tài)方法不存在this局部變量
* 原因直接類名.就能調用
*/
public static synchronized void method() {
System.out.println("hello world!");
}
}
/*
0 getstatic #2 <java/lang/System.out>
3 ldc #3 <hello world!>
5 invokevirtual #4 <java/io/PrintStream.println>
8 return
*/
Monitor設計的概念
互斥與同步定義
關于“互斥”和“同步”的概念
- 答案很清楚了,互斥就是線程A訪問了一組數(shù)據(jù),線程BCD就不能同時訪問這些數(shù)據(jù),直到A停止訪問了
- 同步就是ABCD這些線程要約定一個執(zhí)行的協(xié)調順序,比如D要執(zhí)行,B和C必須都得做完,而B和C要開始,A必須先得做完。
synchronized底層原理
JVM中的同步是基于進入與退出監(jiān)視器對象(管程對象)(Monitor)來實現(xiàn)的,每個對象實例都會有一個Monitor對象(每個Class生成時都會有且只有一個Monitor對象(鎖) ), Monitor對象會和Java對象一同創(chuàng)建并銷毀。Monitor對象是由C++來實現(xiàn)的。
當多個線程同時訪問一段同步代碼時,這些線程會被放到一個EntryList集合當中,處于阻塞狀態(tài)(未獲取對象鎖 要區(qū)別WaitSet)的線程都會被放到該列表當中。 接下來,當線程獲取到對象的Monito時,Monitor是依賴于底層操作系統(tǒng)的mutex lock(互斥鎖)來實現(xiàn)互斥的,線程獲取mutex成功。則會持有該mutex,這時其他線程就無法獲取到該mutex.。
如果線程調用了wait方法(意思的調用wait方法才會進入WaitSet 競爭monitor時是和entryList 公平競爭),那么該線程就會釋放掉所持有的mutex, 并且該線程會進入到WaitSet集合(等待集合)中,等待下一次被其他該對象鎖線程調用notify/notifyAll喚醒(此處注意如果在WaitSet中被喚醒的線程沒有競爭到鎖該線程會進入entryList阻塞集合)。如果當前線程順利執(zhí)行完畢方法。那么它也會釋放掉所持有的mutex。
用戶態(tài)和內核態(tài)資源調度
總結一下:同步鎖在這種實現(xiàn)方式當中,因為Monitor是依賴底層的操作系統(tǒng)實現(xiàn),這樣就存在用戶態(tài)(如程序執(zhí)行業(yè)務代碼在用戶端)與內核態(tài)(Monitor是依賴于底層操作系統(tǒng) 此時阻塞就是內核執(zhí)行)之間的切換,所以會增加性能開銷。 采用自旋作為回退機制當線程自旋時還是用戶態(tài)占用的是CPU資源==(自旋太久也會造成CUP資源的浪費) 當自旋時間超過預期值還是會進入內核態(tài)。
通過對象互斥鎖的概念來保證共享數(shù)據(jù)操作的完整性。每個對象都對應與一個可稱為【互斥鎖】的標記,這個標記用于保證在任何時刻,只能有一個線程訪問該對象。
存在問題
那些處于EntryList與WaitSet中的線程均處于阻塞狀態(tài)(兩個集合都屬于Monitor對象的成員變量),阻塞操作是由操作系統(tǒng)來完成的,在Linux下是通過pthread_mutex_lock函數(shù)實現(xiàn)的。 線程被阻塞后便會進入到內核調度狀態(tài),這會導致系統(tǒng)在用戶態(tài)與內核態(tài)之間來回切換,嚴重影響鎖的性能
解決方案
解決上述問題的辦法便是自旋(Spin)。其原理是:當發(fā)生對Monitor的爭用時,若Owner(擁有線程或BasicLock指針)能夠在很短的時間內釋放掉鎖,則哪些正在爭用的線程就可以稍微等待一下(即所謂的自旋),在Owner線程釋放鎖之后,爭用線程可能會立刻獲取到鎖,從而避免了系統(tǒng)阻塞(內核態(tài)).不過,當Owner運行的時間超過了臨界值后。爭用線程自旋一段時間后依然無法獲取到鎖,這時爭用線程則會停止自旋而進入到阻塞狀態(tài)(內核態(tài))。所有總體的思想:先自旋,不成功再進行阻塞,盡量降低阻塞的可能性,這對執(zhí)行時間很短的代碼塊來時有極大的性能提升。顯然自旋在多處理器(多核心)上才有意義。
互斥鎖屬性
PTHREAD_MUTEX_TIMED_NP: 這是省缺值,也就是普通鎖,當一個線程加鎖以后,其余請求鎖的線程將會形成一個等待隊列,并且在解鎖后按照優(yōu)先級獲取到鎖,這種策略可以確保資源分配的公平性。
PTHREAD_MUTEX_RECURSIVE_NP:嵌套鎖.允許一個線程對同一個鎖成功獲取多次,并通過unlock解鎖。如果是不同線程請求,則在加鎖線程解鎖時重寫進行競爭。
PTHREAD_MUTEX_ERRORCHECK_NP:檢錯鎖。如果一個線程請求同一把鎖,則返回EDEADLK,否則與PTHREAD_MUTEX_TIMEDNP類型動作相同,這樣就能保證了當不允許多次加鎖時不會出現(xiàn)最簡單的死鎖。
PTHREAD_MUTEX_ADAPTIVE_NP:適應鎖.動作最簡單的鎖類型,僅僅等待解鎖后重新競爭。
Monitor
JVM中的同步是基于進入與退出監(jiān)視器對象(管程對象)(Monitor)來實現(xiàn)的,每個對象實例都會有一個Monitor對象(每個Class生成對象時都會有且只有一個Monitor對象(鎖)伴生 ), Monitor對象會和Java對象一同創(chuàng)建并銷毀。Monitor對象是由C++來實現(xiàn)的。
Monitor對象是啥?
jdk8u/jdk8u-dev/hotspot: 3b255f489efa src/share/vm/runtime/objectMonitor.hpp
通過OpenJDK翻看JVM底層的一些C++代碼。
點擊進入hpp后綴文件找到如下的方法,ObjectWaiter對當前線程的封裝 底層通過鏈表來記錄。
//截取如下
class ObjectWaiter : public StackObj {
public:
enum TStates { TS_UNDEF, TS_READY, TS_RUN, TS_WAIT, TS_ENTER, TS_CXQ } ;
enum Sorted { PREPEND, APPEND, SORTED } ;
ObjectWaiter * volatile _next;//指向下一個ObjectWaiter
ObjectWaiter * volatile _prev;//指向上游的ObjectWaiter
Thread* _thread;
這么做的好處。
我們可以從一個ObjectWaiter 知道其他ObjectWaiter 的位置可以根據(jù)對應的策略選擇性的喚醒對應的ObjectWaiter 如首位 中間指定等。
當waitset中喚醒的線程沒有獲取到monitor 就會將喚醒的線程放到entryList(也是鏈表格式)當中當entryList當中拿到鎖就將對應線程從entryList中移除。
當沒有遇到wait()方法時直接進入EntryList集合當中。
注意:WaitSet線程只是那些調用了wait()的線程,而EntryList是用來存儲阻塞線程。
Wait JVM底層核心代碼解析。
class ObjectMonitor {
public:
enum {
OM_OK, // no error 沒有錯誤
OM_SYSTEM_ERROR, // operating system error 操作系統(tǒng)錯誤
OM_ILLEGAL_MONITOR_STATE, // IllegalMonitorStateException 異常
OM_INTERRUPTED, // Thread.interrupt()
OM_TIMED_OUT // Object.wait() timed out 超時
};
對應成員變量。
// initialize the monitor, exception the semaphore, all other fields
// 初始化monitor,
// are simple integers or pointers
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;//嵌套鎖 遞歸嵌套
_object = NULL;
_owner = NULL;//擁有線程或BasicLock指針
_WaitSet = NULL;//wait等待集合
_WaitSetLock = 0 ; //自旋鎖標識字段 保護等待隊列
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //阻塞集合
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
如下文檔注釋。
protected:
ObjectWaiter * volatile _WaitSet; // LL of threads wait()ing on the monitor
//Monitor上的所有線程等待()集合
protected:
ObjectWaiter * volatile _EntryList ; // Threads blocked on entry or reentry.
//線程在進入或返回時被阻塞。
protected: // protected for jvmtiRawMonitor
void * volatile _owner; // pointer to owning thread OR BasicLock
//指向擁有線程或BasicLock的指針
由JVM底層C++代碼和文檔注釋我們可知_WaitSet和_EntryList 其實是其Monitor對應的的成員變量 初始值都為NULL。
在objectMonitor.cpp文件當中如wait方法實際對應與java Object基類當中的wait(0)所對應的方法。
// Wait/Notify/NotifyAll
//
// Note: a subset of changes to ObjectMonitor::wait()
// will need to be replicated in complete_exit above
void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) {
.....
ObjectWaiter node(Self);//被包裝的線程節(jié)點
node.TState = ObjectWaiter::TS_WAIT ;
Self->_ParkEvent->reset() ;
OrderAccess::fence(); // ST into Event; membar ; LD interrupted-flag
// Enter the waiting queue, which is a circular doubly linked list in this case
//輸入等待隊列,在本例中是一個循環(huán)的雙鏈接列表
// but it could be a priority queue or any data structure.
//但它可以是優(yōu)先級隊列或任何數(shù)據(jù)結構。(鏈表優(yōu)勢)
// _WaitSetLock protects the wait queue. Normally the wait queue is accessed only
//_WaitSetLock保護等待隊列。通常只訪問等待隊列
// by the the owner of the monitor *except* in the case where park()
//由監(jiān)視器的所有者*except*在park()的情況下
// returns because of a timeout of interrupt. Contention is exceptionally rare
//由于中斷超時而返回。爭論異常罕見
// so we use a simple spin-lock instead of a heavier-weight blocking lock.
//所以我們使用了一個簡單的自旋鎖,而不是一個更重的重量級鎖。
Thread::SpinAcquire (&_WaitSetLock, "WaitSet - add") ;//自旋捕獲 鎖
AddWaiter (&node) ;//用來更換指針引用
......
exit (true, Self) ; // exit the monitor 退出monitor
更換內容如下:
inline void ObjectMonitor::AddWaiter(ObjectWaiter* node) {
assert(node != NULL, "should not dequeue NULL node");
assert(node->_prev == NULL, "node already in list");
assert(node->_next == NULL, "node already in list");
// put node at end of queue (circular doubly linked list)
if (_WaitSet == NULL) {
_WaitSet = node;
node->_prev = node;
node->_next = node;
} else {
ObjectWaiter* head = _WaitSet ;
ObjectWaiter* tail = head->_prev;
assert(tail->_next == head, "invariant check");
tail->_next = node;
head->_prev = node;
node->_next = head;
node->_prev = tail;
}
}
在這我們看出當調用了waitSet方法時底層C++時,先進行SpinAcquire (自旋捕獲)嘗試獲取鎖,沒獲取到則將對應線程添加到waitSet當中以鏈表的形式,當完成上述操作時exit monitor。
notify底層核心代碼解析
void ObjectMonitor::notify(TRAPS) {
CHECK_OWNER();
if (_WaitSet == NULL) {//_WaitSet 為null 直接返回
TEVENT (Empty-Notify) ;
return ;
}
....
Thread::SpinAcquire (&_WaitSetLock, "WaitSet - notify") ;
//DequeueWaiter 根據(jù)不同的調度策略獲取waitSet集合鏈表中目標線程
ObjectWaiter * iterator = DequeueWaiter() ;
.....
if (Policy == 0) { // prepend to EntryList
if (List == NULL) {
iterator->_next = iterator->_prev = NULL ;
_EntryList = iterator ;
} else {
List->_prev = iterator ;
iterator->_next = List ;
iterator->_prev = NULL ;
_EntryList = iterator ; //此時如果目標線程未獲取到monitor則放入ENtryList當中
}
總結:notify底層會先根據(jù)不同調度策略獲取waitSet集合鏈表中目標線程,此時如果目標線程未獲取到monitor則放入ENtryList當中。
notifyAll底層核心代碼解析
void ObjectMonitor::notifyAll(TRAPS) {
CHECK_OWNER();
ObjectWaiter* iterator;
if (_WaitSet == NULL) { //WaitSet null 直接返回
TEVENT (Empty-NotifyAll) ;
return ;
}
DTRACE_MONITOR_PROBE(notifyAll, this, object(), THREAD);
int Policy = Knob_MoveNotifyee ;
int Tally = 0 ;
Thread::SpinAcquire (&_WaitSetLock, "WaitSet - notifyall") ;
for (;;) { //遍歷所有
iterator = DequeueWaiter () ; //拿到對應的WaitSet 全部喚醒
if (iterator == NULL) break ;
TEVENT (NotifyAll - Transfer1) ;
++Tally ;
....
總結:notifyAll底層通過死循環(huán)喚醒WaitSet 所有的ObjectWaiter 目標線程。