Java併發之synchronized深度解析

Java併發之synchronized深度解析


前言:

本篇從示例和理論兩方面講解synchronized關鍵字,希望對學習併發的你有所幫助。

併發基礎需瞭解的請跳轉:

https://www.jianshu.com/p/1adedd2b2727


正文


synchronized簡介

  • 作用

專業:如果一個對象對多個線程可見,則對該對象變量的所有讀取和寫入都是通過同步方法完成的。

通俗:能夠保證你在同一時刻最多隻有一個線程執行該段代碼,以達到保證併發安全的效果。

  • 地位

synchronized是Java的關鍵字,是最基本的互斥同步手段,是併發編程必學內容。

併發後果

舉例:

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

說明:

  1. 創建了兩個線程thread1和thread2以及定義了一個變量num
  2. thread1.start();thread2.start();意思是開啟了thread1和thread2,也就是兩個子線程都開始執行run方法。
  3. thread1.join();thread2.join();意思是thread1子線程執行完再執行主線程,thread2子線程執行完再執行主線程.所以有了這兩句代碼以後,兩個子線程都執行完自己的代碼,代碼System.out.println(num);才執行。
  4. 運行結構發現,大多數時候沒有達到預期結果20000,那原因在哪裡呢?是因為num++這操作,首先Cpu要去內存中讀數據,然後賦值+1,然後寫入內存,經歷三個步驟;假設num值是9,線程thread1讀取到了9,並且加了1,但是還沒有寫入內存,這時候thread2讀取到的內存中num的值還是9,所以線程thread1和thread2最後寫到內存的值都是10,所以最終num++的結果比預期少,我們把這種情況稱為線程不安全。
  5. 其實就是併發不能保證內存的可見性。

鎖分類

  • 對象鎖
  • 方法鎖:默認鎖對象為this
  • 同步代碼塊鎖:this或者自定義鎖對象
  • 類鎖
  • 靜態鎖:添加static
  • Class對象鎖:Main.class

對象鎖

  • 同步代碼塊鎖

鎖對象this

synchronized(this) {
System.out.println("我是對象鎖的代碼塊形式。我的名字是:"
+ Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "運行結束");
}
運行結果:

我是對象鎖的代碼塊形式。我的名字是:Thread-0

Thread-0運行結束
我是對象鎖的代碼塊形式。我的名字是:Thread-1

Thread-1運行結束
finish

說明:

  1. 這個時候this指的是誰呢?大家都知道this指代的是當前對象,也就是Main的實例對象。
  2. 雖然創建了兩個線程,但是Runnable的實例對象從來沒有變過,也就是this在這裡是唯一的,所以線程安全。
  3. 如果這裡用的是繼承Thread的方式創建的線程,this就不安全,因為每次創建新的線程,this所指代的內容就會發生變化。

自定義鎖對象

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() + "運行結束");
}
synchronized(lock2) {
System.out.println("我是lock2部分,我叫:"
+ Thread.currentThread().getName());
try {
Thread.sleep(3000);

} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "運行結束");
}
}
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");
}
}
運行結果:

我是lock1部分,我叫Thread-0

Thread-0 lock1部分運行結束
我是lock2部分,我叫:Thread-0
我是lock1部分,我叫Thread-1

Thread-0 lock2部分運行結束
Thread-1 lock1部分運行結束
我是lock2部分,我叫:Thread-1

Thread-1 lock2部分運行結束
finish

說明:

  1. lock1鎖被Thread1釋放後,Thread2才拿到lock1的鎖。
  2. lock2鎖被Thread1釋放後,Thread2才拿到lock2的鎖。
  3. 試想鎖的內容都是lock1,那Thread1的執行完兩個代碼塊的內容後,Thread2才會執行第一個代碼塊,運行結果會是:
運行結果:

我是lock1部分,我叫Thread-0

Thread-0 lock1部分運行結束
我是lock2部分,我叫:Thread-0

Thread-0 lock2部分運行結束
我是lock1部分,我叫Thread-1

Thread-1 lock1部分運行結束
我是lock2部分,我叫:Thread-1

Thread-1 lock2部分運行結束
finish
  • 方法鎖

舉例說明

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()+"運行結束");
}
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-1

Thread-0運行結束
方法鎖,我的名字是Thread-1

Thread-1運行結束
finish

