AQS底層原理、運用和volatile詳解

AbstractQueuedSynchronizer

什么叫做AQS?從名字可以看出,AQS就是抽象隊列同步器,是用來構(gòu)建鎖或者其他同步組件的基礎(chǔ)框架。它使用了一個int成員變量表示同步狀態(tài),通過內(nèi)置的FIFO隊列來完成資源獲取線程的排隊工作。并發(fā)包的大師(Doug Lea)期望它能夠成為實現(xiàn)大部分同步需求的基礎(chǔ)。

AQS使用方式和其中的設(shè)計模式

AQS的主要使用方式是繼承,子類通過繼承AQS并實現(xiàn)它的抽象方法來管理同步狀態(tài),在AQS里由一個int型的state來代表這個狀態(tài),在抽象方法的實現(xiàn)過程中免不了要對同步狀態(tài)進行更改,這時就需要使用同步器提供的3個方法getState()、setState(int newState)compareAndSetState(int expect,int update)來進行操作,因為它們能夠保證狀態(tài)的改變是安全的。

  /**
   * The synchronization state.
   */
  private volatile int state;

在實現(xiàn)上,子類推薦被定義為自定義同步組件的靜態(tài)內(nèi)部類,AQS自身沒有實現(xiàn)任何同步接口,它僅僅是定義了若干同步狀態(tài)獲取和釋放的方法來供自定義同步組件使用,同步器既可以支持獨占式地獲取同步狀態(tài),也可以支持共享式地獲取同步狀態(tài),這樣就可以方便實現(xiàn)不同類型的同步組件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。

同步器是實現(xiàn)鎖(也可以是任意同步組件)的關(guān)鍵,在鎖的實現(xiàn)中聚合同步器??梢赃@樣理解二者之間的關(guān)系:鎖是面向使用者的,它定義了使用者與鎖交互的接口(比如可以允許兩個線程并行訪問),隱藏了實現(xiàn)細(xì)節(jié);

同步器面向的是鎖的實現(xiàn)者,它簡化了鎖的實現(xiàn)方式,屏蔽了同步狀態(tài)管理、線程的排隊、等待與喚醒等底層操作。鎖和同步器很好地隔離了使用者和實現(xiàn)者所需關(guān)注的領(lǐng)域。

實現(xiàn)者需要繼承同步器并重寫指定的方法,隨后將同步器組合在自定義同步組件的實現(xiàn)中,并調(diào)用同步器提供的模板方法,而這些模板方法將會調(diào)用使用者重寫的方法。

模板方法模式

同步器的設(shè)計基于模板方法模式。模板方法模式的意圖是,定義一個操作中的算法的骨架,而將一些步驟的實現(xiàn)延遲到子類中。模板方法使得子類可以不改變一個算法的結(jié)構(gòu)即可重定義該算法的某些特定步驟。我們最常見的就是Spring框架里的各種Template。

來看這么一個例子:

我們開了個蛋糕店,蛋糕店不能只賣一種蛋糕呀,于是我們決定先賣奶油蛋糕,芝士蛋糕和慕斯蛋糕。三種蛋糕在制作方式上一樣,都包括造型,烘焙和涂抹蛋糕上的東西。所以可以定義一個抽象蛋糕模型。

/**
 * 類說明:抽象蛋糕模型
 */
public abstract class AbstractCake {
    protected abstract void shape();
    protected abstract void apply();
    protected abstract void brake();

    /*模板方法*/
   public final void run(){
       this.shape();
       this.apply();
       this.brake();
   }
}

然后就可以批量生產(chǎn)三種蛋糕。

/**
 * 類說明:芝士蛋糕
 */
public class CheeseCake  extends AbstractCake {

    @Override
    protected void shape() {
        System.out.println("芝士蛋糕造型");
    }

    @Override
    protected void apply() {
        System.out.println("芝士蛋糕涂抹");
    }

    @Override
    protected void brake() {
        System.out.println("芝士蛋糕烘焙");
    }
}

public class CreamCake extends AbstractCake {}
public class MouseCake extends AbstractCake {}

當(dāng)我們使用的時候,則可以

/**
 * 類說明:生產(chǎn)蛋糕
 */
public class MakeCake {
    public static void main(String[] args) {
        AbstractCake cake = new CheeseCake();
        AbstractCake cake2 = new CreamCake();
        //AbstractCake cake = new MouseCake();
        cake.run();
    }
}

這樣一來,不但可以批量生產(chǎn)三種蛋糕,而且如果日后有擴展,只需要繼承抽象蛋糕方法就可以了,十分方便,我們天天生意做得越來越賺錢。突然有一天,我們發(fā)現(xiàn)市面有一種最簡單的小蛋糕銷量很好,這種蛋糕就是簡單烘烤成型就可以賣,并不需要涂抹什么食材,由于制作簡單銷售量大,這個品種也很賺錢,于是我們也想要生產(chǎn)這種蛋糕。

