多線程安全性和Java中的鎖

Java是天生的并發(fā)語言。多線程在帶來更高效率的同時,又帶來了數(shù)據(jù)安全性問題。一般我們將多線程的數(shù)據(jù)安全性問題分為三種:原子性、可見性和有序性。原子性是指我們的一系列操作要么全部都做,要么全部不做??梢娦允侵府?dāng)一個線程修改了一個共享變量后,這個修改能夠及時地被另一個線程看到。有序性是指在java為了性能優(yōu)化,會對指令進(jìn)行重排序,在本線程中我們的前后操作看起來是有序的,但是如果在另一個線程中觀察,我們的操作是無序的。
為了解決多線程的數(shù)據(jù)安全性問題,java中引入了鎖,鎖是為了防止在多線程同時讀寫一個共享內(nèi)存時出現(xiàn)的并發(fā)數(shù)據(jù)安全性問題。Java中的鎖大體分為兩類:"synchronized"關(guān)鍵字鎖和"JUC"(java.util.concurrent包)中的locks包和atomic中提供的鎖。

原子性,可見性和有序性

原子性

原子性是指我們的一系列操作是一個整體,要么全部做,要么全部不做,不能不分做,否則就會產(chǎn)生數(shù)據(jù)安全性問題。請看一個例子:

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class AtomicityViolation {

    static long counter = 0L;
    static ExecutorService executorService = Executors.newFixedThreadPool(10);

    public static void main(String[] args) {
        violateAtomicity();
    }

    static void violateAtomicity() {
        CountDownLatch latch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 10000; j++) {
                        counter++;
                    }
                    latch.countDown();
                }
            });
        }
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter);
        executorService.shutdown();
    }
}

在上面的例子中,我們開啟10個線程,每個線程負(fù)責(zé)對一個counter計數(shù)器累計10000次,如果沒有安全性問題,我們期望得到的結(jié)果是100000,可是事實卻并不如此,并且每次運行的結(jié)果都不一樣,但是總是小于等于100000。為什么會這樣呢?原因就在于counter++操作并不是一個原子操作。java內(nèi)存模型規(guī)定了6種原子操作:read、load、assign、use、store和write。
如果我們要保證counter++是一個原子操作必須要對這個操作加鎖:

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SafeCounter {

    static long counter = 0L;
    static ExecutorService executorService = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException {
        safeCount();
    }

    static void safeCount() throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            executorService.submit(() -> {
                for (int j = 0; j < 10000; j++) {
                    synchronized (SafeCounter.class) {
                        counter++;
                    }
                }
                latch.countDown();
            });
        }
        latch.await();
        System.out.println(volatileCounter);
        executorService.shutdown();
    }
}

經(jīng)過加鎖處理后可以得到預(yù)期的結(jié)果。注意上面加鎖處理在for循環(huán)中,一般我們不這么寫,應(yīng)該將加鎖處理放到循環(huán)體外。這里只是為了說明原子性操作才這么寫。

可見性

java內(nèi)存模型規(guī)定,每個java線程可以有自己的工作內(nèi)存,工作內(nèi)存是線程私有的,而共享內(nèi)存(主存)是線程共享的。線程工作內(nèi)存中會有共享變量的副本,當(dāng)線程對一個共享變量進(jìn)行寫入時,會先寫入線程私有的工作內(nèi)存,然后再刷新到主存中。
這樣就可能會產(chǎn)生一個問題:線程1改變了共享變量的值,在還未刷新到主存時候,線程2去讀取這個變量,此時線程2將看不到線程1對這個變量所做的修改。這就是多線程并發(fā)帶來的數(shù)據(jù)可見性問題。

java內(nèi)存模型

java中可以通過申明一個變量為volatile來解決可見性問題。線程讀取一個volatile變量時JMM會強(qiáng)制要求線程從主內(nèi)存中讀取,寫一個volatile變量時JMM會要求立馬刷新到主內(nèi)存中。java中通過synchronized加鎖后的寫入也可以保證數(shù)據(jù)的可見性。
volatile能夠解決可見性和有序性但是不能保證原子性,如果需要保證原子性則需要加鎖。<font color="#FF0000">這里有一點需要注意的是:volatile類型的long,double變量的讀取是原子讀取,而非volatile的long,double類型變量讀取是非原子讀取,所以也可以說volatile在一定程度上解決了原子性問題。</font>