說明:

  1. 我們給method方法添加了關鍵字synchronized,線程安全,同一時刻只有一個線程訪問這個方法。
  2. 方法鎖的默認鎖對象是this。

類鎖

  • 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() + "運行結束");
}
}
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");
}
}
運行結果:

我是類鎖的代碼塊形式。我的名字是:Thread-0

Thread-0運行結束
我是類鎖的代碼塊形式。我的名字是:Thread-1

Thread-1運行結束
finish

說明:

  1. 發現添加關鍵字synchronized後,thread1執行完run方法以後,thread2才會執行,線程安全。
  2. 只要鎖內容唯一,線程就安全。上面的示例的鎖內容是Main.class,Main.class始終不變,當jvm加載一個類時就會為這個類創建一個Class對象。而Main.class就是這個Class對象。
  3. Java類可能會有很多個對象,但是Class對象只有一個。不過Class對象其實也是存放在堆中的實例對象,只不過比new出來的對象特殊一點,是jvm加載類時所創建的。所以這個Runnable對象不管new多少新的實例傳入不同的Thread中,Class對象也只有一個,作為鎖的對象,線程安全。
  • 靜態鎖(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() + "運行結束");
}
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");
}
}


我是靜態鎖的代碼塊形式。我的名字是:Thread-0

Thread-0運行結束
我是靜態鎖的代碼塊形式。我的名字是:Thread-1

Thread-1運行結束
finish

說明:

如果不添加static,那麼因為Runnable的實例對象是兩個不同的,所以在訪問synchronized修改的普通方法的時候,線程0和線程1都會同時訪問到。而添加了static之後,就算不同的實例訪問這個方法,那麼也只有一個線程可以訪問到。

查看線程的生命週期

這裡主要介紹如何通過調試,查看線程的狀態,在這裡介紹RUNNING和BLOCKED。


Java併發之synchronized深度解析

打斷點

說明:在斷點的右邊打斷點,然後點擊小蟲子按鈕,進行Debug調試模式


Java併發之synchronized深度解析


斷點選項

說明:選擇All意味著JVM,即所有的線程停下來,選擇Thread意味著當前的線程停下來,這裡我們選擇All。


Java併發之synchronized深度解析

選擇線程

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


Java併發之synchronized深度解析


線程選擇

Java併發之synchronized深度解析

查看狀態

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


Java併發之synchronized深度解析

image.png

多線程訪問同步方法的7種具體情況

條件:以下同步方法指的是非static同步方法。

  • 兩個線程同時訪問一個對象的同步方法
  • 線程安全,因為有synchronized關鍵字修飾且是在一個對象中,可以起到同步的作用。
  • 兩個線程訪問的是兩個對象的同步方法
  • 當鎖對象是this的時候,因為是兩個對象,鎖對象指的內容會發生變化,這時候不安全。
  • 當鎖對象是類名.class的時候,儘管是兩個對象,但是Class對象只有一個,這個時候安全。
  • 兩個線程訪問的是Synchronized的靜態方法
  • synchronized+static 不管在幾個對象中,線程都是安全的。
  • 同時訪問同步方法和非同步方法
  • synchronized的作用域是修飾的方法,沒有被修飾的方法不能起到同步的作用。
  • 訪問同一個對象的不同的普通同步方法
  • 同步方法的默認鎖對象是this,因為是同一個對象,所以線程0先走完兩個方法,然後線程1再執行,串行。
  • 同時訪問靜態synchronized和非靜態synchronized方法
  • synchronized+static的所對象是Class對象,普通synchronized的鎖對象是this,因為鎖對象不同,所以兩個方法可以並行。
  • 方法拋出異常後,釋放鎖
  • synchronized修飾的方法拋出異常後,鎖會釋放

總結:

  1. 一把鎖只能同時被一個線程獲取,沒有拿到鎖的線程 必須等待。
  2. 每個實例都對應有自己的一把鎖,不同實例之間互不影響;例外:鎖對象是類名.class以及Synchronized修飾的是static方法的時候,所有對象共用統一把類鎖。
  3. 無論你是方法正常執行完畢或者方法拋出異常,都會釋放鎖。

synchronized的性質

  • 可重入

簡介:同一線程的外層函數獲得鎖之後,內層函數可以直接再次獲取該鎖。

好處:可避免死鎖、提升封裝性。

粒度(作用域):線程而非調用。

同一個方法是可重入的

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("我是父類方法");
}
}
我是子類方法
我是父類方法
  • 不可中斷

