關(guān)于java中的鎖的理解

Java 鎖

樂觀鎖與悲觀鎖

樂觀鎖和悲觀鎖都是用于解決并發(fā)場(chǎng)景下的數(shù)據(jù)競(jìng)爭(zhēng)問題,但是卻是兩種完全不同的思想。它們的使用非常廣泛,也不局限于某種編程語言或數(shù)據(jù)庫。

樂觀鎖的概念

所謂的樂觀鎖,指的是在操作數(shù)據(jù)的時(shí)候非常樂觀,樂觀地認(rèn)為別人不會(huì)同時(shí)修改數(shù)據(jù),因此樂觀鎖不會(huì)上鎖,只有在執(zhí)行更新的時(shí)候才會(huì)去判斷在此期間別人是否修改了數(shù)據(jù),如果別人修改了數(shù)據(jù)則放棄操作,否則執(zhí)行操作。


樂觀鎖執(zhí)行過程

悲觀鎖的概念

所謂的悲觀鎖,指的是在操作數(shù)據(jù)的時(shí)候比較悲觀,悲觀地認(rèn)為別人一定會(huì)同時(shí)修改數(shù)據(jù),因此悲觀鎖在操作數(shù)據(jù)時(shí)是直接把數(shù)據(jù)上鎖,直到操作完成之后才會(huì)釋放鎖,在上鎖期間其他人不能操作數(shù)據(jù)。

悲觀鎖的執(zhí)行過程

樂觀鎖的實(shí)現(xiàn)方式

樂觀鎖的實(shí)現(xiàn)方式主要有兩種,一種是CAS(Compare and Swap,比較并交換)機(jī)制,一種是版本號(hào)機(jī)制。

CAS機(jī)制

CAS操作包括了三個(gè)操作數(shù),分別是需要讀取的內(nèi)存位置(V)、進(jìn)行比較的預(yù)期值(A)和擬寫入的新值(B),操作邏輯是,如果內(nèi)存位置V的值等于預(yù)期值A(chǔ),則將該位置更新為新值B,否則不進(jìn)行操作。另外,許多CAS操作都是自旋的,意思就是,如果操作不成功,就會(huì)一直重試,直到操作成功為止。

版本號(hào)機(jī)制

版本號(hào)機(jī)制的基本思路,是在數(shù)據(jù)中增加一個(gè)version字段用來表示該數(shù)據(jù)的版本號(hào),每當(dāng)數(shù)據(jù)被修改版本號(hào)就會(huì)加1。當(dāng)某個(gè)線程查詢數(shù)據(jù)的時(shí)候,會(huì)將該數(shù)據(jù)的版本號(hào)一起讀取出來,之后在該線程需要更新該數(shù)據(jù)的時(shí)候,就將之前讀取的版本號(hào)與當(dāng)前版本號(hào)進(jìn)行比較,如果一致,則執(zhí)行操作,如果不一致,則放棄操作。

悲觀鎖的實(shí)現(xiàn)方式

悲觀鎖的實(shí)現(xiàn)方式也就是加鎖,加鎖既可以在代碼層面(比如Java中的synchronized關(guān)鍵字),也可以在數(shù)據(jù)庫層面(比如MySQL中的排他鎖)。

樂觀鎖與悲觀鎖的優(yōu)缺點(diǎn)和使用場(chǎng)景

樂觀鎖和悲觀鎖并沒有優(yōu)劣之分,它們有各自適合的場(chǎng)景。

功能限制

樂觀鎖與悲觀鎖相比,適用的場(chǎng)景受到了更多的限制,無論是CAS機(jī)制還是版本號(hào)機(jī)制。

比如,CAS機(jī)制只能保證單個(gè)變量操作的原子性,當(dāng)涉及到多個(gè)變量的時(shí)候,CAS機(jī)制是無能為力的,而synchronized卻可以通過對(duì)整個(gè)代碼塊進(jìn)行加鎖處理;再比如,版本號(hào)機(jī)制如果在查詢數(shù)據(jù)的時(shí)候是針對(duì)表1,而更新數(shù)據(jù)的時(shí)候是針對(duì)表2,也很難通過簡(jiǎn)單的版本號(hào)來實(shí)現(xiàn)樂觀鎖。

競(jìng)爭(zhēng)激烈程度

在競(jìng)爭(zhēng)不激烈(出現(xiàn)并發(fā)沖突的概率比較?。┑膱?chǎng)景中,樂觀鎖更有優(yōu)勢(shì)。因?yàn)楸^鎖會(huì)鎖住代碼塊或數(shù)據(jù),其他的線程無法同時(shí)訪問,必須等待上一個(gè)線程釋放鎖才能進(jìn)入操作,會(huì)影響并發(fā)的響應(yīng)速度。另外,加鎖和釋放鎖都需要消耗額外的系統(tǒng)資源,也會(huì)影響并發(fā)的處理速度。