有序性

如果在本線程內(nèi)觀察,所有操作都是有序的,但是如果在一個線程觀察另一個線程,所有的操作都是無序的。產(chǎn)生這種問題的根本原因在于"指令重排序"和"工作內(nèi)存和主內(nèi)存同步延遲"。java中volatile變量通過內(nèi)存屏障來防止指令重排序從而保證有序。

java中的鎖

上面介紹了多線程并發(fā)中的數(shù)據(jù)安全性問題:原子性、可見性和有序性。java中的鎖就是用來保證這三條特性。java中的鎖可以分為兩大類:synchronized鎖和JUC包中的Lock鎖。

synchronized鎖

synchronized加鎖方式

synchronized是jvm中的一個關(guān)鍵字,它有兩種使用方式:加在方法上或者代碼塊上。
加在方法上:

synchronized void foo() {
    //...
}

如果加在方法上且當(dāng)前方法是非"static"方法,則鎖住的是當(dāng)前類的實例,如果該方法是"static"的,則鎖住的是當(dāng)前類的class對象。
加在代碼塊上:

void foo() {
    synchronized(lock) {
        //...
    }
}

對于加在代碼塊的鎖,鎖住的是'lock'代表的對象。

synchronized鎖特性

synchronized鎖是JVM提供的內(nèi)置鎖。synchronized鎖是非公平的鎖,并且是阻塞的,不支持鎖請求中斷。synchronized鎖是可重入的,所謂可重入是指同一個線程獲取到某個對象的鎖之后在未釋放鎖之前還可以通過synchronized再次獲取鎖,而不會阻塞。一個對象在JVM中的內(nèi)存布局包括對象頭、實例數(shù)據(jù)和對齊填充,synchronized鎖就是通過對象頭來實現(xiàn)鎖的。synchronized還支持偏向鎖、輕量級鎖和重量級鎖。

偏向鎖

大多數(shù)情況下鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得。偏向鎖的目的是在某個線程獲得鎖之后,消除這個線程鎖重入(CAS)的開銷,看起來讓這個線程得到了偏護(hù)。另外,JVM對那種會有多線程加鎖,但不存在鎖競爭的情況也做了優(yōu)化,聽起來比較拗口,但在現(xiàn)實應(yīng)用中確實是可能出現(xiàn)這種情況,因為線程之前除了互斥之外也可能發(fā)生同步關(guān)系,被同步的兩個線程(一前一后)對共享對象鎖的競爭很可能是沒有沖突的。對這種情況,JVM用一個epoch表示一個偏向鎖的時間戳(真實地生成一個時間戳代價還是蠻大的,因此這里應(yīng)當(dāng)理解為一種類似時間戳的identifier)

  1. 偏向鎖的獲?。寒?dāng)一個線程訪問同步塊并獲取鎖時,<font color="#FF0000">會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進(jìn)入和退出同步塊時不需要花費CAS操作來加鎖和解鎖,而只需簡單的測試一下對象頭的Mark Word里是否存儲著指向當(dāng)前線程的偏向鎖,如果測試成功,表示線程已經(jīng)獲得了鎖,如果測試失敗,則需要再測試下Mark Word中偏向鎖的標(biāo)識是否設(shè)置成1(表示當(dāng)前是偏向鎖),如果沒有設(shè)置,則使用CAS競爭鎖,如果設(shè)置了,則嘗試使用CAS將對象頭的偏向鎖指向當(dāng)前線程。</font>

  2. 偏向鎖的撤銷:偏向鎖使用了一種等到競爭出現(xiàn)才釋放鎖的機(jī)制,所以當(dāng)其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節(jié)碼正在執(zhí)行),它會首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否活著,如果線程不處于活動狀態(tài),則將對象頭設(shè)置成無鎖狀態(tài),如果線程仍然活著,擁有偏向鎖的棧會被執(zhí)行,遍歷偏向?qū)ο蟮逆i記錄,棧中的鎖記錄和對象頭的Mark Word,要么重新偏向于其他線程,要么恢復(fù)到無鎖或者標(biāo)記對象不適合作為偏向鎖,最后喚醒暫停的線程。

  3. 偏向鎖的設(shè)置:關(guān)閉偏向鎖:偏向鎖在Java 6和Java 7里是默認(rèn)啟用的,但是它在應(yīng)用程序啟動幾秒鐘之后才激活,如有必要可以使用JVM參數(shù)來關(guān)閉延遲-XX:BiasedLockingStartupDelay = 0。如果你確定自己應(yīng)用程序里所有的鎖通常情況下處于競爭狀態(tài),可以通過JVM參數(shù)關(guān)閉偏向鎖-XX:-UseBiasedLocking=false,那么默認(rèn)會進(jìn)入輕量級鎖狀態(tài)。