一旦這個鎖已經被別人獲得,如果還想獲得,只能選擇等待或者阻塞,直到別的線程釋放這個鎖。如果別人永遠不釋放鎖,那麼只能永遠等下去。

加鎖解鎖的實現原理

代碼Main.java:

package com.smartisan;
public class Main {
public static synchronized void method() {
}
public static void main(String[] args) {
synchronized (Main.class){
}
method();
}
}

javac Main.class並執行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"

說明:以上是代碼的字節碼信息,不難看出:

  • 同步在形式上有兩種方式完成

同步塊的實現使用了monitorenter和moniterexit指令

Synchronization of sequences of instructions is typically used to encode the synchronized block of the Java programming language. The Java Virtual Machine supplies the monitorenter and monitorexit instructions to support such language constructs. Proper implementation of synchronized blocks requires cooperation from a compiler targeting the Java Virtual Machine (§3.14).同步指令集通常用來實現同步代碼塊。Java虛擬機的指令集中有monitorenter和monitorexit兩條指令來支持synchronized關鍵字的語義。正確實現synchronized關鍵字需要Javac編譯器與Java虛擬器兩者共同協作支持。

同步方法依靠修飾符ACC_SYNCHRONIZED完成

Method-level synchronization is performed implicitly, as part of method invocation and return (§2.11.8). A synchronized method is distinguished in the run-time constant pool's method_info structure (§4.6) by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions. When invoking a method for which ACC_SYNCHRONIZED is 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 the synchronized method and the synchronized method does not handle the exception, the monitor for the method is automatically exited before the exception is rethrown out of the synchronized method.方法級的同步是隱式的,即無須通過字節碼指令來控制,它實現在方法調用和返回操作之中。虛擬機可以從方法常量池的方法表結構中的ACC_SYNCHRONIZED訪問標記得知一個方法是否聲明為同步方法。當方法調用時,調用指令將會檢查方法的ACC_SYNCHRONIZED訪問標誌是否被設置,如果設置了,執行線程就要求先成功持有監視器(monitor),然後才能執行方法,最後當方法完成(無論是正常完成還是非正常完成)時釋放監視器(monitor)。在方法執行期間,執行線程持有了監視器(monitor),其他任何線程都無法再獲取到同一個監視器(monitor)。如果一個同步方法執行期間拋出了異常,並且在方法內部無法處理此異常,那麼這個同步方法所持有的監視器(monitor)將在異常拋到同步方法之外時自動釋放。
  • 同步本質上都是通過監視器(monitor)提供支持

任意一個對象都擁有自己的監視器(monitor),當這個對象由同步代碼塊或者這個對象的同步方法調用時,執行方法的線程必須先獲取到該對象的監視器才能進入同步代碼塊或者同步方法,而沒有獲取到監視器的線程將會被阻塞在同步代碼塊和同步方法的入口處,進入BLOCKED狀態。

  • 為什麼會有兩個monitorexit?
  1. 一個monitorexit是正常退出同步時執行,一個monitorexit是拋出異常時monitorenter和monitorexit指令依然可以正確配對執行。
  2. 編譯器會自動產生一個異常處理器,這個異常處理器聲明可處理所有的異常,它的目的就是用來執行monitorexit指令。

可重入性質的原理

既然synchronized的實現是通過監視器(monitor)提供支持的,那麼我們分別看下monitorenter和monitorexit,理解了兩者,我們便可以理解可重入性質的原理:

  • 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.每個對象都與一個監視器相關聯。只有當監視器有所有者時,它才會被鎖定。執行monitorenter的線程嘗試獲得與鎖對象關聯的監視器的所有權時:如果與鎖對象關聯的監視器的條目計數為零,則線程將進入監視器並將其條目計數設置為1。此時這個線程是監視器的所有者。如果線程已經擁有與鎖對象關聯的監視器,它將重新進入監視器,並增加其條目計數。如果一個線程已經擁有與鎖對象關聯的監視器,則其他線程將一直阻塞,直到監視器的條目計數為零,然後再次嘗試獲得所有權。
  • 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.執行monitorexit的線程必須是與鎖對象關聯的監視器的所有者。線程會減少與鎖對象關聯的監視器的條目計數。如果結果是條目計數的值為零,則線程將不再是監視器的所有者。之前被阻止進入監視器的其他線程可嘗試去擁有監視器(moniter)