在競(jìng)爭(zhēng)激烈(出現(xiàn)并發(fā)沖突的概率較大)的場(chǎng)景中,悲觀鎖則更有優(yōu)勢(shì)。因?yàn)闃酚^鎖在執(zhí)行更新的時(shí)候,可能會(huì)因?yàn)閿?shù)據(jù)被反復(fù)修改而更新失敗,進(jìn)而不斷重試,造成CPU資源的浪費(fèi)。

樂觀鎖是否會(huì)加鎖

樂觀鎖本身是不加鎖的,只有在更新的時(shí)候才會(huì)去判斷數(shù)據(jù)是否被其他線程更新了,比如AtomicInteger便是一個(gè)例子。但是有時(shí)候樂觀鎖可能會(huì)與加鎖操作合作,比如MySQL在執(zhí)行更新數(shù)據(jù)操作的時(shí)候會(huì)加上排他鎖。因此可以理解為樂觀鎖本身是不加鎖的,只有在更新數(shù)據(jù)的時(shí)候才有可能會(huì)加鎖。

CAS的缺點(diǎn)

CAS的缺點(diǎn)主要有ABA問題、高競(jìng)爭(zhēng)下的開銷問題和本身的功能限制。

ABA問題

所謂的ABA問題,指的就是一個(gè)線程在操作數(shù)據(jù)的時(shí)候,有別的線程對(duì)數(shù)據(jù)進(jìn)行了一系列操作,但是在該線程重新讀取該數(shù)據(jù)的時(shí)候,被修改過的數(shù)據(jù)卻和該線程一開始讀取的數(shù)據(jù)一致,該線程不會(huì)知道該數(shù)據(jù)已經(jīng)被修改過了,然后CAS操作就被判斷是成功了。

ABA問題在一些場(chǎng)景下可能不會(huì)造成什么危害,但是在某些場(chǎng)景中卻可能會(huì)造成隱患。比如CAS操作的是棧頂?shù)臄?shù)據(jù),棧頂?shù)臄?shù)據(jù)雖然經(jīng)過兩次(或多次)變化后又恢復(fù)了原值,但是棧卻可能是發(fā)生了變化,棧中數(shù)據(jù)的變化就可能會(huì)引發(fā)一些問題。

對(duì)于ABA問題,比較有效的方案是引入版本號(hào)。只要內(nèi)存中的值發(fā)生變化,版本號(hào)就加1,在進(jìn)行CAS操作的時(shí)候不僅比較內(nèi)存中的值,也比較版本號(hào),只有當(dāng)二者都沒有變化的時(shí)候,CAS操作才能執(zhí)行成功。Java中的AtomicStampedReference類便是適用版本號(hào)來解決ABA問題的。

高競(jìng)爭(zhēng)下的開銷問題

在并發(fā)沖突的概率較大的高競(jìng)爭(zhēng)場(chǎng)景下,如果CAS操作一直失敗,就會(huì)一直重試,造成CPU開銷大的問題。針對(duì)這個(gè)問題,一個(gè)簡(jiǎn)單的思路是引入退出機(jī)制,如果重試次數(shù)超過一定閾值,就強(qiáng)制失敗退出。當(dāng)然了,最好是避免在高競(jìng)爭(zhēng)的場(chǎng)景下使用樂觀鎖。

自身的功能限制

CAS的功能是比較受限的,比如CAS只能保證單個(gè)變量(或者說單個(gè)內(nèi)存值)操作的原子性。這就意味著原子性不一定能保證線程安全,當(dāng)涉及到多個(gè)變量(或者說多個(gè)內(nèi)存值),CAS也是無能為力。除此之外,CAS的實(shí)現(xiàn)需要硬件層面處理器的支持,在Java中普通的用戶是無法直接使用的,只有借助atomic包下的原子類才能使用,靈活性有限。

公平鎖與非公平鎖

公平鎖的概念

公平鎖 是指多個(gè)線程按照申請(qǐng)鎖的順序來獲取鎖,類似排隊(duì)打飯,先來后到。

非公平鎖的概念

非公平鎖 是指多個(gè)線程獲取鎖的順序并不是按照申請(qǐng)鎖的順序,有可能后申請(qǐng)的線程比先申請(qǐng)的線程優(yōu)先獲取鎖,在高并發(fā)的情況,有可能會(huì)造成優(yōu)先級(jí) 反轉(zhuǎn)饑餓現(xiàn)象。

兩者區(qū)別

公平鎖 / 非公平鎖

并發(fā)包中ReentrantLock的創(chuàng)建可以指定構(gòu)造函數(shù)的boolean類型來得到公平鎖或非公平鎖,默認(rèn)是非公平鎖。