但是我們發(fā)現(xiàn)了一個問題,抽象蛋糕是定義了抽象的涂抹方法的,也就是說擴展的這種蛋糕是必須要實現(xiàn)涂抹方法,這就很雞兒蛋疼了。怎么辦?我們可以將原來的模板修改為帶鉤子的模板。

 /*模板方法*/
    public final void run(){
        this.shape();
        if(this.shouldApply()){
            this.apply();
        }
        this.brake();
    }

    protected boolean shouldApply(){
        return true;
    }

做小蛋糕的時候通過flag來控制是否涂抹,其余已有的蛋糕制作不需要任何修改可以照常進行。

/**
 * 類說明:小蛋糕
 */
public class SmallCake extends AbstractCake {

    private boolean flag = false;
    public void setFlag(boolean shouldApply){
        flag = shouldApply;
    }
    @Override
    protected boolean shouldApply() {
        return this.flag;
    }

}

了解AQS其中的方法

實現(xiàn)自定義同步組件時,將會調(diào)用同步器提供的模板方法,而AQS為我們提供了如下所示的模版方法:

方法名稱 描述
void acquire(int arg) 獨占式獲取同步狀態(tài),如果當(dāng)前線程獲取同步狀態(tài)成功,則由該方法返回,否則,將會進入同步隊列等待,該方法將會調(diào)用重寫的tryAcquire(intarg)方法
void acquireInterruptibly(int arg) 與acquire(int arg)相同,但是該方法響應(yīng)中斷,當(dāng)前線程未獲取到同步狀態(tài)而進入同步隊列中,如果當(dāng)前線程被中斷,則該方法會拋出InterruptedException 并返回
boolean tryAcquireNanos(int arg,long nanos) 在acquirenterpibyit arg)基礎(chǔ)上增加了超時限制,如果當(dāng)前線程在超時時間內(nèi)沒有獲取到同步狀態(tài)。那么將會返回false. 如果獲取到了返回true
void acquireShared(int arg) 共享式的獲取同步狀態(tài),如果當(dāng)前線程未獲取到同步狀態(tài),將會進入同步隊列等待,與獨占式獲取的主要區(qū)別是在同一時刻可以有多個線程獲取到同步狀態(tài) void acquireSharedInteruptibly(int arg) 與acquireShared(int arg)相同,該方法響應(yīng)中斷
boolean tryAcquireSbaredNanos(intarg, long nanos) 在acquiresharedntepriblylit arg)基硝上增加了超時限制
boolean release(int arg) 獨占式的釋放同步狀態(tài),該方法會在釋放同步狀態(tài)之后,將同步隊列中第個節(jié)點包含的線程喚醒
bboolean releaseShared(int arg) 共享式的釋放同步狀態(tài)
ClelinrThreaea getQuevedTmeads() 獲取等待在同步隊列上的線程集合

這些模板方法同步器提供的模板方法基本上分為3類:獨占式獲取與釋放同步狀態(tài)、共享式獲取與釋放、同步狀態(tài)和查詢同步隊列中的等待線程情況。

可重寫的方法

方法名稱 描述
protected boolean tryAcquiret(int arg) 獨占式獲取同步狀態(tài),實現(xiàn)該方法需要查詢當(dāng)前狀態(tài),判斷網(wǎng)步狀態(tài)是否符合預(yù)期,然后再進行CAS設(shè)置同步狀態(tài)
protected boolean tryRelease(int arg) 獨占式釋放同少狀態(tài), 等待獲取同步狀態(tài)的線程將有機會獲取同步狀態(tài)
protected int tryAcquireshared(in ag) 共享式獲取同步狀態(tài),返回大于等于0的值,表示獲取成功,反之獲取失敗
protected boolean tryReleaseShared(int arg) 共享式釋放同步狀態(tài)
protected boolean isHeldExchusively() 判斷是否當(dāng)前線程所獨占

例如,我們來實現(xiàn)一個自己的獨占鎖,則可以基于以上方法。通過一個繼承自AbstractQueuedSynchronizer的靜態(tài)內(nèi)部類,實現(xiàn)AQS提供的模版方法,并使用其作為代理作為獨占鎖的真實實現(xiàn)。

public class SelfLock implements Lock {
// 靜態(tài)內(nèi)部類,自定義同步器
    private static class Sync extends AbstractQueuedSynchronizer {

        /*判斷處于占用狀態(tài)*/
        @Override
        protected boolean isHeldExclusively() {
            return getState()==1;
        }