說明:從上述分析不難看出,可重入性質依賴的是加鎖次數計算器。

Java的內存模型

Java的內存模型定義了線程之間的抽象關係:線程之間的共享變量存儲在主內存中,每個線程都有一個私有的本地內存,本地內存中存儲了該線程共享變量的副本。Java的內存模型控制線程之間的通信,它決定了一個線程對主存共享變量的寫入何時對另一個線程可見。示意圖如下:


Java併發之synchronized深度解析


JMM

線程A與線程B之間若要通信的話,必須要經歷以下兩個步驟:

  1. 線程A把線程A本地內存中更新過的共享變量刷新到主內存中去。
  2. 線程B到主內存中讀取線程A之前更新過的共享變量。

可見性

可見性,指的是線程之間的可見性,即一個線程修改的狀態對另一個線程是可見的,也就是一個線程修改的結果,另一個線程馬上可以看到。所以保證了可見性,就可以保證線程的安全性。

synchronized線程安全的根本原因

瞭解了Java的內存模型和線程的可見性,不難得出synchronized的線程安全的根本原因:加鎖保證了只有一個線程可以操作主存中的共享變量:當本地內存中的共享變量副本發生變化後,解鎖之前會把本地內存中共享變量的值刷新到主存。而當其他線程獲取到鎖,會去主內存中讀取該共享變量的新值。

synchronized的缺陷

效率低:鎖的釋放情況少、試圖獲得鎖時不能設定超時、不能中斷一個正在試圖獲得鎖的線程

不夠靈活:加鎖和釋放的時機單一,每個所僅有單一的條件(某個對象),可能是不夠的

無法知道是否成功獲取到鎖

注意點

  • 鎖對象不能為空
  • 作用域不宜過大
  • 避免死鎖

今日科技快訊


近日,據國外媒體報道,當地時間週四高通擔保了總額約13.4億歐元(約合15.2億美元/104億元人民幣)的保證金,從而執行德國法院針對部分iPhone頒佈的銷售禁令。 據悉,德國一家法院去年12月20日裁定,蘋果侵犯了高通在智能手機節能技術方面的專利。蘋果早些時候曾表示,當禁令生效時,將從其在德國的15家零售店下架iPhone 7和8。該禁令在高通公佈這些保證金後即時生效。


週一上午好,新的一週繼續加油!

本篇來自 劍走偏鋒雨 的投稿文章,對Java中的synchronized關鍵字進行了非常精彩的講解,希望對大家有所幫助。

劍走偏鋒雨 的博客地址:

https://www.jianshu.com/u/4d1476815da2


前言


本篇從示例和理論兩方面講解synchronized關鍵字,希望對學習併發的你有所幫助。

併發基礎需瞭解的請跳轉:

https://www.jianshu.com/p/1adedd2b2727


正文


synchronized簡介

  • 作用

專業:如果一個對象對多個線程可見,則對該對象變量的所有讀取和寫入都是通過同步方法完成的。

通俗:能夠保證你在同一時刻最多隻有一個線程執行該段代碼,以達到保證併發安全的效果。

  • 地位

synchronized是Java的關鍵字,是最基本的互斥同步手段,是併發編程必學內容。

併發後果

舉例:

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

說明:

  1. 創建了兩個線程thread1和thread2以及定義了一個變量num
  2. thread1.start();thread2.start();意思是開啟了thread1和thread2,也就是兩個子線程都開始執行run方法。
  3. thread1.join();thread2.join();意思是thread1子線程執行完再執行主線程,thread2子線程執行完再執行主線程.所以有了這兩句代碼以後,兩個子線程都執行完自己的代碼,代碼System.out.println(num);才執行。
  4. 運行結構發現,大多數時候沒有達到預期結果20000,那原因在哪裡呢?是因為num++這操作,首先Cpu要去內存中讀數據,然後賦值+1,然後寫入內存,經歷三個步驟;假設num值是9,線程thread1讀取到了9,並且加了1,但是還沒有寫入內存,這時候thread2讀取到的內存中num的值還是9,所以線程thread1和thread2最後寫到內存的值都是10,所以最終num++的結果比預期少,我們把這種情況稱為線程不安全。
  5. 其實就是併發不能保證內存的可見性。

鎖分類

  • 對象鎖
  • 方法鎖:默認鎖對象為this
  • 同步代碼塊鎖:this或者自定義鎖對象
  • 類鎖
  • 靜態鎖:添加static
  • Class對象鎖:Main.class

