您好,登錄后才能下訂單哦!
synchronized主要是用于解決線程安全問題的,而線程安全問題的主要誘因有如下兩點:
解決線程安全問題的根本方法:
所以互斥鎖是解決問題的辦法之一,互斥鎖的特性如下:
互斥性:即在同一時間只允許一個線程持有某個對象鎖,通過這種特性來實現多線程的協調機制,這樣在同一時間只有一個線程對需要同步的代碼塊(復合操作)進行訪問。互斥性也稱為操作的原子性
可見性:必須確保在鎖被釋放之前,對共享變量所做的修改,對于隨后獲得該鎖的另一個線程是可見的(即在獲得鎖時應獲得最新共享變量的值),否則另一個線程可能是在本地緩存的某個副本上繼續操作,從而引起數據不一致問題
而synchronized就可以實現互斥鎖的特性,不過需要注意的是synchronized鎖的不是代碼,而是對象。
根據獲取的鎖可以分為兩類:
對象鎖和類鎖的總結:
實現synchronized需要依賴兩個基礎概念:
Java對象在內存中的布局主要分為三塊區域:
synchronized使用的鎖對象是存儲在Java對象頭里的,對象頭結構如下:
由于對象頭信息是與對象自身定義的數據沒有關系的額外存儲成本,考慮到JVM的空間效率,Mark Word被設計為非固定的數據結構以便存儲更多有效的數據,它會根據對象自身的狀態賦予自己的存儲空間:
簡單介紹了對象頭,接著我們來了解一下Monitor,每個Java對象天生自帶了一把看不見的鎖,它叫做內部鎖或Monitor鎖。Monitor的主要實現代碼在ObjectMonitor.hpp中:
Monitor鎖的競爭、獲取與釋放:
然后我們從字節碼層面上看一下synchronized,將如下代碼通過javac編譯成class文件:
package com.example.demo.thread;
/**
* @author 01
* @date 2019-07-20
**/
public class SyncBlockAndMethod {
public void syncsTask() {
synchronized (this) {
System.out.println("Hello syncsTask");
}
}
public synchronized void syncTask() {
System.out.println("Hello syncTask");
}
}
然后通過 javap -verbose 將class文件反編譯成可閱讀的字節碼內容,如下:
Classfile /E:/Java_IDEA/demo/src/main/java/com/example/demo/thread/SyncBlockAndMethod.class
Last modified 2019年7月20日; size 637 bytes
MD5 checksum 7600723349daa088a5353acd84c80fa5
Compiled from "SyncBlockAndMethod.java"
public class com.example.demo.thread.SyncBlockAndMethod
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #6 // com/example/demo/thread/SyncBlockAndMethod
super_class: #7 // java/lang/Object
interfaces: 0, fields: 0, methods: 3, attributes: 1
Constant pool:
#1 = Methodref #7.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #19.#20 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #21 // Hello syncsTask
#4 = Methodref #22.#23 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = String #24 // Hello syncTask
#6 = Class #25 // com/example/demo/thread/SyncBlockAndMethod
#7 = Class #26 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 syncsTask
#13 = Utf8 StackMapTable
#14 = Class #27 // java/lang/Throwable
#15 = Utf8 syncTask
#16 = Utf8 SourceFile
#17 = Utf8 SyncBlockAndMethod.java
#18 = NameAndType #8:#9 // "<init>":()V
#19 = Class #28 // java/lang/System
#20 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#21 = Utf8 Hello syncsTask
#22 = Class #31 // java/io/PrintStream
#23 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#24 = Utf8 Hello syncTask
#25 = Utf8 com/example/demo/thread/SyncBlockAndMethod
#26 = Utf8 java/lang/Object
#27 = Utf8 java/lang/Throwable
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public com.example.demo.thread.SyncBlockAndMethod();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
public void syncsTask();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter // 指向同步代碼塊的開始位置
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String Hello syncsTask
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit // 指向同步代碼塊的結束位置,monitorenter和monitorexit之間就是同步代碼塊
14: goto 22
17: astore_2
18: aload_1
19: monitorexit // 若代碼發生異常時就會執行這句指令釋放鎖
20: aload_2
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any
LineNumberTable:
line 10: 0
line 11: 4
line 12: 12
line 13: 22
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 17
locals = [ class com/example/demo/thread/SyncBlockAndMethod, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
public synchronized void syncTask();
descriptor: ()V
flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED // 用于標識是一個同步方法,不需要像同步塊那樣需要通過顯式的字節碼指令去標識哪里需要獲取鎖,哪里需要釋放鎖。同步方法無論是正常執行還是發生異常都會釋放鎖
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String Hello syncTask
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 16: 0
line 17: 8
}
SourceFile: "SyncBlockAndMethod.java"
什么是重入:
從互斥鎖的設計上來說,當一個線程試圖操作一個由其他線程持有的對象鎖的臨界資源時,將會處于阻塞狀態,但當一個線程再次請求自己持有對象鎖的臨界資源時,這種情況屬于重入
為什么會對synchronized嗤之以鼻:
鎖優化之自旋鎖:
許多情況下,共享數據的鎖定狀態持續時間較短,切換線程不值得。于是自旋鎖應運而生,所謂自旋就是通過讓線程執行忙循環等待鎖的釋放,從而不讓出CPU時間片,例如while某個標識變量
缺點:若鎖被其他線程長時間占用,將會帶來許多性能上的開銷,所以一般超過指定的自旋次數就會將線程掛起處于阻塞狀態
鎖優化之自適應自旋鎖:
自適應自旋鎖與普通自旋鎖不同的就是可以自適應自旋次數,即自旋次數不再固定。而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定
鎖優化之鎖消除,鎖消除是JVM另一種鎖優化,這種優化更徹底:
在JIT編譯時,對運行上下文進行掃描,去除不可能存在資源競爭的鎖。這種方式可以消除不必要的鎖,可以減少毫無意義的請求鎖時間
關于鎖消除,我們可以看一個例子,代碼如下:
public class StringBufferWithoutSync {
public void add(String str1, String str2) {
//StringBuffer是線程安全,由于sb只會在append方法中使用,不可能被其他線程引用
//因此sb屬于不可能共享的資源,JVM會自動消除內部的鎖
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
public static void main(String[] args) {
StringBufferWithoutSync withoutSync = new StringBufferWithoutSync();
for (int i = 0; i < 1000; i++) {
withoutSync.add("aaa", "bbb");
}
}
}
鎖優化之鎖粗化,我們再來了解鎖粗化的概念,有些情況下可能會需要頻繁且重復進行加鎖和解鎖操作,例如同步代碼寫在循環語句里,此時JVM會有鎖粗化的機制,即通過擴大加鎖的范圍,以避免反復加鎖和解鎖操作。代碼示例:
public class CoarseSync {
public static String copyString100Times(String target){
int i = 0;
// JVM會將鎖粗化到外部,使得重復的加解鎖操作只需要進行一次
StringBuffer sb = new StringBuffer();
while (i < 100){
sb.append(target);
}
return sb.toString();
}
}
synchronized鎖存在四種狀態:
偏向鎖:
大多數情況下,鎖不存在多線程競爭,總是由同一線程多次獲得,為了減少同一線程獲取鎖的代價,就會使用偏向鎖
核心思想:
如果一個線程獲得了鎖,那么鎖就進入偏向模式,此時Mark Word的結構也變為偏向鎖結構,當該線程再次請求鎖時,無需再做任何同步操作,即獲取鎖的過程只需要檢查Mark Word的鎖標記位為偏向鎖以及當前線程id等于Mark Word的ThreadID即可,這樣就省去了大量有關鎖申請的操作,那么這個鎖也就偏向于該線程了偏向鎖不適用于鎖競爭比較激烈的多線程場合
輕量級鎖:
輕量級鎖是由偏向鎖升級而來,偏向鎖運行在一個線程進入同步塊的情況下,當有第二個線程加入鎖競爭時,偏向鎖就會升級為輕量級鎖
適用場景:線程交替執行同步塊
若存在線程同一時間訪問同一鎖的情況,就會導致輕量級鎖膨脹為重量級鎖
輕量級鎖的加鎖過程:
在代碼進入同步塊的時候,如果同步對象鎖狀態為無鎖狀態(鎖標志位為“01”狀態),虛擬機首先將在當前線程的棧幀中建立一個名為鎖記錄(LockRecord)的空間,用于存儲鎖對象目前的Mark Word的拷貝,官方稱之為Displaced Mark Word。這時候線程堆棧與對象頭的狀態如下圖所示:
如果這個更新動作成功了,那么這個線程就擁有了該對象的鎖,并且對象Mark Word的鎖標志位設置為“00",即表示此對象處于輕量級鎖定狀態,這時候線程堆棧與對象頭的狀態如下圖所示:
輕量級鎖的解鎖過程:
鎖的內存語義:
當線程釋放鎖時,Java內存模型會把該線程對應的本地內存中的共享變量刷新到主內存中;而當線程獲取鎖時,Java內存模型會把該線程對應的本地內存置為無效,從而使得被監視器保護的臨界區代碼必須從主內存中讀取共享變量
偏向鎖、輕量級鎖、重量級鎖的匯總:
在JDK1.5之前,synchronized是Java唯一的同步手段,而在1.5之后則有了ReentrantLock類(重入鎖):
ReentrantLock公平性的設置:
ReentrantLock fairLock = new ReentrantLock(true);
ReentrantLock的好處在于將鎖對象化了,因此可以實現synchronized難以實現的邏輯,例如:
如果說ReentrantLock將synchronized轉變為了可控的對象,那么是否能將wait、notify及notifyall等方法對象化,答案是有的,即Condition:
synchronized和ReentrantLock的區別:
Java內存模型(JMM):
Java內存模型(Java Memory Model,簡稱JMM)本身是一種抽象的概念,并不真實存在,它描述的是一組規則或規范,通過這組規范定義了程序中各個變量(包括實例字段,靜態字段和構成數組對象的元素)的訪問方式
JMM中的主內存(即堆空間):
JMM中的工作內存(即本地內存,或線程棧):
JMM與Java內存區域劃分(即Java內存結構)是不同的概念層次:
主內存與工作內存的數據存儲類型以及操作方式歸納:
JMM如何解決可見性問題:
指令重排序需要滿足的條件:
什么是Java內存模型中的happens-before:
happens-before的八大原則:
- 程序次序規則:一個線程內,按照代碼順序,書寫在前面的操作先行發生于書寫在后面的操作
- 鎖定規則:一個unLock操作先行發生于后面對同一個鎖的lock操作
- volatile變量規則:對一個變量的寫操作先行發生于后面對這個變量的讀操作(保證了可見性)
- 傳遞規則:如果操作A先行發生于操作B,而操作B又先行發生于操作C,則可以得出操作A先行發生于操作C
- 線程啟動規則:Thread對象的start()方法先行發生于此線程的每一個動作
- 線程中斷規則:對線程interrupt()方法的調用先行發生于被中斷線程的代碼檢測到中斷事件的發生
- 線程終結規則:線程中所有的操作都先行發生于線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行
- 對象終結規則:一個對象的初始化完成先行發生于他的finalize()方法的開始
volatile:
volatile變量為何立即可見?簡單來說:
volatile變量如何禁止重排序優化:
volatile和synchronized的區別:
- volatile本質是在告訴JVM當前變量在寄存器(工作內存)中的值是不確定的,需要從主存中讀取;synchronized則是鎖定當前變量,只有當前線程可以訪問該變量,其他線程被阻塞住直到該線程完成變量操作為止
- volatile僅能使用在變量級別;synchronized則可以使用在變量、方法和類級別
- volatile僅能實現變量的修改可見性,不能保證原子性;而synchronized則可以保證變量修改的可見性和原子性
- volatile不會造成線程的阻塞;synchronized可能會造成線程的阻塞
- volatile標記的變量不會被編譯器優化;synchronized標記的變量可以被編譯器優化
CAS(Compare and Swap)是一種線程安全性的方法:
CAS思想:
CAS多數情況下對開發者來說是透明的:
缺點:
利用Executors創建不同的線程池滿足不同場景的需求:
- newFixedThreadPool(int nThreads):指定工作線程數量的線程池
- newCachedThreadPool():處理大量短時間工作任務的線程池,特點:
- 試圖緩存線程并重用,當無緩存線程可用時,就會創建新的工作線程
- 如果線程閑置的時間超過閾值,則會被終止并移出緩存
- 系統長時間閑置的時候,不會消耗什么資源
- newSingleThreadExecutor():創建唯一的工作者線程來執行任務,如果線程異常結束,會有另一個線程取代它
- newSingleThreadScheduledExecutor()與newScheduledThreadPool(int corePoolSize):定時或者周期性的工作調度,兩者的區別在于單一工作線程還是多個線程
- JDK8新增的newWorkStealingPool():內部會構建ForkJoinPool ,利用working-stealing算法,并行地處理任務,不保證處理順序
- working-stealing算法:某個線程從其他線程的任務隊列里竊取任務來執行
Fork/Join框架(JDK7提供):
為什么要使用線程池:
Executor的框架:
J.U.C的三個Executor接口:
線程池執行任務流程圖:
ThreadPoolExecutor的七個構造器參數:
int corePoolSize
:核心線程數int maximumPoolSize
:最大線程數long keepAliveTime
:線程空閑存活時間TimeUnit unit
:存活時間的單位BlockingQueue<Runnable> workQueue
:任務等待隊列ThreadFactory threadFactory
:線程創建工廠,用于創建新線程RejectedExecutionHandler handler
:任務拒絕策略
新任務提交execute執行后的判斷:
execute執行流程圖:
線程池的狀態:
線程池狀態轉換圖:
線程池中工作線程的生命周期:
關于線程池大小如何選定參考:
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。