        /*獲得鎖*/
        @Override
        protected boolean tryAcquire(int arg) {
            if(compareAndSetState(0,1)){
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        /*釋放鎖*/
        @Override
        protected boolean tryRelease(int arg) {
            if(getState()==0){
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            //compareAndSetState(1,0);
            return true;
        }

        // 返回一個Condition,每個condition都包含了一個condition隊列
        Condition newCondition() {
            return new ConditionObject();
        }
    }

    // 僅需要將操作代理到Sync上即可
    private final Sync sync = new Sync();

    @Override
    public void lock() {
        System.out.println(Thread.currentThread().getName()+" ready get lock");
        sync.acquire(1);
        System.out.println(Thread.currentThread().getName()+" already got lock");
    }
    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @Override
    public void unlock() {
        System.out.println(Thread.currentThread().getName()+" ready release lock");
        sync.release(1);
        System.out.println(Thread.currentThread().getName()+" already released lock");
    }

    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }

    public boolean isLocked() {
        return sync.isHeldExclusively();
    }

    public boolean hasQueuedThreads() {
        return sync.hasQueuedThreads();
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    @Override
    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
}

訪問或修改同步狀態(tài)的方法
重寫同步器指定的方法時,需要使用同步器提供的如下3個方法來訪問或修改同步狀態(tài)。

  • getState():獲取當(dāng)前同步狀態(tài)。
  • setState(int newState):設(shè)置當(dāng)前同步狀態(tài)。
  • compareAndSetState(int expect,int update):使用CAS設(shè)置當(dāng)前狀態(tài),該方法能夠保證狀態(tài)設(shè)置的原子性。

CLH隊列鎖

CLH隊列鎖即Craig, Landin, and Hagersten (CLH) locks。

CLH隊列鎖也是一種基于鏈表的可擴展、高性能、公平的自旋鎖,申請線程僅僅在本地變量上自旋,它不斷輪詢前驅(qū)的狀態(tài),假設(shè)發(fā)現(xiàn)前驅(qū)釋放了鎖就結(jié)束自旋。

當(dāng)線程A需要獲取鎖時:

  1. 創(chuàng)建一個的QNode,將其中的locked設(shè)置為true表示需要獲取鎖,myPred表示對其前驅(qū)結(jié)點的引用

  2. 線程A對tail域調(diào)用getAndSet()方法,使自己成為隊列的尾部,同時獲取一個指向其前驅(qū)結(jié)點的引用myPred

線程B需要獲得鎖,同樣的流程再來一遍,myPred指向前驅(qū)結(jié)點A

  1. 線程就在前驅(qū)結(jié)點的locked字段上旋轉(zhuǎn),直到前驅(qū)結(jié)點釋放鎖(前驅(qū)節(jié)點的鎖值 locked == false)
  2. 當(dāng)一個線程需要釋放鎖時,將當(dāng)前結(jié)點的locked域設(shè)置為false,同時回收前驅(qū)結(jié)點

前驅(qū)結(jié)點釋放鎖,線程A的myPred所指向的前驅(qū)結(jié)點的locked字段變?yōu)閒alse,線程A就可以獲取到鎖。

CLH隊列鎖的優(yōu)點是空間復(fù)雜度低(如果有n個線程,L個鎖,每個線程每次只獲取一個鎖,那么需要的存儲空間是O(L+n),n個線程有n個myNode,L個鎖有L個tail)。CLH隊列鎖常用在SMP體系結(jié)構(gòu)下。

Java中的AQS是CLH隊列鎖的一種變體實現(xiàn)。但是它的基本思想還是基于CLH隊列鎖實現(xiàn)的,只不過它的自旋并不是無限自旋,而是自旋一定次數(shù)后,就讓線程進入一個阻塞的狀態(tài)。

實現(xiàn)ReentrantLock鎖

在上面我們實現(xiàn)了一個獨占鎖,但是該獨占鎖只是簡單的實現(xiàn)了同步鎖的基本功能,但并沒有實現(xiàn)公平于非公平,鎖的可重入等機制。

公平和非公平鎖

ReentrantLock的構(gòu)造函數(shù)中,默認(rèn)的無參構(gòu)造函數(shù)將會把Sync對象創(chuàng)建為NonfairSync對象,這是一個“非公平鎖”;而另一個構(gòu)造函數(shù)ReentrantLock(boolean fair)傳入?yún)?shù)為true時將會把Sync對象創(chuàng)建為“公平鎖”FairSync。

nonfairTryAcquire(int acquires)方法,只要CAS設(shè)置同步狀態(tài)成功,則表示當(dāng)前線程獲取了鎖。而公平鎖則不同,tryAcquire()方法與nonfairTryAcquire(int acquires)比較,唯一不同的位置為判斷條件多了hasQueuedPredecessors()方法,即加入了同步隊列中當(dāng)前節(jié)點是否有前驅(qū)節(jié)點的判斷,如果該方法返回true,則表示有線程比當(dāng)前線程更早地請求獲取鎖,因此需要等待前驅(qū)線程獲取并釋放鎖之后才能繼續(xù)獲取鎖。