對象鎖

  • 同步代碼塊鎖

鎖對象this

synchronized(this) {
System.out.println("我是對象鎖的代碼塊形式。我的名字是:"
+ Thread.currentThread().getName());
try {
Thread.sleep(3000);

} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "運行結束");
}
運行結果:

我是對象鎖的代碼塊形式。我的名字是:Thread-0

Thread-0運行結束
我是對象鎖的代碼塊形式。我的名字是:Thread-1

Thread-1運行結束
finish

說明:

  1. 這個時候this指的是誰呢?大家都知道this指代的是當前對象,也就是Main的實例對象。
  2. 雖然創建了兩個線程,但是Runnable的實例對象從來沒有變過,也就是this在這裡是唯一的,所以線程安全。
  3. 如果這裡用的是繼承Thread的方式創建的線程,this就不安全,因為每次創建新的線程,this所指代的內容就會發生變化。

自定義鎖對象

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() + "運行結束");
}
synchronized(lock2) {
System.out.println("我是lock2部分,我叫:"
+ Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "運行結束");
}
}
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");
}
}
運行結果:

我是lock1部分,我叫Thread-0

Thread-0 lock1部分運行結束
我是lock2部分,我叫:Thread-0
我是lock1部分,我叫Thread-1

Thread-0 lock2部分運行結束

Thread-1 lock1部分運行結束
我是lock2部分,我叫:Thread-1

Thread-1 lock2部分運行結束
finish

說明:

  1. lock1鎖被Thread1釋放後,Thread2才拿到lock1的鎖。
  2. lock2鎖被Thread1釋放後,Thread2才拿到lock2的鎖。
  3. 試想鎖的內容都是lock1,那Thread1的執行完兩個代碼塊的內容後,Thread2才會執行第一個代碼塊,運行結果會是:
運行結果:

我是lock1部分,我叫Thread-0

Thread-0 lock1部分運行結束
我是lock2部分,我叫:Thread-0

Thread-0 lock2部分運行結束
我是lock1部分,我叫Thread-1

Thread-1 lock1部分運行結束
我是lock2部分,我叫:Thread-1

Thread-1 lock2部分運行結束
finish
  • 方法鎖

舉例說明

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()+"運行結束");
}
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-1

Thread-0運行結束
方法鎖,我的名字是Thread-1

Thread-1運行結束
finish

說明:

  1. 我們給method方法添加了關鍵字synchronized,線程安全,同一時刻只有一個線程訪問這個方法。
  2. 方法鎖的默認鎖對象是this。

類鎖

  • 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() + "運行結束");
}
}
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");
}
}
運行結果:


我是類鎖的代碼塊形式。我的名字是:Thread-0

Thread-0運行結束
我是類鎖的代碼塊形式。我的名字是:Thread-1

Thread-1運行結束
finish

說明:

  1. 發現添加關鍵字synchronized後,thread1執行完run方法以後,thread2才會執行,線程安全。
  2. 只要鎖內容唯一,線程就安全。上面的示例的鎖內容是Main.class,Main.class始終不變,當jvm加載一個類時就會為這個類創建一個Class對象。而Main.class就是這個Class對象。
  3. Java類可能會有很多個對象,但是Class對象只有一個。不過Class對象其實也是存放在堆中的實例對象,只不過比new出來的對象特殊一點,是jvm加載類時所創建的。所以這個Runnable對象不管new多少新的實例傳入不同的Thread中,Class對象也只有一個,作為鎖的對象,線程安全。
  • 靜態鎖(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() + "運行結束");
}
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");
}
}

我是靜態鎖的代碼塊形式。我的名字是:Thread-0

Thread-0運行結束
我是靜態鎖的代碼塊形式。我的名字是:Thread-1

Thread-1運行結束
finish

說明:

如果不添加static,那麼因為Runnable的實例對象是兩個不同的,所以在訪問synchronized修改的普通方法的時候,線程0和線程1都會同時訪問到。而添加了static之後,就算不同的實例訪問這個方法,那麼也只有一個線程可以訪問到。

查看線程的生命週期

這裡主要介紹如何通過調試,查看線程的狀態,在這裡介紹RUNNING和BLOCKED。


Java併發之synchronized深度解析

打斷點

說明:在斷點的右邊打斷點,然後點擊小蟲子按鈕,進行Debug調試模式