輕量級鎖和重量級鎖

  1. 輕量級鎖,加鎖:線程在執(zhí)行同步塊之前,JVM會先在當(dāng)前線程的棧楨中創(chuàng)建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復(fù)制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當(dāng)前線程獲得鎖,如果失敗,則自旋獲取鎖,當(dāng)自旋獲取鎖仍然失敗時,表示存在其他線程競爭鎖(兩條或兩條以上的線程競爭同一個鎖),則輕量級鎖會膨脹成重量級鎖。解鎖:輕量級解鎖時,會使用原子的CAS操作來將Displaced Mark Word替換回到對象頭,如果成功,則表示同步過程已完成。如果失敗,表示有其他線程嘗試過獲取該鎖,則要在釋放鎖的同時喚醒被掛起的線程。

  2. 重量級鎖:重量鎖在JVM中又叫對象監(jiān)視器(Monitor),它很像C中的Mutex,除了具備Mutex(0|1)互斥的功能,它還負(fù)責(zé)實現(xiàn)了Semaphore(信號量)的功能,也就是說它至少包含一個競爭鎖的隊列,和一個信號阻塞隊列(wait隊列),前者負(fù)責(zé)做互斥,后一個用于做線程同步。

鎖膨脹

"JUC"框架提供的鎖

java.util.concurrent(JUC)包中主要有l(wèi)ocks包和atomic包,locks包中提供了Lock鎖,包括可重入鎖(ReentrantLock),可重入讀寫鎖(ReentrantReadWriteLock),和StampedLock。atomic包中提供了基于"CAS"(Compare And Set)的樂觀鎖的一些類。

ReentrantLock

ReentrantLock顧名思義,它是一種可重入鎖,其相對synchronized鎖而言支持鎖中斷,公平鎖等特性。ReentrantLock源碼中涉及加鎖主要的方法有:

public ReentrantLock(boolean fair) { // 支持公平鎖
    sync = fair ? new FairSync() : new NonfairSync();
}
public void lock() {
    sync.lock();
}

public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}

public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}

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

lock()方法用于同步獲取鎖,如果獲取不到鎖,線程將一直阻塞到可以獲取鎖為止。lockInterruptibly()方法用于同步獲取鎖,但是這個請求是可以中斷的。tryLock()方法不會阻塞等待,如果當(dāng)前鎖沒有被其他線程獲取,當(dāng)前線程加鎖后返回true,如果當(dāng)前鎖已經(jīng)被其他線程獲取了,則該方法立馬返回false,不會阻塞等待。tryLock(long timeout, TimeUnit unit)相對于tryLock()方法多了一個超時機(jī)制,如果在指定超時時間之內(nèi)還沒有獲取到鎖則返回false,不會立馬返回false,在超時之前該請求也可以被中斷。注意JUC中的Lock需要我們手動釋放鎖,如果獲取鎖后方法異常也請記得釋放鎖(在finally中釋放鎖),否則其他線程就無法獲取鎖了。synchronized鎖在方法異常時JVM會自動為我們釋放鎖。這也是兩者的不同之處。

ReentrantReadWriteLock

ReentrantLock獲取的是排它鎖,而ReentrantReadWriteLock是一種讀寫鎖分離的鎖。在寫鎖沒有被獲取的情況下,多線程并發(fā)獲取寫鎖不會出現(xiàn)阻塞,在讀多寫少的情況下較ReentrantLock有明顯的優(yōu)勢。
假設(shè)線程1首先獲取讀鎖或者寫鎖,此時線程2再來請求獲取讀鎖或者寫鎖的情況如下圖:

線程1\線程2
×
× ×

StampedLock

首先StampedLock鎖是不可重入的。StampedLock的思想是:<font color="#FF0000">讀請求不僅不應(yīng)該阻塞讀請求(對應(yīng)于ReentrantReadWriteLock),也不應(yīng)該阻塞寫請求。</font>
StampedLock控制鎖有三種模式(寫,讀,樂觀讀),一個StampedLock狀態(tài)是由版本和模式兩個部分組成,鎖獲取方法返回一個數(shù)字作為票據(jù)stamp,它用相應(yīng)的鎖狀態(tài)表示并控制訪問,數(shù)字0表示沒有寫鎖被授權(quán)訪問。在讀鎖上分為悲觀鎖和樂觀鎖。

所謂的樂觀讀模式,也就是若讀的操作很多,寫的操作很少的情況下,你可以樂觀地認(rèn)為,寫入與讀取同時發(fā)生幾率很少,因此不悲觀地使用完全的讀取鎖定,程序可以查看讀取資料之后,是否遭到寫入執(zhí)行的變更,再采取后續(xù)的措施(重新讀取變更信息,或者拋出異常) ,這一個小小改進(jìn),可大幅度提高程序的吞吐量?。?br> 下面是java doc提供的StampedLock一個例子:

class Point {

    private final StampedLock sl = new StampedLock();

    private double x, y;
    
    void move(double deltaX, double deltaY) { // an exclusively locked method
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }
    
    //下面看看樂觀讀鎖案例
    double distanceFromOrigin() { // A read-only method
        long stamp = sl.tryOptimisticRead(); //獲得一個樂觀讀鎖
        double currentX = x, currentY = y; //將兩個字段讀入本地局部變量
        if (!sl.validate(stamp)) { //檢查發(fā)出樂觀讀鎖后同時是否有其他寫鎖發(fā)生?
            stamp = sl.readLock(); //如果沒有,我們再次獲得一個讀悲觀鎖
            try {
                currentX = x; // 將兩個字段讀入本地局部變量
                currentY = y; // 將兩個字段讀入本地局部變量
            } finally {
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
    
    //下面是悲觀讀鎖案例
    void moveIfAtOrigin(double newX, double newY) { // upgrade
        // Could instead start with optimistic, not read mode
        long stamp = sl.readLock();
        try {
            while (x == 0.0 && y == 0.0) { //循環(huán),檢查當(dāng)前狀態(tài)是否符合
                long ws = sl.tryConvertToWriteLock(stamp); //將讀鎖轉(zhuǎn)為寫鎖
                if (ws != 0L) { //這是確認(rèn)轉(zhuǎn)為寫鎖是否成功
                    stamp = ws; //如果成功 替換票據(jù)
                    x = newX; //進(jìn)行狀態(tài)改變
                    y = newY; //進(jìn)行狀態(tài)改變
                    break;
                } else { //如果不能成功轉(zhuǎn)換為寫鎖
                    sl.unlockRead(stamp); //我們顯式釋放讀鎖
                    stamp = sl.writeLock(); //顯式直接進(jìn)行寫鎖 然后再通過循環(huán)再試
                }
            }
        } finally {
            sl.unlock(stamp); //釋放讀鎖或?qū)戞i
        }
    }
}

基于"CAS"樂觀鎖的atomic類

java.util.concurrent.atomic包下提供了一些AtomicXXX類,例如:AtomicInteger,AtomicLong,AtomicBoolean等類。這些類通過"CAS"自旋鎖來保證線程安全性。相對于JUC locks包中的鎖,它不需要掛起和喚醒線程,通過線程"忙自旋"避免系統(tǒng)調(diào)用。他的優(yōu)點是沒有系統(tǒng)調(diào)用不需要掛起和喚醒線程,他的缺點是會過度占用CPU,無法解決"ABA"問題(ABA問題可以通過AtomicStampedReference類來解決)。

參考資料

《深入理解Java虛擬機(jī)》

?著作權(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)容