protected final boolean tryAcquire{
  ...
  if (!hasQueuedPredecessors() &&
    compareAndSetState(0, acquires)) {
    setExclusiveOwnerThread(current);
    return true;
  }
}
鎖的可重入

重進入是指任意線程在獲取到鎖之后能夠再次獲取該鎖而不會被鎖所阻塞,該特性的實現(xiàn)需要解決以下兩個問題。

  1. 線程再次獲取鎖。鎖需要去識別獲取鎖的線程是否為當(dāng)前占據(jù)鎖的線程,如果是,則再次成功獲取。例如線程訪問A()方法需要獲取一次鎖,獲取B()方法也需要獲取一次鎖,如果沒有實現(xiàn)可重入的話,自己將會和自己造成死鎖。
  2. 鎖的最終釋放。線程重復(fù)n次獲取了鎖,隨后在第n次釋放該鎖后,其他線程能夠獲取到該鎖。鎖的最終釋放要求鎖對于獲取進行計數(shù)自增,計數(shù)表示當(dāng)前鎖被重復(fù)獲取的次數(shù),而鎖被釋放時,計數(shù)自減,當(dāng)計數(shù)等于0時表示鎖已經(jīng)成功釋放。

nonfairTryAcquire()方法增加了再次獲取同步狀態(tài)的處理邏輯:通過判斷當(dāng)前線程是否為獲取鎖的線程來決定獲取操作是否成功,如果是獲取鎖的線程再次請求,則將同步狀態(tài)值進行增加并返回true,表示獲取同步狀態(tài)成功。同步狀態(tài)表示鎖被一個線程重復(fù)獲取的次數(shù)。

如果該鎖被獲取了n次,那么前(n - 1)次tryRelease(int releases)方法必須返回false,而只有同步狀態(tài)完全釋放了,才能返回true??梢钥吹剑摲椒▽⑼綘顟B(tài)是否為0作為最終釋放的條件,當(dāng)同步狀態(tài)為0時,將占有線程設(shè)置為null,并返回true,表示釋放成功。

所以,在之前的獨占鎖基礎(chǔ)上要再實現(xiàn)可重入的話,我們需要將代碼進行以下修改:

/**
 *類說明:實現(xiàn)我們自己獨占鎖,可重入
 */
public class ReenterSelfLock implements Lock {
    // 靜態(tài)內(nèi)部類,自定義同步器
    private static class Sync extends AbstractQueuedSynchronizer {
        // 當(dāng)狀態(tài)為0的時候獲取鎖
        public boolean tryAcquire(int acquires) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }else if(getExclusiveOwnerThread() == Thread.currentThread()){
                setState(getState() + 1);
                return  true;
            }
            return false;
        }

        // 釋放鎖,將狀態(tài)設(shè)置為0
        protected boolean tryRelease(int releases) {
            if(getExclusiveOwnerThread() != Thread.currentThread()){
                throw new IllegalMonitorStateException();
            }
            if (getState() == 0)
                throw new IllegalMonitorStateException();
            setState(getState() - 1);
            if(getState() == 0){
                setExclusiveOwnerThread(null);
            }
            return true;
        }
    }

}

我們來測試一下自己實現(xiàn)的可重入鎖。


/**
 * 類說明:
 */
public class TestReenterSelfLock {

    static final Lock lock = new ReenterSelfLock();

    public void reenter(int x) {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + ":遞歸層級:" + x);
            int y = x - 1;
            if (y == 0) return;
            else {
                reenter(y);
            }
        } finally {
            lock.unlock();
        }

    }

    public void test() {
        class Worker extends Thread {
            public void run() {
                System.out.println(Thread.currentThread().getName());
                SleepTools.second(1);
                reenter(3);
            }
        }
        // 啟動3個子線程
        for (int i = 0; i < 3; i++) {
            Worker w = new Worker();
            w.start();
        }
        // 主線程每隔1秒換行
        for (int i = 0; i < 100; i++) {
            SleepTools.second(1);
        }
    }

    public static void main(String[] args) {
        TestReenterSelfLock testMyLock = new TestReenterSelfLock();
        testMyLock.test();
    }
}

深入理解并發(fā)編程歸納和總結(jié)

JMM基礎(chǔ)-計算機原理

Java內(nèi)存模型即Java Memory Model,簡稱JMM。JMM定義了Java 虛擬機(JVM)在計算機內(nèi)存(RAM)中的工作方式。JVM是整個計算機虛擬模型,所以JMM是隸屬于JVM的。Java1.5版本對其進行了重構(gòu),現(xiàn)在的Java仍沿用了Java1.5的版本。物理計算機中的并發(fā)問題,物理機遇到的并發(fā)問題與虛擬機中的情況有不少相似之處,物理機對并發(fā)的處理方案對于虛擬機的實現(xiàn)也有相當(dāng)大的參考意義。

