目錄
前言
毫無疑問,synchronized是我們用過的第一個併發關鍵字,很多博文都在講解這個技術。不過大多數講解還停留在對synchronized的使用層面,其底層的很多原理和優化,很多人可能並不知曉。因此本文將通過對synchronized的大量C源碼分析,讓大家對他的瞭解更加透徹點。
本篇將從爲什麼要引入synchronized,常見的使用方式,存在的問題以及優化部分這四個方面描述,話不多說,開始表演。
synchronized的常見使用方式
修飾代碼塊(同步代碼塊)
synchronized (object) {
//具體代碼
}
修飾方法
synchronized void test(){
//具體代碼
}
synchronized不能繼承?(插曲)
父類A:
public class A {
synchronized void test() throws Exception {
try {
System.out.println("main 下一步 sleep begin threadName="
+ Thread.currentThread().getName() + " time="
+ System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("main 下一步 sleep end threadName="
+ Thread.currentThread().getName() + " time="
+ System.currentTimeMillis());
} catch (Exception e) {
e.printStackTrace();
}
}
}
子類B:(未重寫test方法)
public class B extends A {
}
子類C:(重寫test方法)
public class C extends A {
@Override
void test() throws Exception{
try {
System.out.println("sub 下一步 sleep begin threadName="
+ Thread.currentThread().getName() + " time="
+ System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("sub 下一步 sleep end threadName="
+ Thread.currentThread().getName() + " time="
+ System.currentTimeMillis());
} catch (Exception e) {
e.printStackTrace();
}
}
}
線程A:
public class ThreadA extends Thread {
private A a;
public void setter (A a) {
this.a = a;
}
@Override
public void run() {
try{
a.test();
}catch (Exception e){
}
}
}
線程B:
public class ThreadB extends Thread {
private B b;
public void setB(B b){
this.b=b;
}
@Override
public void run() {
try{
b.test();
}catch (Exception e){
}
}
}
線程C:
public class ThreadC extends Thread{
private C c;
public void setC(C c){
this.c=c;
}
@Override
public void run() {
try{
c.test();
}catch (Exception e){
}
}
}
測試類test:
public class test {
public static void main(String[] args) throws Exception {
A a = new A();
ThreadA A1 = new ThreadA();
A1.setter(a);
A1.setName("A1");
A1.start();
ThreadA A2 = new ThreadA();
A2.setter(a);
A2.setName("A2");
A2.start();
A1.join();
A2.join();
System.out.println("=============");
B b = new B();
ThreadB B1 = new ThreadB();
B1.setB(b);
B1.setName("B1");
B1.start();
ThreadB B2 = new ThreadB();
B2.setB(b);
B2.setName("B2");
B2.start();
B1.join();
B2.join();
System.out.println("=============");
C c = new C();
ThreadC C1 = new ThreadC();
C1.setName("C1");
C1.setC(c);
C1.start();
ThreadC C2 = new ThreadC();
C2.setName("C2");
C2.setC(c);
C2.start();
C1.join();
C2.join();
}
}
運行結果:
子類B繼承了父類A,但是沒有重寫test方法,ThreadB仍然是同步的。子類C繼承了父類A,也重寫了test方法,但是未明確寫上synchronized,所以這個方法並不是同步方法。只有顯式的寫上synchronized關鍵字,纔是同步方法。
所以synchronized不能繼承這句話有歧義,我們只要記住子類如果想要重寫父類的同步方法,synchronized關鍵字一定要顯示寫出,否則無效。
修飾靜態方法
synchronized static void test(){
//具體代碼
}
修飾類
synchronized (Example2.class) {
//具體代碼
}
Java對象 Mark Word
在JVM中,對象在內存中的佈局分爲三塊區域:對象頭,實例數據和對齊數據,如下圖:
其中Mark Word值在不同鎖狀態
下的展示如下:(重點看線程id,是否爲偏向鎖,鎖標誌位信息)
在64位系統中,Mark Word佔了8個字節,類型指針佔了8個字節,一共是16
個字節。Talk is cheap. Show me the code. 咱來看代碼。
- 我們想要看Java對象的Mark Word,先要加載一個jar包,在pom.xml添加即可。
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
- 新建一個對象A,擁有初始值爲666的變量x。
public class A {
private int x=666;
}
-
新建一個測試類test,這涉及到剛纔加載的jar,我們打印Java對象。
import org.openjdk.jol.info.ClassLayout;
public class test {
public static void main(String[] args) {
A a=new A();
System.out.println( ClassLayout.parseInstance(a).toPrintable());
}
}
- 我們發現對象頭(object header)佔了
12
個字節,爲啥和上面說的16個字節不一樣。
- 其實上是默認開啓了指針壓縮,我們需要關閉指針壓縮,也就是添加
-XX:-UseCompressedOops
配置。
-
再次執行,發現對象頭爲16個字節。
偏向鎖
什麼是偏向鎖
JDK1.6之前鎖爲重量級鎖(待會說,只要知道他和內核交互,消耗資源),1.6之後Java設計人員發現很多情況下並不存在多個線程競爭的關係,所以爲了資源問題引入了無鎖
,偏向鎖
,輕量級鎖
,重量級鎖
的概念。先說偏向鎖,他是偏心,偏袒的意思,這個鎖會偏向於第一個獲取他的線程。
偏向鎖演示
- 創建並啓動一個線程,run方法裏面用了synchronized關鍵字,功能是打印this的Java對象。
public class test {
public static void main(String[] args) {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
synchronized (this){
System.out.println(ClassLayout.parseInstance(this).toPrintable());
}
}
});
thread.start();
}
}
標紅的地方爲000,根據之前Mark Word在不同狀態下的標誌,得此爲無鎖狀態。理論上一個線程使用synchronized關鍵字,應爲偏向鎖。
- 實際上偏向鎖在JDK1.6之後是默認開啓的,但是啓動時間有延遲,所以需要添加參數
-XX:BiasedLockingStartupDelay=0
,讓其在程序啓動時立刻啓動。
- 重新運行下代碼,發現標紅地方101,對比Mark Word在不同狀態下的標誌,得此狀態爲偏向鎖。
偏向鎖原理圖解
- 在線程的run方法中,剛執行到synchronized,會判斷當前對象是否爲偏向鎖和鎖標誌,沒有任何線程執行該對象,我們可以看到是否爲偏向鎖爲0,鎖標誌位01,即無鎖狀態。
- 線程會將自己的id賦值給markword,即將原來的hashcode值改爲線程id,是否是偏向鎖改爲1,表示線程擁有對象鎖,可以執行下面的業務邏輯。
如果synchronized執行完,對象還是偏向鎖狀態;如果線程結束之後,會撤銷偏向鎖,將該對象還原成無鎖狀態。
- 如果同一個線程中又對該對象進行加鎖操作,我們只要對比
對象的線程id
是否與線程id
相同,如果相同即爲線程鎖重入問題。
優勢
加鎖和解鎖不需要額外的消耗,和執行非同步方法相比只有納秒級的差距。
白話翻譯
線程1鎖定對象this,他發現對象爲無鎖狀態,所以將線程id賦值給對象的Mark Word字段,表示對象爲線程1專用,即使他退出了同步代碼,其他線程也不能使用該對象。
同學A去自習教室C,他發現教室無人,所以在門口寫了個名字,表示當前教室有人在使用,這樣即使他出去吃了飯,其他同學也不能使用這個房間。
輕量鎖
什麼是輕量級鎖
在多線程交替同步代碼塊的情況下,線程間沒有競爭,使用輕量級鎖可以避免重量級鎖引入的性能消耗。
輕量級圖解
- 在剛纔偏向鎖的基礎上,如果有另外一個線程也想錯峯使用該資源,通過對比線程id是否相同,Java內存會立刻撤銷偏向鎖(需要等待全局安全點),進行鎖升級的操作。
- 撤銷完輕量級鎖,會在線程1的方法棧中新增一個鎖記錄,對象的Mark Word與鎖記錄交換。
優勢
競爭的線程不會阻塞,提高了程序的響應速度。
白話翻譯
在剛纔偏向鎖的基礎上,另外一個線程也想要獲取資源,所以線程1需要撤銷偏向鎖,升級爲輕量鎖。
同學A在使用自習教室外面寫了自己的名字,所以同學B來也想要使用自習教室,他需要提醒同學A,不能使用重量級鎖,同學A將自習教室門口的名字擦掉,換成了一個書包,裏面是自己的書籍。這樣在同學A不使用自習教室的時候,同學B也能使用自習教室,只需要將自己的書包也掛在外面即可。這樣下次來使用的同學就能知道已經有人佔用了該教室。
重量級鎖
什麼是重量級鎖
當多線程之間發生競爭,Java內存會申請一個Monitor對象來實現。
重量級鎖原理圖解
在剛纔的輕量級鎖的基礎上,線程2也想要申請資源,發現鎖的標誌位爲00,即爲輕量級鎖,所以向內存申請一個Monitor,讓對象的MarkWord指向Monitor地址,並將ower指針指向線程1的地址,線程2放在等待隊列裏面,等線程1指向完畢,釋放鎖資源。
Monitor源碼分析
環境搭建
我們去官網http://openjdk.java.net/
找下open源碼,也可以通過其他途徑下載。源碼是C實現的,可以通過DEV C++工具打開,效果如下圖:
構造函數
我們先看下\hotspot\src\share\vm\runtime\ObjectMonitor.hpp
,以.hpp結尾的文件是導入的一些包和一些聲明,之後可以被.cpp文件導入。
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;//線程重入次數
_object = NULL;//存儲該monitor的對象
_owner = NULL;//標識擁有該monitor的線程
_WaitSet = NULL;//處於wait狀態的線程,會加入到_waitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;//多線程競爭鎖時的單項列表
FreeNext = NULL ;
_EntryList = NULL ;//處於等待鎖lock狀態的線程,會被加入到該列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
鎖競爭的過程
我們先看下\hotspot\src\share\vm\interpreter\interpreterRuntime.cpp
,IRT_ENTRY_NO_ASYNC
即爲鎖競爭過程。
//%note monitor_1
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
if (PrintBiasedLockingStatistics) {
Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
}
Handle h_obj(thread, elem->obj());
assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
"must be NULL or an object");
//是否使用偏向鎖,可加參數進行設置 if (UseBiasedLocking) { //如果可以使用偏向鎖,即進入fast_enter
// Retry fast entry if bias is revoked to avoid unnecessary inflation
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {//如果不可以使用偏向鎖,即進行slow_enter
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
"must be NULL or an object");
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END
slow_enter實際上調用的ObjectMonitor.cpp的enter 方法
void ATTR ObjectMonitor::enter(TRAPS) {
// The following code is ordered to check the most common cases first
// and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
Thread * const Self = THREAD ;
void * cur ;
//通過CAS操作嘗試將monitor的_owner設置爲當前線程
cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
//如果設置不成功,直接返回
if (cur == NULL) {
// Either ASSERT _recursions == 0 or explicitly set _recursions = 0.
assert (_recursions == 0 , "invariant") ;
assert (_owner == Self, "invariant") ;
// CONSIDER: set or assert OwnerIsThread == 1
return ;
}
//如果_owner等於當前線程,重入數_recursions加1,直接返回
if (cur == Self) {
// TODO-FIXME: check for integer overflow! BUGID 6557169.
_recursions ++ ;
return ;
}
//如果當前線程第一次進入該monitor,設置重入數_recursions爲1,_owner爲當前線程,返回
if (Self->is_lock_owned ((address)cur)) {
assert (_recursions == 0, "internal state error");
_recursions = 1 ;
// Commute owner from a thread-specific on-stack BasicLockObject address to
// a full-fledged "Thread *".
_owner = Self ;
OwnerIsThread = 1 ;
return ;
}
//如果未搶到鎖,則進行自旋優化,如果還未獲取鎖,則放入到list裏面
// We've encountered genuine contention.
assert (Self->_Stalled == 0, "invariant") ;
Self->_Stalled = intptr_t(this) ;
// Try one round of spinning *before* enqueueing Self
// and before going through the awkward and expensive state
// transitions. The following spin is strictly optional ...
// Note that if we acquire the monitor from an initial spin
// we forgo posting JVMTI events and firing DTRACE probes.
if (Knob_SpinEarly && TrySpin (Self) > 0) {
assert (_owner == Self , "invariant") ;
assert (_recursions == 0 , "invariant") ;
assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ;
Self->_Stalled = 0 ;
return ;
}
assert (_owner != Self , "invariant") ;
assert (_succ != Self , "invariant") ;
assert (Self->is_Java_thread() , "invariant") ;
JavaThread * jt = (JavaThread *) Self ;
assert (!SafepointSynchronize::is_at_safepoint(), "invariant") ;
assert (jt->thread_state() != _thread_blocked , "invariant") ;
assert (this->object() != NULL , "invariant") ;
assert (_count >= 0, "invariant") ;
// Prevent deflation at STW-time. See deflate_idle_monitors() and is_busy().
// Ensure the object-monitor relationship remains stable while there's contention.
Atomic::inc_ptr(&_count);
EventJavaMonitorEnter event;
{ // Change java thread status to indicate blocked on monitor enter.
JavaThreadBlockedOnMonitorEnterState jtbmes(jt, this);
DTRACE_MONITOR_PROBE(contended__enter, this, object(), jt);
if (JvmtiExport::should_post_monitor_contended_enter()) {
JvmtiExport::post_monitor_contended_enter(jt, this);
}
OSThreadContendState osts(Self->osthread());
ThreadBlockInVM tbivm(jt);
Self->set_current_pending_monitor(this);
// TODO-FIXME: change the following for(;;) loop to straight-line code.
for (;;) {
jt->set_suspend_equivalent();
// cleared by handle_special_suspend_equivalent_condition()
// or java_suspend_self()
EnterI (THREAD) ;
if (!ExitSuspendEquivalent(jt)) break ;
//
// We have acquired the contended monitor, but while we were
// waiting another thread suspended us. We don't want to enter
// the monitor while suspended because that would surprise the
// thread that suspended us.
//
_recursions = 0 ;
_succ = NULL ;
exit (false, Self) ;
jt->java_suspend_self();
}
Self->set_current_pending_monitor(NULL);
}
Atomic::dec_ptr(&_count);
assert (_count >= 0, "invariant") ;
Self->_Stalled = 0 ;
// Must either set _recursions = 0 or ASSERT _recursions == 0.
assert (_recursions == 0 , "invariant") ;
assert (_owner == Self , "invariant") ;
assert (_succ != Self , "invariant") ;
assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ;
// The thread -- now the owner -- is back in vm mode.
// Report the glorious news via TI,DTrace and jvmstat.
// The probe effect is non-trivial. All the reportage occurs
// while we hold the monitor, increasing the length of the critical
// section. Amdahl's parallel speedup law comes vividly into play.
//
// Another option might be to aggregate the events (thread local or
// per-monitor aggregation) and defer reporting until a more opportune
// time -- such as next time some thread encounters contention but has
// yet to acquire the lock. While spinning that thread could
// spinning we could increment JVMStat counters, etc.
DTRACE_MONITOR_PROBE(contended__entered, this, object(), jt);
if (JvmtiExport::should_post_monitor_contended_entered()) {
JvmtiExport::post_monitor_contended_entered(jt, this);
}
if (event.should_commit()) {
event.set_klass(((oop)this->object())->klass());
event.set_previousOwner((TYPE_JAVALANGTHREAD)_previous_owner_tid);
event.set_address((TYPE_ADDRESS)(uintptr_t)(this->object_addr()));
event.commit();
}
if (ObjectMonitor::_sync_ContendedLockAttempts != NULL) {
ObjectMonitor::_sync_ContendedLockAttempts->inc() ;
}
}
白話翻譯
同學A在使用自習教室的時候,同學B在同一時刻也想使用自習教室,那就發生了競爭關係。所以同學B在A運行過程中,加入等待隊列。如果此時同學C也要使用該教室,也會加入等待隊列。等同學A使用結束,同學B和C將競爭自習教室。
自旋優化
自旋優化比較簡單,如果將其他線程加入等待隊列,那之後喚醒並運行線程需要消耗資源,所以設計人員讓其空轉一會,看看線程能不能一會結束了,這樣就不要在加入等待隊列。
白話來說,如果同學A在使用自習教室,同學B可以回宿舍,等A使用結束再來,但是B回宿舍再來的過程需要1個小時,而A只要10分鐘就結束了。所以B可以先不回宿舍,而是在門口等個10分鐘,以防止來回時間的浪費。
結語
唉呀媽呀,終於結束了,累死了。終於將synchronized寫完了,如果有不正確的地方,還需要各位指正。如果覺得寫得還行,麻煩幫我點贊,評論哈。
參考資料
Java中System.out.println()爲何會影響內存可見性
Thread--synchronized不能被繼承?!?!!!