深刻學習synchronized

synchronized

併發編程中的三個問題:

可見性(Visibility)

是指一個線程對共享變量進行修改,另外一個先當即獲得修改後的最新值。java

代碼演示:面試

public class Test01Visibility {
    public static boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (flag) {

            }
        }).start();

        Thread.sleep(2000);

        new Thread(() -> {
            flag = false;
            System.out.println("修改了flag");
        }).start();
    }
}

小結:併發編程時,會出現可見性問題,當一個線程對共享變量進行了修改,另外的線程並無當即看到修改編程

後的最新值。數組

原子性(Atomicity)

在一次或屢次操做中,要麼全部的操做都執行而且不會受其餘因素干擾而中斷,要麼全部的操做都不執行緩存

代碼演示:安全

public class Test02Atomicity {
    public static int num = 0;
    public static void main(String[] args) throws InterruptedException {
        // 建立任務
        Runnable increment = () -> {
            for (int i = 0; i < 1000; i++) {
                num++;
            }
        };
        ArrayList<Thread> threads = new ArrayList<>();
        //建立線程
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(increment);
            t.start();
            threads.add(t);
        }
        for (Thread thread : threads) {
            thread.join();
        }
        System.out.println(num);
    }
}

經過 javap -p -v Test02Atomicity對class 文件進行反彙編:發現++ 操做是由4條字節碼指令組成,並非原子操做多線程

小結:併發編程時,會出現原子性問題,當一個線程對共享變量操做到一半時,另外的線程也有可能來操做共享變量,干擾了前一個線程的操做併發

有序性(Ordering)

是指程序中代碼的執行順序,Java在編譯時和運行時會對代碼進行優化,會致使程序最終的執行順序不必定就是咱們編寫代碼時的順序。ide

代碼演示:高併發

@JCStressTest
@Outcome(id={"1","4"},expect=Expect.ACCEPTABLE,desc="ok")
@Outcome(id="0",expect=Expect.ACCEPTABLE_INTERESTING,desc="danger")
@State
public class Test03Orderliness { 
    int num=0;
    boolean ready=false;
    //線程一執行的代碼
    @Actor
    public void actor1(I_Resultr){
        if(ready){
            r.r1=num+num;
        }else{
            r.r1=1;
        }
    }
    //線程2執行的代碼
    @Actor
    public void actor2(I_Resultr){
        num=2;
        ready=true;
    }
}

運行的結果有:0、一、4

小結:程序代碼在執行過程當中的前後順序,因爲Java在編譯期以及運行期的優化,致使了代碼的執行順序未必

就是開發者編寫代碼時的順序。

Java內存模型(JMM)

計算機結構簡介

根據馮諾依曼體系結構,計算機由五大組成部分,輸入設備,輸出設備,存儲器,控制器,運算器。

CPU:

中央處理器,是計算機的控制和運算的核心,咱們的程序最終都會變成指令讓CPU去執行,處理程序中的數據。

內存:

咱們的程序都是在內存中運行的,內存會保存程序運行時的數據,供CPU處理。

緩存:

CPU的運算速度和內存的訪問速度相差比較大。這就致使CPU每次操做內存都要耗費不少等待時間。因而就有了在

CPU和主內存之間增長緩存的設計。CPU Cache分紅了三個級別: L1, L2, L3。級別越小越接近CPU,速度也更快,同時也表明着容量越小。

Java內存模型

Java內存模型是一套規範,描述了Java程序中各類變量(線程共享變量)的訪問規則,以及在JVM中將變量存儲到內存和從內存中讀取變量這樣的底層細節,具體以下。

主內存

主內存是全部線程都共享的,都能訪問的。全部的共享變量都存儲於主內存。

工做內存

每個線程有本身的工做內存,工做內存只存儲該線程對共享變量的副本。線程對變量的全部的操做(讀,取)都必須在工做內存中完成,而不能直接讀寫主內存中的變量,不一樣線程之間也不能直接訪問對方工做內存中的變量。

小結

Java內存模型是一套規範,描述了Java程序中各類變量(線程共享變量)的訪問規則,以及在JVM中將變量存儲到內存和從內存中讀取變量這樣的底層細節,Java內存模型是對共享數據的可見性、有序性、和原子性的規則和保障。

主內存與工做內存之間的交互

注意:1. 若是對一個變量執行lock操做,將會清空工做內存中此變量的值

  1. 對一個變量執行unlock操做以前,必須先把此變量同步到主內存中

synchronized保證三大特性

synchronized保證可見性

while(flag){
    //增長對象共享數據的打印,println是同步方法
    System.out.println("run="+run);
}

小結:

synchronized保證可見性的原理,執行synchronized時,lock原子操做會刷新工做內存中共享變量的值。

synchronized保證原子性

for(int i = 0; i < 1000; i++){
    synchronized(Test01Atomicity.class){
        number++;
    }
}

小結:

synchronized保證原子性的原理,synchronized保證只有一個線程拿到鎖,可以進入同步代碼塊。

synchronized保證有序性

synchronized(Test01Atomicity.class){
    num=2;
	ready=true;
}

小結

synchronized保證有序性的原理,咱們加synchronized後,依然會發生重排序,只不過,咱們有同步代碼塊,能夠保證只有一個線程執行同步代碼中的代碼,保證有序性。

synchronized的特性

可重入特性

public class Demo01 {
    public static void main(String[] args) {
        new MyThread().start();
        new MyThread().start();
    }
}
class MyThread extends Thread {
    @Override
    public void run() {
        synchronized (MyThread.class) {
            System.out.println(Thread.currentThread().getName() + "獲取了鎖1");
            synchronized (MyThread.class) {
                System.out.println(Thread.currentThread().getName() + "獲取了鎖2");
            }
        }
    }
}

可重入原理:

synchronized的鎖對象中有一個計數器(recursions變量)會記錄線程得到幾回鎖。

可重入的好處:

  1. 能夠避免死鎖

  2. 可讓咱們更好的來封裝代碼

小結:

synchronized是可重入鎖,內部鎖對象中會有一個計數器記錄線程獲取幾回鎖啦,獲取一次鎖加+1,在執行完同步代碼塊時,計數器的數量會-1,直到計數器的數量爲0,就釋放這個鎖。

不可中斷特性

什麼是不可中斷?

一個線程得到鎖後,另外一個線程想要得到鎖,必須處於阻塞或等待狀態,若是第一個線程不釋放鎖,第二個線程會一直阻塞或等待,不可被中斷。

public class Uninterruptible {
    private static Object obj = new Object();
    public static void main(String[] args) throws InterruptedException {
        Runnable run = () -> {
            synchronized (obj) {
                String name = Thread.currentThread().getName();
                System.out.println(name + "執行同步代碼塊");
                try {
                    Thread.sleep(888888);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread t1 = new Thread(run);
        t1.start();
        Thread.sleep(1000);
        Thread t2 = new Thread(run);
        t2.start();

        Thread.sleep(1000);
        System.out.println("中止線程2前");
        System.out.println(t2.getState());
        t2.interrupt();
        System.out.println("中止線程2後");
        System.out.println(t1.getState());
        System.out.println(t2.getState());
    }
}

synchronized是不可中斷,處於阻塞狀態的線程會一直等待鎖。

ReentrantLock可中斷演示

public class Interruptible {
    private static Lock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        test01();
    }

    private static void test01() throws InterruptedException {
        Runnable run = () -> {
            boolean flag = false;
            String  name = Thread.currentThread().getName();
            try {
                flag = lock.tryLock(3, TimeUnit.SECONDS);
                if (flag) {
                    System.out.println(name + "得到鎖,進入鎖執行");
                    Thread.sleep(888888);
                } else {
                    System.out.println(name + "沒有得到鎖");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (flag) {
                    lock.unlock();
                    System.out.println(name + "釋放鎖");
                }
            }
        };

        Thread t1 = new Thread(run);
        t1.start();
        Thread.sleep(1000);

        Thread t2 = new Thread(run);
        t2.start();
    }
}

小結:

synchronized屬於不可被中斷

Lock的lock方法是不可中斷的

Lock的tryLock方法是可中斷的

synchronized 的原理

monitorenter:

每個對象都會和一個監視器monitor關聯。監視器被佔用時會被鎖住,其餘線程沒法來獲取該monitor。當JVM執行某個線程的某個方法內部的monitorenter時,它會嘗試去獲取當前對象對應的monitor的全部權。其過程以下:

  1. 若monior的進入數爲0,線程能夠進入monitor,並將monitor的進入數置爲1。當前線程成爲monitor的owner(全部者)

  2. 若線程已擁有monitor的全部權,容許它重入monitor,則進入monitor的進入數加1

  3. 若其餘線程已經佔有monitor的全部權,那麼當前嘗試獲取monitor的全部權的線程會被阻塞,直到monitor的進入數變爲0,才能從新嘗試獲取monitor的全部權。

monitorenter小結:

synchronized的鎖對象會關聯一個monitor, 這個monitor不是咱們主動建立的, 是JVM的線程執行到這個同步代碼塊,發現鎖對象

有monitor就會建立monitor, monitor內部有兩個重要的成員變量owner擁有這把鎖的線程,recursions會記錄線程擁有鎖的次數,

當一個線程擁有monitor後其餘線程只能等待。

monitorexit:

  1. 能執行monitorexit 指令的線程必定是擁有當前對象的monitor的全部權的線程。

  2. 執行monitorexit 時會將monitor的進入數減1。當monitor的進入數減爲0時,當前線程退出monitor,再也不擁有monitor的全部權,此時其餘被這個monitor阻塞的線程能夠嘗試去獲取這個monitor的全部權

monitorexit釋放鎖。

monitorexit插入在方法結束處和異常處,JVM保證每一個monitorenter必須有對應的monitorexit。

面試題synchroznied出現異常會釋放鎖嗎?

:會釋放鎖。

同步方法

同步方法在反彙編後,會增長ACC_SYNCHRONIZED修飾。會隱式調用monitorenter 和monitorexit。在執行同步方法前會調用

monitorenter,在執行完同步方法後會調用monitorexit 。

小結:

經過javap反彙編能夠看到synchronized 使用了monitorentor和monitorexit兩個指令。每一個鎖對象都會關聯一個monitor(監視

器,它纔是真正的鎖對象),它內部有兩個重要的成員變量owner會保存得到鎖的線程,recursions會保存線程得到鎖的次數, 當執行到

monitorexit時, recursions會-1, 當計數器減到0時這個線程就會釋放鎖。

面試題:synchronized與Lock的區別

一、synchronized 是關鍵字,lock 是一個接口

二、synchronized 會自動釋放鎖,lock 須要手動釋放鎖。

三、synchronized 是不可中斷的,lock 能夠中斷也能夠不中斷。

四、經過lock 能夠知道線程有沒有拿到鎖,而synchronized 不能。

五、synchronized 能鎖住方法和代碼塊,而lock 只能鎖住代碼塊。

六、lock 可使用讀鎖提升多線程讀效率。

七、synchronized 是非公平鎖,ReentrantLock 能夠控制是不是公平鎖。

CAS

cas的概述和做用:

compare and swap,能夠將比較和交換轉爲原子操做,這個原子操做直接由cpu保證,cas能夠保證共享變量賦值時的原子操做,cas依賴3個值:內存中的值v,舊的預估值x,要修改的新值b。根據atomicInteger的地址加上偏移量offset的值能夠獲得內存中的值,將內存中的值和舊的預估值進行比較,若是相同,就將新值保存到內存中。不相同就進行重試。

Java對象的佈局

在JVM中,對象在內存中的佈局分爲三塊區域:對象頭、實例數據和對齊填充。以下圖所示:

HotSpot採用instanceOopDesc和arrayOopDesc來描述對象頭,arrayOopDesc對象用來描述數組類型。

從instanceOopDesc代碼中能夠看到 instanceOopDesc繼承自oopDesc。

_mark表示對象標記、屬於markOop類型,也就是Mark World,它記錄了對象和鎖有關的信息

_metadata表示類元信息,類元信息存儲的是對象指向它的類元數據(Klass)的首地址,其中Klass表示普通指針、compressed_klass表示壓縮類指針。

Mark Word

鎖狀態 存儲內容 鎖標誌位
無鎖 對象的hashcode、對象分代年齡、是不是偏向鎖(0) 01
偏向鎖 偏向線程id、偏向時間戳、對象分代年齡、是不是偏向鎖(1) 01
輕量級鎖 指向棧中鎖記錄的指針 00
重量級鎖 指向互斥量(重量級鎖)的指針 10

klass pointer

用於存儲對象的類型指針,該指針指向它的類元數據,JVM經過這個指針肯定對象是哪一個類的實例。經過-XX:+UseCompressedOops開啓指針壓縮,

在64位系統中,Mark Word = 8 bytes,類型指針 = 8bytes,對象頭 = 16 bytes = 128bits;

實例數據

就是類中定義的成員變量。

對齊填充

因爲HotSpot VM的自動內存管理系統要求對象起始地址必須是8字節的整數倍,對象的大小必須是8字節的整數倍。所以,當對象實例數據部分沒有對齊時,就須要經過對齊填來補全。

查看Java對象佈局

<dependency>
   <groupId>org.openjdk.jol</groupId>
   <artifactId>jol-core</artifactId>
   <version>0.9</version>
</dependency>

小結

Java對象由3部分組成,對象頭,實例數據,對齊數據,對象頭分紅兩部分:Mark World + Klass pointer

偏向鎖

什麼是偏向鎖?

鎖會偏向於第一個得到它的線程,會在對象頭存儲鎖偏向的線程ID,之後該線程進入和退出同步塊時只須要檢查是否爲偏向鎖、鎖標誌位以及ThreadID便可。

不過一旦出現多個線程競爭時必須撤銷偏向鎖,因此撤銷偏向鎖消耗的性能必須小於以前節省下來的CAS原子操做的性能消耗,否則就得不償失了。

偏向鎖原理

當線程第一次訪問同步塊並獲取鎖時,偏向鎖處理流程以下:

  1. 虛擬機將會把對象頭中的標誌位設爲「01」,即偏向模式。
  2. 同時使用CAS操做把獲取到這個鎖的線程的ID記錄在對象的Mark Word之中,若是CAS操做成功,持有偏向鎖的線程之後每次進入這個鎖相關的同步塊時,虛擬機均可以再也不進行任何同步操做,偏向鎖的效率高。

偏向鎖的撤銷

  1. 偏向鎖的撤銷動做必須等待全局安全點

  2. 暫停擁有偏向鎖的線程,判斷鎖對象是否處於被鎖定狀態

  3. 撤銷偏向鎖,恢復到無鎖(標誌位爲01)或輕量級鎖(標誌位爲00)的狀態

偏向鎖是自適應的

小結:

偏向鎖的原理是什麼?

當鎖對象第一次被線程獲取的時候,虛擬機將會把對象頭中的標誌位設爲「01」,即偏向模式。同時使用CAS操做把獲取到這個鎖的線程的ID記錄在對象的MarkWord之中,若是CAS操做成功,持有偏向鎖的線程之後每次進入這個鎖相關的同步塊時,虛擬機均可以再也不進行任何同步操做,偏向鎖的效率高。

偏向鎖的好處是什麼?

偏向鎖是在只有一個線程執行同步塊時進一步提升性能,適用於一個線程反覆得到同一鎖的狀況。偏向鎖能夠提升帶有同步但無競爭的程序性能。

輕量級鎖

什麼是輕量級鎖?

輕量級鎖是JDK 6之中加入的新型鎖機制,輕量級鎖並非用來代替重量級鎖的。

引入輕量級鎖的目的:在多線程交替執行同步塊的狀況下,儘可能避免重量級鎖引發的性能消耗,可是若是多個線程在同一時刻進入臨界區,會致使輕量級鎖膨脹升級爲重量級鎖,因此輕量級鎖的出現並不是是要代替重量級鎖。

輕量級鎖原理

當關閉偏向鎖或多個線程競爭偏向鎖致使偏向鎖升級爲輕量級鎖,則會嘗試獲取輕量級鎖,其步驟以下:

  1. 判斷當前對象是否處於無鎖狀態,若是是,則JVM 首先將在當前線程的棧幀中創建一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word 的拷貝,將對象的Mark Word 複製到棧幀中的Lock Record 中,將Lock Record中的owner指向當前對象。
  2. JVM 利用CAS 操做嘗試將對象的Mark Word 更新爲指向Lock Record 的指針,若是成功表示競爭到鎖,則將鎖標誌位變成00,執行同步操做。
  3. 若是失敗則判斷當前對象的Mark Word 是否指向當前線程的棧幀,若是是則表示當前線程已經持有當前對象的鎖,則直接執行同步代碼塊;不然只能說明該鎖對象已經被其餘線程搶佔了,這時輕量級鎖須要膨脹爲重量級鎖,鎖標誌位變成10,後面等待的線程將會進入阻塞狀態。

輕量級鎖的釋放:

輕量級鎖的釋放也是經過CAS操做來進行的,主要步驟以下:

  1. 取出在獲取輕量級鎖時保存在Mark Word 中的數據;
  2. 用CAS 操做將取出的數據替換當前對象的Mark Word 中,若是成功,則說明釋放鎖成功。
  3. 若是CAS 操做替換失敗,說明有其餘線程獲取該鎖,則須要將輕量級鎖膨脹升級爲重量級鎖。

對於輕量級鎖,其性能提高的依據是「對於絕大部分的鎖,在整個生命週期內都是不會存在競爭的」,若是打破這個依據則除了互斥的開銷外,還有額外的CAS 操做,所以在有多線程競爭的狀況下,輕量級鎖比重量級鎖更慢。

輕量級鎖好處:

在多線程交替執行同步塊的狀況下,能夠避免重量級鎖引發的性能消耗。

自旋鎖

monitor 實現鎖的時候, monitor 會阻塞和喚醒線程,線程的阻塞和喚醒須要CPU 從用戶態轉爲核心態,頻繁的阻塞和喚醒對CPU 來講是一件負擔很重的工做,這些操做給系統的併發性能帶來了很大的壓力。同時,共享數據的鎖定狀態可能只會持續很短的一段時間,爲了這段時間阻塞和喚醒線程並不值得。若是有一個以上的處理器,能讓兩個或以上的線程同時並行執行,就可讓後面請求鎖的那個線程「稍微等一下」,但不放棄處理器的執行時間,看看持有鎖的線程是否釋放了鎖。爲了讓線程等待,咱們只需讓線程執行一個循環(即自旋),這就是自旋鎖。

自旋鎖在JDK 1.4.2中就已經引入,只不過默認是關閉的,可使用-XX:+UseSpinning參數來開啓,在JDK 6中就已經改成默認開啓了。自旋等待不能代替阻塞,且先不說對處理器數量的要求,自旋等待自己雖然避免了線程切換的開銷,但它是要佔用處理器時間的,所以,若是鎖被佔用的時間很短,自旋等待的效果就會很是好,反之,若是鎖被佔用的時間很長。那麼自旋的線程只會白白消耗處理器資源,而不會作任何有用的工做,反而會帶來性能上的浪費。所以,自旋等待的時間必需要有必定的限度,若是自旋超過了限定的次數仍然沒有成功得到鎖,就應當使用傳統的方式去掛起線程了。自旋次數的默認值是10次,用戶可使用參數-XX : PreBlockSpin來更改。

適應性自旋鎖

在JDK 6 中引入了自適應的自旋鎖。若是在同一個鎖對象上,自旋等待剛剛成功得到過鎖,而且持有鎖的線程正在運行中,那麼虛擬機就會認爲此次自旋也頗有可能再次成功,進而它將容許自旋等待持續相對更長的時間。若是對於某個鎖,自旋不多成功得到過,那在之後要獲取這個鎖時將可能省略掉自旋過程,以免浪費處理器資源。

平時寫代碼如何對synchronized優化

減小synchronized的範圍:

同步代碼塊中儘可能短,減小同步代碼塊中代碼的執行時間,減小鎖的競爭。

synchronized(Demo01.class){
    System.out.println("aaa");
}

下降synchronized鎖的粒度:

將一個鎖拆分爲多個鎖提升併發度,如HashTable:鎖定整個哈希表,一個操做正在進行時,其餘操做也同時鎖定,效率低下。ConcurrentHashMap:局部鎖定,只鎖定桶。

讀寫分離:

讀取時不加鎖,寫入和刪除時加鎖

ConcurrentHashMap,CopyOnWriteArrayList和ConyOnWriteSet

相關文章
相關標籤/搜索