物理計算機中的并發(fā)問題,物理機遇到的并發(fā)問題與虛擬機中的情況有不少相似之處,物理機對并發(fā)的處理方案對于虛擬機的實現(xiàn)也有相當(dāng)大的參考意義。根據(jù)《Jeff Dean在Google全體工程大會的報告》,我們可以看到

操作 響應(yīng)時間
打開一個站點 幾秒
數(shù)據(jù)庫查詢條記錄(有索引) 十幾毫秒
1.6G的CPU執(zhí)行一 條指令 0.6納秒
從機械磁盤順序讀取1M數(shù)據(jù) 2- 10毫秒
從SSD磁盤順序讀取1M數(shù)據(jù) 0.3毫秒
從內(nèi)存連續(xù)讀取1M數(shù)據(jù) 250微秒
CPU讀取一 次內(nèi)存 100納秒
1G網(wǎng)卡,網(wǎng)絡(luò)傳輸2Kb數(shù)據(jù) 20微秒
  • 1秒= 1000毫秒1毫秒=1000微秒 1微秒= 1000納秒

如果從內(nèi)存中讀取1M的int型數(shù)據(jù)由CPU進行累加,耗時要多久?

做個簡單的計算,1M的數(shù)據(jù),Java里int型為32位,4個字節(jié),共有1024*1024/4 = 262144個整數(shù) ,則CPU 計算耗時:262144 *0.6 = 157 286 納秒,而我們知道從內(nèi)存讀取1M數(shù)據(jù)需要250000納秒,兩者雖然有差距(當(dāng)然這個差距并不小,十萬納秒的時間足夠CPU執(zhí)行將近二十萬條指令了),但是還在一個數(shù)量級上。但是,沒有任何緩存機制的情況下,意味著每個數(shù)都需要從內(nèi)存中讀取,這樣加上CPU讀取一次內(nèi)存需要100納秒,262144個整數(shù)從內(nèi)存讀取到CPU加上計算時間一共需要262144*100+250000 = 26 464 400 納秒,這就存在著數(shù)量級上的差異了

而且現(xiàn)實情況中絕大多數(shù)的運算任務(wù)都不可能只靠處理器“計算”就能完成,處理器至少要與內(nèi)存交互,如讀取運算數(shù)據(jù)、存儲運算結(jié)果等,這個I/O操作是基本上是無法消除的(無法僅靠寄存器來完成所有運算任務(wù))。早期計算機中cpu和內(nèi)存的速度是差不多的,但在現(xiàn)代計算機中,cpu的指令速度遠超內(nèi)存的存取速度,由于計算機的存儲設(shè)備與處理器的運算速度有幾個數(shù)量級的差距,所以現(xiàn)代計算機系統(tǒng)都不得不加入一層讀寫速度盡可能接近處理器運算速度的高速緩存(Cache)來作為內(nèi)存與處理器之間的緩沖:將運算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運算能快速進行,當(dāng)運算結(jié)束后再從緩存同步回內(nèi)存之中,這樣處理器就無須等待緩慢的內(nèi)存讀寫了。

在計算機系統(tǒng)中,寄存器劃是L0級緩存,接著依次是L1,L2,L3(接下來是內(nèi)存,本地磁盤,遠程存儲)。越往上的緩存存儲空間越小,速度越快,成本也更高;越往下的存儲空間越大,速度更慢,成本也更低。從上至下,每一層都可以看做是更下一層的緩存,即:L0寄存器是L1一級緩存的緩存,L1是L2的緩存,依次類推;每一層的數(shù)據(jù)都是來至它的下一層,所以每一層的數(shù)據(jù)是下一層的數(shù)據(jù)的子集。

在現(xiàn)代CPU上,一般來說L0, L1,L2,L3都集成在CPU內(nèi)部,而L1還分為一級數(shù)據(jù)緩存(Data Cache,D-Cache,L1d)和一級指令緩存(Instruction Cache,I-Cache,L1i),分別用于存放數(shù)據(jù)和執(zhí)行數(shù)據(jù)的指令解碼。每個核心擁有獨立的運算處理單元、控制器、寄存器、L1、L2緩存,然后一個CPU的多個核心共享最后一層CPU緩存L3。

JMM-Java內(nèi)存模型

從抽象的角度來看,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲在主內(nèi)存(Main Memory)中,每個線程都有一個私有的本地內(nèi)存(Local Memory),本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本。本地內(nèi)存是JMM的一個抽象概念,并不真實存在。它涵蓋了緩存、寫緩沖區(qū)、寄存器以及其他的硬件和編譯器優(yōu)化。

線程不允許直接訪問主內(nèi)存,也不允許去訪問其他線程的本地內(nèi)存,那么JMM會帶來什么樣的問題呢?我們來看這么一個例子:

主內(nèi)存中有一個count變量,初始化為0,有A和B兩個線程對count并行進行 +1 操作。由于工作內(nèi)存的存在,A需要先讀取count值,創(chuàng)建本地副本,進行+1后再寫回主內(nèi)存中。而同時,B也在進行這個操作,但是并不知道A已經(jīng)修改過count的值了,于是最后count的值為1。跟我們想要的結(jié)果并不一致,產(chǎn)生了線程不安全的問題。

可見性

可見性是指當(dāng)多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。

由于線程對變量的所有操作都必須在工作內(nèi)存中進行,而不能直接讀寫主內(nèi)存中的變量,那么對于共享變量V,它們首先是在自己的工作內(nèi)存,之后再同步到主內(nèi)存。可是并不會及時的刷到主存中,而是會有一定時間差。很明顯,這個時候線程 A 對變量 V 的操作對于線程 B 而言就不具備可見性了 。

要解決共享對象可見性這個問題,我們可以使用volatile關(guān)鍵字或者是加鎖。

我們知道,使用volatile關(guān)鍵字可以在讀寫的時候強制線程去訪問最新的主內(nèi)存地址,但是僅僅使用volatile就夠了嗎?

原子性

即一個操作或者多個操作 要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就都不執(zhí)行。

我們都知道CPU資源的分配都是以線程為單位的,并且是分時調(diào)用,操作系統(tǒng)允許某個進程執(zhí)行一小段時間,例如 50 毫秒,過了 50 毫秒操作系統(tǒng)就會重新選擇一個進程來執(zhí)行(我們稱為“任務(wù)切換”),這個 50 毫秒稱為“時間片”。而任務(wù)的切換大多數(shù)是在時間片段結(jié)束以后,

那么線程切換為什么會帶來bug呢?因為操作系統(tǒng)做任務(wù)切換,可以發(fā)生在任何一條CPU 指令執(zhí)行完!注意,是CPU 指令,CPU 指令,而不是高級語言里的一條語句。比如count++,在java里就是一句話,但高級語言里一條語句往往需要多條 CPU 指令完成。其實count++包含了多個CPU指令!

因此,由于count++操作不是原子操作的原因,當(dāng)A和B線程多次調(diào)用count++,假如A已經(jīng)完成了一次累加,count這時候為1,B讀取到工作內(nèi)存中,卻剛好被操作系統(tǒng)剝奪了時間片,而A又再一次進行了count++,count變?yōu)?。對于B來說,當(dāng)它重新分配到CPU時間片的時候,count的副本值仍然是1,這就產(chǎn)生了線程安全問題。

因為volatile不能保證原子性,所以我們可以使用synchronized關(guān)鍵字去保證線程的安全,因為它同時保證了可見性和原子性。

volatile詳解

volatile變量自身具有下列特性:
可見性:對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入。
原子性:對任意單個volatile變量的讀/寫具有原子性,但類似于volatile++這種復(fù)合操作不具有原子性。

因此,可以把對volatile變量的單個讀/寫,看成是使用同一個鎖對這些單個讀/寫操作做了同步。

volatile雖然能保證執(zhí)行完及時把變量刷到主內(nèi)存中,但對于count++這種非原子性、多指令的情況,由于線程切換,線程A剛把count=0加載到工作內(nèi)存,線程B就可以開始工作了,這樣就會導(dǎo)致線程A和B執(zhí)行完的結(jié)果都是1,都寫到主內(nèi)存中,主內(nèi)存的值還是1,而不是2。

volatile的實現(xiàn)原理

通過對OpenJDK中的unsafe.cpp源碼的分析,會發(fā)現(xiàn)被volatile關(guān)鍵字修飾的變量會存在一個“Lock:”的前綴。

Lock不是一種內(nèi)存屏障,但是它能完成類似內(nèi)存屏障的功能,它會對CPU總線和高速緩存加鎖,可以理解為CPU指令級的一種鎖。同時該指令會將當(dāng)前處理器緩存行的數(shù)據(jù)直接寫會到系統(tǒng)內(nèi)存中,且這個寫回內(nèi)存的操作會使在其他CPU里緩存了該地址的數(shù)據(jù)無效。

synchronized的實現(xiàn)原理

Synchronized在JVM里的實現(xiàn)都是基于進入和退出Monitor對象來實現(xiàn)方法同步和代碼塊同步,雖然具體實現(xiàn)細(xì)節(jié)不一樣,但是都可以通過成對的MonitorEnter和MonitorExit指令來實現(xiàn)。

對同步塊,MonitorEnter指令插入在同步代碼塊的開始位置,當(dāng)代碼執(zhí)行到該指令時,將會嘗試獲取該對象Monitor的所有權(quán),即嘗試獲得該對象的鎖,而monitorExit指令則插入在方法結(jié)束處和異常處,JVM保證每個MonitorEnter必須有對應(yīng)的MonitorExit。

