本篇文章已授權(quán)微信公眾號 guolin_blog (郭霖)獨(dú)家發(fā)布
0. 序言
- 本篇從示例和理論兩方面講解synchronized關(guān)鍵字,希望對學(xué)習(xí)并發(fā)的你有所幫助。
- 主要內(nèi)容:
- synchronized簡介
- 并發(fā)后果
- 鎖分類
- 對象鎖
- 類鎖(可能你覺得這樣稱呼不合理,稱呼而已,暫可不必計較)
- 查看線程的生命周期
- 多線程訪問同步方法的7種具體情況
- synchronized的性質(zhì)
- 加鎖解鎖的實現(xiàn)原理
- 可重入性質(zhì)的原理
- Java的內(nèi)存模型
- 可見性
- synchronized線程安全的根本原因
- synchronized的缺陷
- 注意點(diǎn)
- 并發(fā)基礎(chǔ)需了解的請?zhí)D(zhuǎn):
http://www.itdecent.cn/p/1adedd2b2727
1. synchronized簡介
- 作用
專業(yè):如果一個對象對多個線程可見,則對該對象變量的所有讀取和寫入都是通過同步方法完成的。
通俗:能夠保證你在同一時刻最多只有一個線程執(zhí)行該段代碼,以達(dá)到保證并發(fā)安全的效果。 - 地位
synchronized是Java的關(guān)鍵字,是最基本的互斥同步手段,是并發(fā)編程必學(xué)內(nèi)容。
2. 并發(fā)后果
- 舉例:
public class Main implements Runnable{
static Main main = new Main();
static int num = 0;
@Override
public void run() {
for (int i = 0 ;i<10000;i++){
num++;
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(main);
Thread thread2 = new Thread(main);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(num);
}
}
11756
20000
11185
10485
- 說明:
① 創(chuàng)建了兩個線程thread1和thread2以及定義了一個變量num
② thread1.start();thread2.start();意思是開啟了thread1和thread2,也就是兩個子線程都開始執(zhí)行run方法。
③ thread1.join();thread2.join();意思是thread1子線程執(zhí)行完再執(zhí)行主線程,thread2子線程執(zhí)行完再執(zhí)行主線程.所以有了這兩句代碼以后,兩個子線程都執(zhí)行完自己的代碼,代碼System.out.println(num);才執(zhí)行。
④ 運(yùn)行結(jié)構(gòu)發(fā)現(xiàn),大多數(shù)時候沒有達(dá)到預(yù)期結(jié)果20000,那原因在哪里呢?是因為num++這操作,首先Cpu要去內(nèi)存中讀數(shù)據(jù),然后賦值+1,然后寫入內(nèi)存,經(jīng)歷三個步驟;假設(shè)num值是9,線程thread1讀取到了9,并且加了1,但是還沒有寫入內(nèi)存,這時候thread2讀取到的內(nèi)存中num的值還是9,所以線程thread1和thread2最后寫到內(nèi)存的值都是10,所以最終num++的結(jié)果比預(yù)期少,我們把這種情況稱為線程不安全。
⑤ 其實就是并發(fā)不能保證內(nèi)存的可見性。
3. 鎖分類
- 對象鎖
- 方法鎖:默認(rèn)鎖對象為this
- 同步代碼塊鎖:this或者自定義鎖對象
- 類鎖
- 靜態(tài)鎖:添加static
- Class對象鎖:Main.class
4. 對象鎖
4.1 同步代碼塊鎖
- 鎖對象this
synchronized(this) {
System.out.println("我是對象鎖的代碼塊形式。我的名字是:"
+ Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "運(yùn)行結(jié)束");
}
①
我是對象鎖的代碼塊形式。我的名字是:Thread-0
②
Thread-0運(yùn)行結(jié)束
我是對象鎖的代碼塊形式。我的名字是:Thread-1
③
Thread-1運(yùn)行結(jié)束
finish
說明:
① 這個時候this指的是誰呢?大家都知道this指代的是當(dāng)前對象,也就是Main的實例對象。
② 雖然創(chuàng)建了兩個線程,但是Runnable的實例對象從來沒有變過,也就是this在這里是唯一的,所以線程安全。
② 如果這里用的是繼承Thread的方式創(chuàng)建的線程,this就不安全,因為每次創(chuàng)建新的線程,this所指代的內(nèi)容就會發(fā)生變化。
- 自定義鎖對象
public class Main implements Runnable{
Object lock1 = new Object();
Object lock2 = new Object();
static Main instance = new Main();
@Override
public void run() {
synchronized(lock1) {
System.out.println("我是lock1部分,我叫"
+ Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " lock1部分運(yùn)行結(jié)束"));
}
synchronized(lock2) {
System.out.println("我是lock2部分,我叫:"
+ Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " lock2部分運(yùn)行結(jié)束"));
}
}
public static void main(String[] args) {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
while(thread1.isAlive()||thread2.isAlive()){
}
System.out.println("finish");
}
}
運(yùn)行結(jié)果:
①
我是lock1部分,我叫Thread-0
②
Thread-0 lock1部分運(yùn)行結(jié)束
我是lock2部分,我叫:Thread-0
我是lock1部分,我叫Thread-1
③
Thread-0 lock2部分運(yùn)行結(jié)束
Thread-1 lock1部分運(yùn)行結(jié)束
我是lock2部分,我叫:Thread-1
④
Thread-1 lock2部分運(yùn)行結(jié)束
finish
說明:
① lock1鎖被Thread1釋放后,Thread2才拿到lock1的鎖。
② lock2鎖被Thread1釋放后,Thread2才拿到lock2的鎖。
③ 試想鎖的內(nèi)容都是lock1,那Thread1的執(zhí)行完兩個代碼塊的內(nèi)容后,Thread2才會執(zhí)行第一個代碼塊,運(yùn)行結(jié)果會是:
①
我是lock1部分,我叫Thread-0
②
Thread-0 lock1部分運(yùn)行結(jié)束
我是lock2部分,我叫:Thread-0
③
Thread-0 lock2部分運(yùn)行結(jié)束
我是lock1部分,我叫Thread-1
④
Thread-1 lock1部分運(yùn)行結(jié)束
我是lock2部分,我叫:Thread-1
⑤
Thread-1 lock2部分運(yùn)行結(jié)束
finish
4.2 方法鎖
- 舉例說明
public class Main implements Runnable{
static Main instance = new Main();
@Override
public void run() {
method();
}
private synchronized void method() {
System.out.println("方法鎖,我的名字是"+Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"運(yùn)行結(jié)束");
}
public static void main(String[] args) {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
while(thread1.isAlive()||thread2.isAlive()){
}
System.out.println("finish");
}
}
方法鎖,我的名字是Thread-0
Thread-0運(yùn)行結(jié)束
方法鎖,我的名字是Thread-1
Thread-1運(yùn)行結(jié)束
finish
說明:
① 我們給method方法添加了關(guān)鍵字synchronized,線程安全,同一時刻只有一個線程訪問這個方法。
② 方法鎖的默認(rèn)鎖對象是this。
5. 類鎖
5.1 Class對象鎖(鎖對象是類名.class)
public class Main implements Runnable{
static Main instance1 = new Main();
static Main instance2 = new Main();
@Override
public void run() {
synchronized(Main.class) {
System.out.println("我是類鎖的代碼塊形式。我的名字是:"
+ Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "運(yùn)行結(jié)束");
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(instance1);
Thread thread2 = new Thread(instance2);
thread1.start();
thread2.start();
while(thread1.isAlive()||thread2.isAlive()){
}
System.out.println("finish");
}
}
運(yùn)行結(jié)果:
①
我是類鎖的代碼塊形式。我的名字是:Thread-0
②
Thread-0運(yùn)行結(jié)束
我是類鎖的代碼塊形式。我的名字是:Thread-1
③
Thread-1運(yùn)行結(jié)束
finish
說明:
① 發(fā)現(xiàn)添加關(guān)鍵字synchronized后,thread1執(zhí)行完run方法以后,thread2才會執(zhí)行,線程安全。
② 只要鎖內(nèi)容唯一,線程就安全。上面的示例的鎖內(nèi)容是Main.class,Main.class始終不變,當(dāng)jvm加載一個類時就會為這個類創(chuàng)建一個Class對象。而Main.class就是這個Class對象。
③ Java類可能會有很多個對象,但是Class對象只有一個。不過Class對象其實也是存放在堆中的實例對象,只不過比new出來的對象特殊一點(diǎn),是jvm加載類時所創(chuàng)建的。所以這個Runnable對象不管new多少新的實例傳入不同的Thread中,Class對象也只有一個,作為鎖的對象,線程安全。
5.2 靜態(tài)鎖(synchronized添加在static方法上)
public class Main implements Runnable{
static Main instance1 = new Main();
static Main instance2 = new Main();
@Override
public void run() {
method();
}
private static synchronized void method() {
System.out.println("我是類鎖的代碼塊形式。我的名字是:"
+ Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "運(yùn)行結(jié)束");
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(instance1);
Thread thread2 = new Thread(instance2);
thread1.start();
thread2.start();
while(thread1.isAlive()||thread2.isAlive()){
}
System.out.println("finish");
}
}
①
我是靜態(tài)鎖的代碼塊形式。我的名字是:Thread-0
②
Thread-0運(yùn)行結(jié)束
我是靜態(tài)鎖的代碼塊形式。我的名字是:Thread-1
③
Thread-1運(yùn)行結(jié)束
finish
說明:
如果不添加static,那么因為Runnable的實例對象是兩個不同的,所以在訪問synchronized修改的普通方法的時候,線程0和線程1都會同時訪問到。而添加了static之后,就算不同的實例訪問這個方法,那么也只有一個線程可以訪問到。
6. 查看線程的生命周期
這里主要介紹如何通過調(diào)試,查看線程的狀態(tài),在這里介紹RUNNING和BLOCKED。

說明:在斷點(diǎn)的右邊打斷點(diǎn),然后點(diǎn)擊小蟲子按鈕,進(jìn)行Debug調(diào)試模式

說明:選擇All意味著JVM,即所有的線程停下來,選擇Thread意味著當(dāng)前的線程停下來,這里我們選擇All。

說明:選擇Debugger-Frames-Thread0(方框里面可以選擇線程)-選擇Thread(Thread是當(dāng)前線程,Main是主線程)


說明:選擇Thread,右擊鼠標(biāo),選擇計算機(jī)小白色按鈕,在彈出框輸入this.getState(),就可以查看查看線程是RUNNING還是BLOCKED

7. 多線程訪問同步方法的7種具體情況
條件:以下同步方法指的是非static同步方法。
- 兩個線程同時訪問一個對象的同步方法
線程安全,因為有synchronized關(guān)鍵字修飾且是在一個對象中,可以起到同步的作用。 - 兩個線程訪問的是兩個對象的同步方法
當(dāng)鎖對象是this的時候,因為是兩個對象,鎖對象指的內(nèi)容會發(fā)生變化,這時候不安全。
當(dāng)鎖對象是類名.class的時候,盡管是兩個對象,但是Class對象只有一個,這個時候安全。 - 兩個線程訪問的是Synchronized的靜態(tài)方法
synchronized+static 不管在幾個對象中,線程都是安全的。 - 同時訪問同步方法和非同步方法
synchronized的作用域是修飾的方法,沒有被修飾的方法不能起到同步的作用。 - 訪問同一個對象的不同的普通同步方法
同步方法的默認(rèn)鎖對象是this,因為是同一個對象,所以線程0先走完兩個方法,然后線程1再執(zhí)行,串行。 - 同時訪問靜態(tài)synchronized和非靜態(tài)synchronized方法
synchronized+static的所對象是Class對象,普通synchronized的鎖對象是this,因為鎖對象不同,所以兩個方法可以并行。 - 方法拋出異常后,釋放鎖
synchronized修飾的方法拋出異常后,鎖會釋放 - 總結(jié):
① 一把鎖只能同時被一個線程獲取,沒有拿到鎖的線程 必須等待。
② 每個實例都對應(yīng)有自己的一把鎖,不同實例之間互不影響;例外:鎖對象是類名.class以及Synchronized修飾的是static方法的時候,所有對象共用統(tǒng)一把類鎖。
③ 無論你是方法正常執(zhí)行完畢或者方法拋出異常,都會釋放鎖。
8. synchronized的性質(zhì)
- 可重入
- 簡介:同一線程的外層函數(shù)獲得鎖之后,內(nèi)層函數(shù)可以直接再次獲取該鎖。
- 好處:可避免死鎖、提升封裝性。
- 粒度(作用域):線程而非調(diào)用。
① 同一個方法是可重入的
public class Main {
int a = 0;
public static void main(String[] args) {
Main main = new Main();
main.method1();
}
private synchronized void method1() {
System.out.println("a="+a);
if (a == 0){
a++;
method1();
}
}
}
a=0
a=1
② 可重入不要求是同一個方法
public class Main {
public static void main(String[] args) {
Main main = new Main();
main.method1();
}
private synchronized void method1() {
System.out.println("我是method1");
method2();
}
private synchronized void method2() {
System.out.println("我是method2");
}
}
我是method1
我是method2
③ 可重入不要求是同一個類中的
public class Main extends SuperClass{
int a = 0;
public static void main(String[] args) {
Main main = new Main();
main.doSomting();
}
public synchronized void doSomting(){
System.out.println("我是子類方法");
super.doSomthing();
}
}
class SuperClass{
public synchronized void doSomthing(){
System.out.println("我是父類方法");
}
}
我是子類方法
我是父類方法
- 不可中斷
一旦這個鎖已經(jīng)被別人獲得,如果還想獲得,只能選擇等待或者阻塞,直到別的線程釋放這個鎖。如果別人永遠(yuǎn)不釋放鎖,那么只能永遠(yuǎn)等下去。
9. 加鎖解鎖的實現(xiàn)原理
- 代碼Main.java:
public class Main {
public static synchronized void m() {
}
public static void main(String[] args) {
synchronized (Main.class){
}
m();
}
}
- javac Main.class并執(zhí)行javap -v Main.class 截取部分信息
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class com/smartisan/Synchronized
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 m:()V
18: return
public static synchronized void m();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 14: 0
}
SourceFile: "Synchronized.java"
說明:以上是代碼的字節(jié)碼信息,不難看出:
- 同步在形式上有兩種方式完成:
① 同步塊的實現(xiàn)使用了monitorenter和moniterexit指令
Synchronization of sequences of instructions is typically used to encode the
synchronizedblock of the Java programming language. The Java Virtual Machine supplies the monitorenter and monitorexit instructions to support such language constructs. Proper implementation ofsynchronizedblocks requires cooperation from a compiler targeting the Java Virtual Machine (§3.14).
同步指令集通常用來實現(xiàn)同步代碼塊。Java虛擬機(jī)的指令集中有monitorenter和monitorexit兩條指令來支持synchronized關(guān)鍵字的語義。正確實現(xiàn)synchronized關(guān)鍵字需要Javac編譯器與Java虛擬器兩者共同協(xié)作支持。
② 同步方法依靠修飾符ACC_SYNCHRONIZED完成。
Method-level synchronization is performed implicitly, as part of method invocation and return (§2.11.8). A
synchronizedmethod is distinguished in the run-time constant pool'smethod_infostructure (§4.6) by theACC_SYNCHRONIZEDflag, which is checked by the method invocation instructions. When invoking a method for whichACC_SYNCHRONIZEDis set, the executing thread enters a monitor, invokes the method itself, and exits the monitor whether the method invocation completes normally or abruptly. During the time the executing thread owns the monitor, no other thread may enter it. If an exception is thrown during invocation of thesynchronizedmethod and thesynchronizedmethod does not handle the exception, the monitor for the method is automatically exited before the exception is rethrown out of thesynchronizedmethod.
方法級的同步是隱式的,即無須通過字節(jié)碼指令來控制,它實現(xiàn)在方法調(diào)用和返回操作之中。虛擬機(jī)可以從方法常量池的方法表結(jié)構(gòu)中的ACC_SYNCHRONIZED訪問標(biāo)記得知一個方法是否聲明為同步方法。當(dāng)方法調(diào)用時,調(diào)用指令將會檢查方法的ACC_SYNCHRONIZED訪問標(biāo)志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程就要求先成功持有監(jiān)視器(monitor),然后才能執(zhí)行方法,最后當(dāng)方法完成(無論是正常完成還是非正常完成)時釋放監(jiān)視器(monitor)。在方法執(zhí)行期間,執(zhí)行線程持有了監(jiān)視器(monitor),其他任何線程都無法再獲取到同一個監(jiān)視器(monitor)。如果一個同步方法執(zhí)行期間拋出了異常,并且在方法內(nèi)部無法處理此異常,那么這個同步方法所持有的監(jiān)視器(monitor)將在異常拋到同步方法之外時自動釋放。
- 同步本質(zhì)上都是通過監(jiān)視器(monitor)提供支持
任意一個對象都擁有自己的監(jiān)視器(monitor),當(dāng)這個對象由同步代碼塊或者這個對象的同步方法調(diào)用時,執(zhí)行方法的線程必須先獲取到該對象的監(jiān)視器才能進(jìn)入同步代碼塊或者同步方法,而沒有獲取到監(jiān)視器的線程將會被阻塞在同步代碼塊和同步方法的入口處,進(jìn)入BLOCKED狀態(tài)。
- 為什么會有兩個monitorexit?
① 不管你的代碼是否會拋出異常,都會有兩個monitorexit:一個monitorexit是正常退出同步時執(zhí)行,一個monitorexit是拋出異常時monitorenter和monitorexit指令依然可以正確配對執(zhí)行。
② 編譯器會自動產(chǎn)生一個異常處理器,這個異常處理器聲明可處理所有的異常,它的目的就是用來執(zhí)行monitorexit指令。
10. 可重入性質(zhì)的原理
既然synchronized的實現(xiàn)是通過監(jiān)視器(mJava并發(fā)之synchronized深度解析
onitor)提供支持的,那么我們分別看下monitorenter和monitorexit,理解了兩者,我們便可以理解可重入性質(zhì)的原理:
monitorenter
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
- If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
- If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
- If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.
每個對象都與一個監(jiān)視器相關(guān)聯(lián)。只有當(dāng)監(jiān)視器有所有者時,它才會被鎖定。執(zhí)行monitorenter的線程嘗試獲得與鎖對象關(guān)聯(lián)的監(jiān)視器的所有權(quán)時:
- 如果與鎖對象關(guān)聯(lián)的監(jiān)視器的條目計數(shù)為零,則線程將進(jìn)入監(jiān)視器并將其條目計數(shù)設(shè)置為1。此時這個線程是監(jiān)視器的所有者。
- 如果線程已經(jīng)擁有與鎖對象關(guān)聯(lián)的監(jiān)視器,它將重新進(jìn)入監(jiān)視器,并增加其條目計數(shù)。
- 如果一個線程已經(jīng)擁有與鎖對象關(guān)聯(lián)的監(jiān)視器,則其他線程將一直阻塞,直到監(jiān)視器的條目計數(shù)為零,然后再次嘗試獲得所有權(quán)。
monitorexit
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
執(zhí)行monitorexit的線程必須是與鎖對象關(guān)聯(lián)的監(jiān)視器的所有者。
線程會減少與鎖對象關(guān)聯(lián)的監(jiān)視器的條目計數(shù)。如果結(jié)果是條目計數(shù)的值為零,則線程將不再是監(jiān)視器的所有者。之前被阻止進(jìn)入監(jiān)視器的其他線程可嘗試去擁有監(jiān)視器(moniter)
說明:從上述分析不難看出,可重入性質(zhì)依賴的是加鎖次數(shù)計算器。
11. Java的內(nèi)存模型
-
Java的內(nèi)存模型定義了線程之間的抽象關(guān)系:線程之間的共享變量存儲在主內(nèi)存中,每個線程都有一個私有的本地內(nèi)存,本地內(nèi)存中存儲了該線程共享變量的副本。Java的內(nèi)存模型控制線程之間的通信,它決定了一個線程對主存共享變量的寫入何時對另一個線程可見。示意圖如下:
JMM - 線程A與線程B之間若要通信的話,必須要經(jīng)歷以下兩個步驟:
① 線程A把線程A本地內(nèi)存中更新過的共享變量刷新到主內(nèi)存中去。
② 線程B到主內(nèi)存中讀取線程A之前更新過的共享變量。
12. 可見性
可見性,指的是線程之間的可見性,即一個線程修改的狀態(tài)對另一個線程是可見的,也就是一個線程修改的結(jié)果,另一個線程馬上可以看到。所以保證了可見性,就可以保證線程的安全性。
13. synchronized線程安全的根本原因
了解了Java的內(nèi)存模型和線程的可見性,不難得出synchronized的線程安全的根本原因:加鎖保證了只有一個線程可以操作主存中的共享變量:當(dāng)本地內(nèi)存中的共享變量副本發(fā)生變化后,解鎖之前會把本地內(nèi)存中共享變量的值刷新到主存。而當(dāng)其他線程獲取到鎖,會去主內(nèi)存中讀取該共享變量的新值。
14. synchronized的缺陷
- 效率低:鎖的釋放情況少、試圖獲得鎖時不能設(shè)定超時、不能中斷一個正在試圖獲得鎖的線程
- 不夠靈活:加鎖和釋放的時機(jī)單一,每個所僅有單一的條件(某個對象),可能是不夠的
- 無法知道是否成功獲取到鎖
15. 注意點(diǎn)
- 鎖對象不能為空
- 作用域不宜過大
- 避免死鎖
16. 后續(xù)
如果大家喜歡這篇文章,歡迎點(diǎn)贊;
如果想看更多 并發(fā) 方面的技術(shù),歡迎關(guān)注!
