synchronized主要是用於解決線程安全問題的,而線程安全問題的主要誘因有以下兩點:java
解決線程安全問題的根本方法:算法
因此互斥鎖是解決問題的辦法之一,互斥鎖的特性以下:數組
互斥性:即在同一時間只容許一個線程持有某個對象鎖,經過這種特性來實現多線程的協調機制,這樣在同一時間只有一個線程對須要同步的代碼塊(複合操做)進行訪問。互斥性也稱爲操做的原子性
可見性:必須確保在鎖被釋放以前,對共享變量所作的修改,對於隨後得到該鎖的另外一個線程是可見的(即在得到鎖時應得到最新共享變量的值),不然另外一個線程多是在本地緩存的某個副本上繼續操做,從而引發數據不一致問題緩存
而synchronized就能夠實現互斥鎖的特性,不過須要注意的是synchronized鎖的不是代碼,而是對象。安全
根據獲取的鎖能夠分爲兩類:數據結構
對象鎖和類鎖的總結:多線程
實現synchronized須要依賴兩個基礎概念:併發
Java對象在內存中的佈局主要分爲三塊區域:app
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執行流程圖:
線程池的狀態:
線程池狀態轉換圖:
線程池中工做線程的生命週期:
關於線程池大小如何選定參考: