Java多線程 - 鎖機制

鎖的做用

在不一樣線程中,對同一變量、方法或代碼塊進行同步訪問html

鎖的實現方式

咱們經過一個例子瞭解鎖的不一樣實現,開啓100個線程對同一int變量進行++操做1000次,在這個過程當中如何對這個變量進行同步java

未同步代碼:算法

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/** * \* Created with IntelliJ IDEA. * \* User: guohezuzi * \* Date: 2018-04-30 * \* Time: 上午11:26 * \* Description:本身編寫的多線程的栗子(多個線程添加元素到數組中) * \ * * @author guohezuzi */
public class MyExample {
    private int count = 0;

    class addHundredNum extends Thread {
        @Override
        public void run() {
            //...執行其餘操做
            for (int i = 0; i < 1000; i++) {
                    count++;
            }
            //...執行其餘操做
        }
    }

    public void test() throws InterruptedException {
        addHundredNum[] addHundredNums = new addHundredNum[100];
        for (int i = 0; i < addHundredNums.length; i++) {
            addHundredNums[i] = new addHundredNum();
        }

        for (addHundredNum addHundredNum : addHundredNums) {
            addHundredNum.start();
        }
        // 等待全部addHundredNum線程執行完畢
        for (addHundredNum addHundredNum : addHundredNums) {
            addHundredNum.join();
        }
    }

    public static void main(String[] args) throws Exception {
        MyExample example = new MyExample();
        example.test();
        System.out.println(example.count);
    }
}
複製代碼

synchronized

經過synchronized(addHundredNum.class)給當前對象加鎖而不是synchronized(this)給對象實例加鎖編程

public class MyExample {
    private int count = 0;

    class addHundredNum extends Thread {
        @Override
        public void run() {
            //...執行其餘操做
            synchronized (addHundredNum.class) {
            for (int i = 0; i < 1000; i++) {
                    count++;
            }
            }
            //...執行其餘操做
        }
    }

    public void test() throws InterruptedException {
        addHundredNum[] addHundredNums = new addHundredNum[100];
        for (int i = 0; i < addHundredNums.length; i++) {
            addHundredNums[i] = new addHundredNum();
        }

        for (addHundredNum addHundredNum : addHundredNums) {
            addHundredNum.start();
        }

        for (addHundredNum addHundredNum : addHundredNums) {
            addHundredNum.join();
        }
    }

    public static void main(String[] args) throws Exception {
        MyExample example = new MyExample();
        example.test();
        System.out.println(example.count);
    }
}
複製代碼

拓展

synchronized的不一樣加鎖方式
  • 給對象加鎖
    • 修飾靜態方法
    • 修飾代碼塊時使用synchronized(class)
  • 給對象實例加鎖
    • 修飾非靜態方法
    • 修飾代碼塊時使用synchronized(this) 或 synchronized(Object)
JVM角度理解synchronized關鍵字

synchronized關鍵字通過編譯以後,會在同步塊的先後分別造成monitorenter和monitorexit這兩個字節碼指令,這兩個字節碼都須要一個reference類型的參數來指明要鎖定和解鎖的對象。若是Java程序中的synchronized明確指定了對象參數,那就是這個對象的reference;若是沒有明確指定,那就根據synchronized修飾的是實例方法仍是類方法,去取對應的對象實例或Class對象來做爲鎖對象。 根據虛擬機規範的要求,在執行monitorenter指令時,首先要去嘗試獲取對象的鎖。若是這個對象沒被鎖定,或者當前線程已經擁有了那個對象的鎖,把鎖的計數器加1,相應地,在執行monitorexit指令時會將鎖計數器減1,當計數器爲0時,鎖就被釋放了。若是獲取對象鎖失敗了,那當前線程就要阻塞等待,直到對象鎖被另一個線程釋放爲止。數組

JDK1.6後對sysnchronized的優化

在JDK1.6以前,使用sysnchronized同步時,若是要掛起或者喚醒一個線程,都須要操做系統幫忙完成,而操做系統實現線程之間的切換時須要從用戶態轉換到內核態,這個狀態之間的轉換須要相對比較長的時間,時間成本相對較高緩存

JDK1.6以後,JVM對sysnchronized進行了大量優化,從原來的重量級鎖到如今的鎖的不一樣階段升級 無鎖 -> 偏向鎖 -> 輕量級鎖及自旋鎖 -> 重量級鎖安全

  • 偏向鎖多線程

    當進行同步時,偏向於第一個得到它的線程,若是在接下來的執行中,該鎖沒有被其餘線程獲取,那麼持有偏向鎖的線程就不須要進行同步併發

    可是對於鎖競爭比較激烈的場合,偏向鎖就失效了,由於這樣場合極有可能每次申請鎖的線程都是不相同的,此時,偏向鎖會升級爲輕量級鎖ide

  • 輕量級鎖

    是指當鎖是偏向鎖的時候,被另外的線程所訪問,偏向鎖就會升級爲輕量級鎖,其餘線程會經過CAS自旋的形式嘗試獲取鎖,不會阻塞,從而提升性能。

    但若是存在鎖競爭,除了互斥量開銷外,還會額外發生CAS操做,所以在有鎖競爭的狀況下,輕量級鎖比傳統的重量級鎖更慢!若是鎖競爭激烈,那麼輕量級將很快膨脹爲重量級鎖!

  • 自旋鎖和自適應自旋鎖

    輕量級鎖失敗後,虛擬機爲了不線程真實地在操做系統層面掛起,還會進行一項稱爲自旋鎖的優化手段。讓線程自旋的方式等待一段時間

    自適應的自旋鎖:自旋的時間不在固定了,而是和前一次同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定。

  • 鎖消除

    指的就是虛擬機即便編譯器在運行時,若是檢測到那些共享數據不可能存在競爭,那麼就執行鎖消除。鎖消除能夠節省毫無心義的請求鎖的時間。

  • 鎖粗化

    若是一系列的連續操做都對同一個對象反覆加鎖和解鎖,會帶來不少沒必要要的性能消耗,經過對連續操做的一次加鎖和解鎖(及鎖的粗化)來節省時間

顯式鎖

經過JDK層面AQS實現的鎖,須要咱們經過編程實現,如調用lock()、unlock()

public class MyExample {

    private int count = 0;
    private final Lock lock = new ReentrantLock();

    class addHundredNum extends Thread {
        @Override
        public void run() {
            lock.lock();
            try {
                for (int i = 0; i < 1000; i++) {
                    count++;
                }
            } finally {
                lock.unlock();
            }
        }
    }

    public void test() throws InterruptedException {
        addHundredNum[] addHundredNums = new addHundredNum[100];
        for (int i = 0; i < addHundredNums.length; i++) {
            addHundredNums[i] = new addHundredNum();
        }

        for (addHundredNum addHundredNum : addHundredNums) {
            addHundredNum.start();
        }

        for (addHundredNum addHundredNum : addHundredNums) {
            addHundredNum.join();
        }
    }

    public static void main(String[] args) throws Exception {
        MyExample example = new MyExample();
        example.test();
        System.out.println(example.count);
    }
}
複製代碼

AQS詳解參考:JAVA多線程 - AQS詳解

CAS操做

經過使用原子類的CAS方法來實現

public class MyExample {
    private AtomicInteger count = new AtomicInteger(0);

    class addHundredNum extends Thread {
        @Override
        public void run() {
                for (int i = 0; i < 1000; i++) {
                    count.getAndAdd(1);
                }
        }
    }

    public void test() throws InterruptedException {
        addHundredNum[] addHundredNums = new addHundredNum[100];
        for (int i = 0; i < addHundredNums.length; i++) {
            addHundredNums[i] = new addHundredNum();
        }

        for (addHundredNum addHundredNum : addHundredNums) {
            addHundredNum.start();
        }

        for (addHundredNum addHundredNum : addHundredNums) {
            addHundredNum.join();
        }
    }