對同步方法,從同步方法反編譯的結(jié)果來看,方法的同步并沒有通過指令monitorenter和monitorexit來實現(xiàn),相對于普通方法,其常量池中多了ACC_SYNCHRONIZED標(biāo)示符。

JVM就是根據(jù)該標(biāo)示符來實現(xiàn)方法的同步的:當(dāng)方法被調(diào)用時,調(diào)用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標(biāo)志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先獲取monitor,獲取成功之后才能執(zhí)行方法體,方法執(zhí)行完后再釋放monitor。在方法執(zhí)行期間,其他任何線程都無法再獲得同一個monitor對象。

synchronized使用的鎖是存放在Java對象頭里面。

長度 內(nèi)容 說明
32/64bit Mark Word 存儲對象的hashCode或鎖信息等
32/64bit Class Metadata Address 存儲到對象類型數(shù)據(jù)的指針
32/32bit Array length 數(shù)組的長度 (如果當(dāng)前對象是數(shù)組)

具體位置是對象頭里面的MarkWord,MarkWord里默認(rèn)數(shù)據(jù)是存儲對象的HashCode等信息,

鎖狀態(tài) 25bit 4bit 1bit: 是否是偏向鎖 2bit鎖標(biāo)志位
無鎖狀態(tài) 對象的Hashcode 對象分代年嶺 0 01

但是會隨著對象的運行改變而發(fā)生變化,不同的鎖狀態(tài)對應(yīng)著不同的記錄存儲方式。


了解各種鎖

自旋鎖

原理
自旋鎖原理非常簡單,如果持有鎖的線程能在很短時間內(nèi)釋放鎖資源,那么那些等待競爭鎖的線程就不需要做內(nèi)核態(tài)和用戶態(tài)之間的切換進入阻塞掛起狀態(tài),它們只需要等一等(自旋),等持有鎖的線程釋放鎖后即可立即獲取鎖,這樣就避免用戶線程和內(nèi)核的切換的消耗。但是線程自旋是需要消耗CPU的,說白了就是讓CPU在做無用功,線程不能一直占用CPU自旋做無用功,所以需要設(shè)定一個自旋等待的最大時間。

如果持有鎖的線程執(zhí)行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會導(dǎo)致其它爭用鎖的線程在最大等待時間內(nèi)還是獲取不到鎖,這時爭用線程會停止自旋進入阻塞狀態(tài)。

自旋鎖的優(yōu)缺點
自旋鎖盡可能的減少線程的阻塞,這對于鎖的競爭不激烈,且占用鎖時間非常短的代碼塊來說性能能大幅度的提升,因為自旋的消耗會小于線程阻塞掛起操作的消耗!

但是如果鎖的競爭激烈,或者持有鎖的線程需要長時間占用鎖執(zhí)行同步塊,這時候就不適合使用自旋鎖了,因為自旋鎖在獲取鎖前一直都是占用cpu做無用功,占著XX不XX,線程自旋的消耗大于線程阻塞掛起操作的消耗,其它需要cup的線程又不能獲取到cpu,造成cpu的浪費。

自旋鎖時間閾值
自旋鎖的目的是為了占著CPU的資源不釋放,等到獲取到鎖立即進行處理。但是如何去選擇自旋的執(zhí)行時間呢?如果自旋執(zhí)行時間太長,會有大量的線程處于自旋狀態(tài)占用CPU資源,進而會影響整體系統(tǒng)的性能。因此自旋次數(shù)很重要。

JVM對于自旋次數(shù)的選擇,jdk1.5默認(rèn)為10次,在1.6引入了適應(yīng)性自旋鎖,適應(yīng)性自旋鎖意味著自旋的時間不在是固定的了,而是由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態(tài)來決定,基本認(rèn)為一個線程上下文切換的時間是最佳的一個時間。

JDK1.6中-XX:+UseSpinning開啟自旋鎖; JDK1.7后,去掉此參數(shù),由jvm控制;

鎖的狀態(tài)

一共有四種狀態(tài),無鎖狀態(tài),偏向鎖狀態(tài),輕量級鎖狀態(tài)和重量級鎖狀態(tài),它會隨著競爭情況逐漸升級。鎖可以升級但不能降級,目的是為了提高獲得鎖和釋放鎖的效率。

偏向鎖

引入背景:大多數(shù)情況下鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖,減少不必要的CAS操作。