關(guān)于兩者的區(qū)別

公平鎖Threads acquire a fair lock in the order which they requested it.

公平鎖,就是很公平,在并發(fā)環(huán)境中,每個(gè)線程在獲取鎖時(shí)會(huì)先查看此鎖維護(hù)的等待隊(duì)列,如果為空,或者當(dāng)前線程是等待隊(duì)列的第一個(gè),就占有鎖,否則就會(huì)加入到等待隊(duì)列中,以后按照FIFO的規(guī)則從隊(duì)列中取到自己。

非公平鎖a nonfair lock permits barging: treads requesting a lock can jump ahead of the queue of waiting threads if the lock happens to be available when it is requested.

非公平鎖,畢竟粗魯,上來就直接嘗試占有鎖,如果嘗試失敗,就再采用類似公平鎖的那種方式。

題外話

Java ReentrantLock而言

通過構(gòu)造函數(shù)指定該鎖是否是公平鎖,默認(rèn)是非公平鎖。非公平鎖的優(yōu)點(diǎn)在于吞吐量比公平鎖大。

對(duì)于Synchronized而言,也是一種非公平鎖。

自旋鎖與非自旋鎖

自旋鎖的概念

自旋鎖(spinlock),是指嘗試獲取鎖的線程不會(huì)立即阻塞,而是采用循環(huán)的方式去嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗,缺點(diǎn)是循環(huán)會(huì)消耗CPU。

自旋鎖的優(yōu)點(diǎn)

自旋鎖不會(huì)使線程狀態(tài)發(fā)生切換,一直處于用戶態(tài),即線程一直都是active的;不會(huì)使線程進(jìn)入阻塞狀態(tài),減少了不必要的上下文切換,執(zhí)行速度快

非自旋鎖在獲取不到鎖的時(shí)候會(huì)進(jìn)入阻塞狀態(tài),從而進(jìn)入內(nèi)核態(tài),當(dāng)獲取到鎖的時(shí)候需要從內(nèi)核態(tài)恢復(fù),需要線程上下文切換。 (線程被阻塞后便進(jìn)入內(nèi)核(Linux)調(diào)度狀態(tài),這個(gè)會(huì)導(dǎo)致系統(tǒng)在用戶態(tài)與內(nèi)核態(tài)之間來回切換,嚴(yán)重影響鎖的性能)

// unsafe.getAndAddInt
public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

Demo1# 實(shí)現(xiàn)一個(gè)自旋鎖

package com.nuih.lock;

import sun.misc.Unsafe;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 題目:實(shí)現(xiàn)一個(gè)自旋鎖
 * 自旋鎖好處:循環(huán)比較獲取直到成功為止,沒有類似wait的阻塞
 *
 * 通過cas操作完成自旋鎖,A線程先進(jìn)來調(diào)用myLock方法自己持有鎖5秒鐘,
 * B隨后進(jìn)來后發(fā)現(xiàn)當(dāng)前又線程持有鎖,不是null,所以只能通過自旋等待,直到A釋放鎖后B隨后搶到
 *
 * @author: hejianhui
 * @create: 2020-06-21 15:05
 * @see SpinLockDemo
 * @since JDK1.8
 */
public class SpinLockDemo {

    // 原子引用
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void myLock(){
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + "\t come in ??");
        while (!atomicReference.compareAndSet(null,thread)){

        }
    }

    public void myUnLock(){
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread,null);
        System.out.println(thread.getName() + "\t invoked myUnLock");
    }

    public static void main(String[] args) throws InterruptedException {
        SpinLockDemo spinLockDemo = new SpinLockDemo();

        new Thread(() -> {
            spinLockDemo.myLock();

            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.myUnLock();

        },"A").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {
            spinLockDemo.myLock();

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.myUnLock();
        },"B").start();

    }
}

可重入鎖(又名遞歸鎖)

可重入鎖的概念

可重入鎖(也叫做遞歸鎖)

指的是同一線程外層函數(shù)獲得鎖之后,內(nèi)層遞歸函數(shù)仍然能獲取該鎖的代碼,在同一個(gè)線程在外層方法獲取鎖的時(shí)候,在進(jìn)入內(nèi)層方法會(huì)自動(dòng)獲取鎖。也就是說,線程可以進(jìn)入任何一個(gè)它已經(jīng)擁有的鎖所同步著的代碼快。

ReentrantLock / Synchronized 就是一個(gè)典型的可重入鎖

可重入鎖的最大作用是避免死鎖

Demo2# 可重入鎖

package com.nuih.lock;

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

class MyPhone implements Runnable {

    public synchronized void sendMsg(){
        System.out.println(Thread.currentThread().getName() + "\t invoked sendMsg");
        sendEmail();
    }

    public synchronized void sendEmail(){
        System.out.println(Thread.currentThread().getName() + "\t##### invoked sendEmail");
    }

    Lock lock = new ReentrantLock();

    @Override
    public void run() {
        get();
    }

    public void get(){
        lock.lock();
        try{
            System.out.println(Thread.currentThread().getName() + "\t invoked get");
            set();
        }finally {
            lock.unlock();
        }
    }

    public void set(){
        lock.lock();
        try{
            System.out.println(Thread.currentThread().getName() + "\t###### invoked set");
        }finally {
            lock.unlock();
        }
    }
}

/**
 * 可重入鎖(也叫做遞歸鎖)
 *
 * 指的是同一線程外層函數(shù)獲得鎖之后,內(nèi)層遞歸函數(shù)仍然能獲取該鎖的代碼,
 * 在同一個(gè)線程在外層方法獲取鎖的時(shí)候,在進(jìn)入內(nèi)層方法會(huì)自動(dòng)獲取鎖。
 *
 * 也就是說,線程可以進(jìn)入任何一個(gè)它已經(jīng)擁有的鎖所同步著的代碼快。
 *
 * t1 invoked sendMsg()        t1線程在外層方法獲取鎖的時(shí)候
 * t1 ##### invoked sendEmail  t1在進(jìn)入內(nèi)層方法會(huì)自動(dòng)獲取鎖
 *
 * t2 invoked sendMsg()
 * t2 ##### invoked sendEmail
 *
 * @author: hejianhui
 * @create: 2020-06-21 14:10
 * @see ReentrantLockDemo
 * @since JDK1.8
 */
public class ReentrantLockDemo {

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

        MyPhone myPhone = new MyPhone();

        new Thread(() -> {
            myPhone.sendMsg();
        },"t1").start();

        new Thread(() -> {
            myPhone.sendMsg();
        },"t2").start();


        TimeUnit.SECONDS.sleep(1);
        System.out.println("\n\n\n\n");

        Thread t3 = new Thread(myPhone,"t3");
        t3.start();

        Thread t4 = new Thread(myPhone,"t4");
        t4.start();

    }
}

獨(dú)占鎖(寫鎖)/共享鎖(寫鎖)/互斥鎖

是什么?

獨(dú)占鎖:指該鎖一次只能被一個(gè)線程所持有。對(duì)ReentrantLock和Synchronized而言都是獨(dú)占鎖

共享鎖:指該鎖可被多個(gè)線程所持有。

對(duì)ReentrantReadWriteLock其讀鎖是共享鎖,其寫鎖是獨(dú)占鎖。

讀鎖的共享鎖可保證并發(fā)讀是非常高效的,讀寫、寫讀、寫寫的過程是互斥的。

Demo#3 獨(dú)占鎖/共享鎖

package com.nuih.lock;

import com.nuih.map.HashMap;
import com.nuih.map.Map;

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class MyCache{

    private volatile Map<String,Object> map = new HashMap<>();
    final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public void put(String key,Object value){
        readWriteLock.writeLock().lock();
        try{
            System.out.println(Thread.currentThread().getName() + "\t 開始寫入:" + key);
            map.put(key,value);
            System.out.println(Thread.currentThread().getName() + "\t 寫入完成");
        }finally {
            readWriteLock.writeLock().unlock();
        }
    }

    public void get(String key){
        readWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t 開始讀取");
            Object result = map.get(key);
            System.out.println(Thread.currentThread().getName() + "\t 讀物完成:" + result);
        }finally {
            readWriteLock.readLock().unlock();
        }
    }
}

/**
 * 多個(gè)線程同時(shí)讀一個(gè)資源類沒有任何問題,所以為了滿足并發(fā)量,讀取共享資源應(yīng)該可以同時(shí)進(jìn)行
 * 但是
 * 如果又一個(gè)線程想去寫共享資源了,就不應(yīng)該再有其它線程可以對(duì)該資源進(jìn)行讀或?qū)? * 小總結(jié):
 *      讀-讀可以共存
 *      讀-寫不能共存
 *      寫-寫不能共存
 *
 *      寫操作:原子+獨(dú)占,中間過程必須一個(gè)完整的統(tǒng)一體,中間不許被分割
 *
 * @author: hejianhui
 * @create: 2020-06-21 15:39
 * @see ReadWriteLockDemo
 * @since JDK1.8
 */
public class ReadWriteLockDemo {

    public static void main(String[] args) {

        MyCache myCache = new MyCache();

        for(int i=0;i<5;i++){
            int finalI = i;
            new Thread(() -> {
                myCache.put(finalI+"",finalI);
            },"A"+String.valueOf(i)).start();
        }

        for(int i=0;i<5;i++){
            int finalI = i;
            new Thread(() -> {
                myCache.get(finalI+"");
            },"B"+String.valueOf(i)).start();
        }
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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