Java併發之synchronized深度解析


斷點選項

說明:選擇All意味著JVM,即所有的線程停下來,選擇Thread意味著當前的線程停下來,這裡我們選擇All。


Java併發之synchronized深度解析

選擇線程

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


Java併發之synchronized深度解析


線程選擇

Java併發之synchronized深度解析

查看狀態

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


Java併發之synchronized深度解析

image.png

多線程訪問同步方法的7種具體情況

條件:以下同步方法指的是非static同步方法。

  • 兩個線程同時訪問一個對象的同步方法
  • 線程安全,因為有synchronized關鍵字修飾且是在一個對象中,可以起到同步的作用。
  • 兩個線程訪問的是兩個對象的同步方法
  • 當鎖對象是this的時候,因為是兩個對象,鎖對象指的內容會發生變化,這時候不安全。
  • 當鎖對象是類名.class的時候,儘管是兩個對象,但是Class對象只有一個,這個時候安全。
  • 兩個線程訪問的是Synchronized的靜態方法
  • synchronized+static 不管在幾個對象中,線程都是安全的。
  • 同時訪問同步方法和非同步方法
  • synchronized的作用域是修飾的方法,沒有被修飾的方法不能起到同步的作用。
  • 訪問同一個對象的不同的普通同步方法
  • 同步方法的默認鎖對象是this,因為是同一個對象,所以線程0先走完兩個方法,然後線程1再執行,串行。
  • 同時訪問靜態synchronized和非靜態synchronized方法
  • synchronized+static的所對象是Class對象,普通synchronized的鎖對象是this,因為鎖對象不同,所以兩個方法可以並行。
  • 方法拋出異常後,釋放鎖
  • synchronized修飾的方法拋出異常後,鎖會釋放

總結:

  1. 一把鎖只能同時被一個線程獲取,沒有拿到鎖的線程 必須等待。
  2. 每個實例都對應有自己的一把鎖,不同實例之間互不影響;例外:鎖對象是類名.class以及Synchronized修飾的是static方法的時候,所有對象共用統一把類鎖。
  3. 無論你是方法正常執行完畢或者方法拋出異常,都會釋放鎖。

synchronized的性質

  • 可重入

簡介:同一線程的外層函數獲得鎖之後,內層函數可以直接再次獲取該鎖。

好處:可避免死鎖、提升封裝性。

粒度(作用域):線程而非調用。

同一個方法是可重入的

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("我是父類方法");
}
}
我是子類方法
我是父類方法
  • 不可中斷

一旦這個鎖已經被別人獲得,如果還想獲得,只能選擇等待或者阻塞,直到別的線程釋放這個鎖。如果別人永遠不釋放鎖,那麼只能永遠等下去。

加鎖解鎖的實現原理

代碼Main.java:

package com.smartisan;
public class Main {
public static synchronized void method() {
}

public static void main(String[] args) {
synchronized (Main.class){
}
method();
}
}

javac Main.class並執行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"

說明:以上是代碼的字節碼信息,不難看出:

  • 同步在形式上有兩種方式完成

同步塊的實現使用了monitorenter和moniterexit指令

Synchronization of sequences of instructions is typically used to encode the synchronized block of the Java programming language. The Java Virtual Machine supplies the monitorenter and monitorexit instructions to support such language constructs. Proper implementation of synchronized blocks requires cooperation from a compiler targeting the Java Virtual Machine (§3.14).同步指令集通常用來實現同步代碼塊。Java虛擬機的指令集中有monitorenter和monitorexit兩條指令來支持synchronized關鍵字的語義。正確實現synchronized關鍵字需要Javac編譯器與Java虛擬器兩者共同協作支持。

同步方法依靠修飾符ACC_SYNCHRONIZED完成