偏向鎖,顧名思義,它會偏向于第一個訪問鎖的線程,如果在運行過程中,同步鎖只有一個線程訪問,不存在多線程爭用的情況,則線程是不需要觸發(fā)同步的,減少加鎖/解鎖的一些CAS操作(比如等待隊列的一些CAS操作),這種情況下,就會給線程加一個偏向鎖。 如果在運行過程中,遇到了其他線程搶占鎖,則持有偏向鎖的線程會被掛起,JVM會消除它身上的偏向鎖,將鎖恢復(fù)到標(biāo)準(zhǔn)的輕量級鎖。它通過消除資源無競爭情況下的同步原語,進一步提高了程序的運行性能。
偏向鎖獲取過程:

  • 步驟1、 訪問Mark Word中偏向鎖的標(biāo)識是否設(shè)置成1,鎖標(biāo)志位是否為01,確認(rèn)為可偏向狀態(tài)。
  • 步驟2、 如果為可偏向狀態(tài),則測試線程ID是否指向當(dāng)前線程,如果是,進入步驟5,否則進入步驟3。
  • 步驟3、 如果線程ID并未指向當(dāng)前線程,則通過CAS操作競爭鎖。如果競爭成功,則將Mark Word中線程ID設(shè)置為當(dāng)前線程ID,然后執(zhí)行5;如果競爭失敗,執(zhí)行4。
  • 步驟4、 如果CAS獲取偏向鎖失敗,則表示有競爭。當(dāng)?shù)竭_全局安全點(safepoint)時獲得偏向鎖的線程被掛起,偏向鎖升級為輕量級鎖,然后被阻塞在安全點的線程繼續(xù)往下執(zhí)行同步代碼。(撤銷偏向鎖的時候會導(dǎo)致stop the word)
  • 步驟5、 執(zhí)行同步代碼。

偏向鎖的釋放:
偏向鎖的撤銷在上述第四步驟中有提到。偏向鎖只有遇到其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放偏向鎖,線程不會主動去釋放偏向鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節(jié)碼正在執(zhí)行),它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處于被鎖定狀態(tài),撤銷偏向鎖后恢復(fù)到未鎖定(標(biāo)志位為“01”)或輕量級鎖(標(biāo)志位為“00”)的狀態(tài)。

偏向鎖的適用場景
始終只有一個線程在執(zhí)行同步塊,在它沒有執(zhí)行完釋放鎖之前,沒有其它線程去執(zhí)行同步塊,在鎖無競爭的情況下使用,一旦有了競爭就升級為輕量級鎖,升級為輕量級鎖的時候需要撤銷偏向鎖,撤銷偏向鎖的時候會導(dǎo)致stop the word操作;
在有鎖的競爭時,偏向鎖會多做很多額外操作,尤其是撤銷偏向所的時候會導(dǎo)致進入安全點,安全點會導(dǎo)致stw,導(dǎo)致性能下降,這種情況下應(yīng)當(dāng)禁用。

jvm開啟/關(guān)閉偏向鎖
開啟偏向鎖:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
關(guān)閉偏向鎖:-XX:-UseBiasedLocking

輕量級鎖

輕量級鎖是由偏向鎖升級來的,偏向鎖運行在一個線程進入同步塊的情況下,當(dāng)?shù)诙€線程加入鎖爭用的時候,偏向鎖就會升級為輕量級鎖;

輕量級鎖的加鎖過程:
在代碼進入同步塊的時候,如果同步對象鎖狀態(tài)為無鎖狀態(tài)且不允許進行偏向(鎖標(biāo)志位為“01”狀態(tài),是否為偏向鎖為“0”),虛擬機首先將在當(dāng)前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的Mark Word的拷貝,官方稱之為 Displaced Mark Word。

拷貝成功后,虛擬機將使用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針,并將Lock record里的owner指針指向object mark word。如果更新成功,則執(zhí)行步驟4,否則執(zhí)行步驟5。
如果這個更新動作成功了,那么這個線程就擁有了該對象的鎖,并且對象Mark Word的鎖標(biāo)志位設(shè)置為“00”,即表示此對象處于輕量級鎖定狀態(tài)。

如果這個更新操作失敗了,虛擬機首先會檢查對象的Mark Word是否指向當(dāng)前線程的棧幀,如果是就說明當(dāng)前線程已經(jīng)擁有了這個對象的鎖,那就可以直接進入同步塊繼續(xù)執(zhí)行。否則說明多個線程競爭鎖,當(dāng)競爭線程嘗試占用輕量級鎖失敗多次之后,輕量級鎖就會膨脹為重量級鎖,重量級線程指針指向競爭線程,競爭線程也會阻塞,等待輕量級線程釋放鎖后喚醒他。鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,后面等待鎖的線程也要進入阻塞狀態(tài)。

優(yōu)點 缺點 適用場景
偏向鎖 加鎖和解鎖不需要額外的消耗。和執(zhí)行非同步方法比僅存在納秒級的差距。 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 適用于只有一一個線程訪問同步塊場景。
輕量級鎖 競爭的線程不會阻塞,提高了程序的響應(yīng)速度。 如果始終得不到鎖競爭的線程使用自旋會消耗CPU. 追求響應(yīng)時間, 同步塊執(zhí)行速度非???。
重量級鎖 線程競爭不使用自旋,不會消耗CPU. 線程阻塞,響應(yīng)時間緩慢 追求吞吐量,同步塊執(zhí)行速度較長。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容