

樂觀鎖與悲觀鎖
樂觀鎖和悲觀鎖都是用于解決并發(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í)行操作。

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

樂觀鎖的實(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();
}
}
}