    public static void main(String[] args) throws Exception {
        MyExample example = new MyExample();
        example.test();
        System.out.println(example.count);
    }
}
複製代碼

JDK8可使用新增LongAdder類實現,該類自己會分紅多個區域,多線程寫入時,寫入對應區域,讀取會將整個區域統計輸入。

拓展

什麼是CAS操做

CAS全稱 Compare And Swap(比較與交換),是一種無鎖算法。在不使用鎖(沒有線程被阻塞)的狀況下實現多線程之間的變量同步。

CAS算法涉及到三個操做數:

  • 須要讀寫的內存值 V。
  • 進行比較的值 A。
  • 要寫入的新值 B。

當且僅當 V 的值等於 A 時,CAS經過原子方式用新值B來更新V的值(「比較+更新」總體是一個原子操做),不然不會執行任何操做。通常狀況下,「更新」是一個不斷重試的操做。

CAS操做存在的問題
  1. ABA問題。CAS須要在操做值的時候檢查內存值是否發生變化,沒有發生變化纔會更新內存值。可是若是內存值原來是A,後來變成了B,而後又變成了A,那麼CAS進行檢查時會發現值沒有發生變化,可是其實是有變化的。ABA問題的解決思路就是在變量前面添加版本號,每次變量更新的時候都把版本號加一,這樣變化過程就從「A-B-A」變成了「1A-2B-3A」。
    • JDK從1.5開始提供了AtomicStampedReference類來解決ABA問題,具體操做封裝在compareAndSet()中。compareAndSet()首先檢查當前引用和當前標誌與預期引用和預期標誌是否相等,若是都相等,則以原子方式將引用值和標誌的值設置爲給定的更新值。
  2. 循環時間長開銷大。CAS操做若是長時間不成功,會致使其一直自旋,給CPU帶來很是大的開銷。
  3. 只能保證一個共享變量的原子操做。對一個共享變量執行操做時,CAS可以保證原子操做,可是對多個共享變量操做時,CAS是沒法保證操做的原子性的。
    • Java從1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,能夠把多個變量放在一個對象裏來進行CAS操做。

volatile

volatile關鍵字使用時,只能做用於變量,且並不能保證不一樣線程中的同步,故沒法實現上面的同步的例子,接下來咱們來介紹一下volatile關鍵字的做用:

  1. 保證不一樣線程中變量的可見性

    volatile英譯易揮發的,表示修飾的變量是不穩定的,易改變,故採用volatile修飾後,會將變量放到主內存中,不會放到每一個線程的cpu高速緩存後在讀取,而是直接所用線程都經過到主內存去讀取,以保證變量在每一個線程的可見性。

    然而,這並不意味着變量的線程安全,不一樣線程cpu進行運算存在時間差,如當多個線程同時對該變量進行++操做時,可能其中一個線程讀取時變量值爲1,這時另一個線程也讀取變量值爲1,第一個線程cpu進行+1操做運行完畢並已經寫回內存,而另外一個線程cpu才進行+1操做運算並寫入內存,此時一個線程的結果被覆蓋,致使線程不安全。

  2. 防止新建對象的重排序現象

    當變量採用volatile修飾後,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。如保守策略的JMM內存屏障插入策略:

  • 在每一個volatile寫操做的前面插入一個StoreStore屏障。

  • 在每一個volatile寫操做的後面插入一個StoreLoad屏障。

  • 在每一個volatile讀操做的後面插入一個LoadLoad屏障。

  • 在每一個volatile讀操做的後面插入一個LoadStore屏障。

具體例子可參考文章雙重校驗鎖實現的單例模式中的volatile關鍵字的做用

Ref

  1. 《深刻理解Java虛擬機:JVM高級特性與最佳實踐》第十三章

  2. 不可不說的Java「鎖」事

  3. Java併發:volatile內存可見性和指令重排

相關文章
相關標籤/搜索