Method-level synchronization is performed implicitly, as part of method invocation and return (§2.11.8). A synchronized method is distinguished in the run-time constant pool's method_info structure (§4.6) by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions. When invoking a method for which ACC_SYNCHRONIZED is 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 the synchronized method and the synchronized method does not handle the exception, the monitor for the method is automatically exited before the exception is rethrown out of the synchronized method.方法級的同步是隱式的,即無須通過字節碼指令來控制,它實現在方法調用和返回操作之中。虛擬機可以從方法常量池的方法表結構中的ACC_SYNCHRONIZED訪問標記得知一個方法是否聲明為同步方法。當方法調用時,調用指令將會檢查方法的ACC_SYNCHRONIZED訪問標誌是否被設置,如果設置了,執行線程就要求先成功持有監視器(monitor),然後才能執行方法,最後當方法完成(無論是正常完成還是非正常完成)時釋放監視器(monitor)。在方法執行期間,執行線程持有了監視器(monitor),其他任何線程都無法再獲取到同一個監視器(monitor)。如果一個同步方法執行期間拋出了異常,並且在方法內部無法處理此異常,那麼這個同步方法所持有的監視器(monitor)將在異常拋到同步方法之外時自動釋放。
  • 同步本質上都是通過監視器(monitor)提供支持

任意一個對象都擁有自己的監視器(monitor),當這個對象由同步代碼塊或者這個對象的同步方法調用時,執行方法的線程必須先獲取到該對象的監視器才能進入同步代碼塊或者同步方法,而沒有獲取到監視器的線程將會被阻塞在同步代碼塊和同步方法的入口處,進入BLOCKED狀態。

  • 為什麼會有兩個monitorexit?
  1. 一個monitorexit是正常退出同步時執行,一個monitorexit是拋出異常時monitorenter和monitorexit指令依然可以正確配對執行。
  2. 編譯器會自動產生一個異常處理器,這個異常處理器聲明可處理所有的異常,它的目的就是用來執行monitorexit指令。

可重入性質的原理

既然synchronized的實現是通過監視器(monitor)提供支持的,那麼我們分別看下monitorenter和monitorexit,理解了兩者,我們便可以理解可重入性質的原理:

  • 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.每個對象都與一個監視器相關聯。只有當監視器有所有者時,它才會被鎖定。執行monitorenter的線程嘗試獲得與鎖對象關聯的監視器的所有權時:如果與鎖對象關聯的監視器的條目計數為零,則線程將進入監視器並將其條目計數設置為1。此時這個線程是監視器的所有者。如果線程已經擁有與鎖對象關聯的監視器,它將重新進入監視器,並增加其條目計數。如果一個線程已經擁有與鎖對象關聯的監視器,則其他線程將一直阻塞,直到監視器的條目計數為零,然後再次嘗試獲得所有權。
  • 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.執行monitorexit的線程必須是與鎖對象關聯的監視器的所有者。線程會減少與鎖對象關聯的監視器的條目計數。如果結果是條目計數的值為零,則線程將不再是監視器的所有者。之前被阻止進入監視器的其他線程可嘗試去擁有監視器(moniter)

說明:從上述分析不難看出,可重入性質依賴的是加鎖次數計算器。

Java的內存模型

Java的內存模型定義了線程之間的抽象關係:線程之間的共享變量存儲在主內存中,每個線程都有一個私有的本地內存,本地內存中存儲了該線程共享變量的副本。Java的內存模型控制線程之間的通信,它決定了一個線程對主存共享變量的寫入何時對另一個線程可見。示意圖如下:


Java併發之synchronized深度解析


JMM

線程A與線程B之間若要通信的話,必須要經歷以下兩個步驟:

  1. 線程A把線程A本地內存中更新過的共享變量刷新到主內存中去。
  2. 線程B到主內存中讀取線程A之前更新過的共享變量。

可見性

可見性,指的是線程之間的可見性,即一個線程修改的狀態對另一個線程是可見的,也就是一個線程修改的結果,另一個線程馬上可以看到。所以保證了可見性,就可以保證線程的安全性。

synchronized線程安全的根本原因

瞭解了Java的內存模型和線程的可見性,不難得出synchronized的線程安全的根本原因:加鎖保證了只有一個線程可以操作主存中的共享變量:當本地內存中的共享變量副本發生變化後,解鎖之前會把本地內存中共享變量的值刷新到主存。而當其他線程獲取到鎖,會去主內存中讀取該共享變量的新值。

synchronized的缺陷

效率低:鎖的釋放情況少、試圖獲得鎖時不能設定超時、不能中斷一個正在試圖獲得鎖的線程

不夠靈活:加鎖和釋放的時機單一,每個所僅有單一的條件(某個對象),可能是不夠的

無法知道是否成功獲取到鎖

注意點

  • 鎖對象不能為空
  • 作用域不宜過大
  • 避免死鎖

更多JAVA乾貨,轉發+關注。私信“資料”即可獲取。


分享到:


相關文章: