淺談Java鎖機(jī)制
1、悲觀鎖和樂觀鎖
我們可以將鎖大體分為兩類:
- 悲觀鎖
- 樂觀鎖
顧名思義,悲觀鎖總是假設(shè)最壞的情況,每次獲取數(shù)據(jù)的時(shí)候都認(rèn)為別的線程會(huì)修改,所以每次在拿數(shù)據(jù)的時(shí)候都會(huì)上鎖,這樣其它線程想要修改這個(gè)數(shù)據(jù)的時(shí)候都會(huì)被阻塞直到獲取鎖。比如MySQL
數(shù)據(jù)庫中的表鎖、行鎖、讀鎖、寫鎖等,Java
中的synchronized
和ReentrantLock
等。
而樂觀鎖總是假設(shè)最好的情況,每次獲取數(shù)據(jù)的時(shí)候都認(rèn)為別的線程不會(huì)修改,所以并不會(huì)上鎖,但是在修改數(shù)據(jù)的時(shí)候需要判斷一下在此期間有沒有別的線程修改過數(shù)據(jù),如果沒有修改過則正常修改,如果修改過則這次修改就是失敗的。常見的樂觀鎖有版本號(hào)控制、CAS算法等。
2、悲觀鎖應(yīng)用
案例如下:
public class LockDemo { static int count = 0; public static void main(String[] args) throws InterruptedException { List<Thread> threadList = new ArrayList<>(); for (int i = 0; i < 50; i++) { Thread thread = new Thread(() -> { for (int j = 0; j < 1000; ++j) { count++; } }); thread.start(); threadList.add(thread); } // 等待所有線程執(zhí)行完畢 for (Thread thread : threadList) { thread.join(); } System.out.println(count); } }
在該程序中一共開啟了50個(gè)線程,并在線程中對(duì)共享變量count
進(jìn)行++操作,所以如果不發(fā)生線程安全問題,最終的結(jié)果應(yīng)該是50000,但該程序中一定存在線程安全問題,運(yùn)行結(jié)果為:
48634
若想解決線程安全問題,可以使用synchronized
關(guān)鍵字:
public class LockDemo { static int count = 0; public static void main(String[] args) throws InterruptedException { List<Thread> threadList = new ArrayList<>(); for (int i = 0; i < 50; i++) { Thread thread = new Thread(() -> { // 使用synchronized關(guān)鍵字解決線程安全問題 synchronized (LockDemo.class) { for (int j = 0; j < 1000; ++j) { count++; } } }); thread.start(); threadList.add(thread); } for (Thread thread : threadList) { thread.join(); } System.out.println(count); } }
將修改count
變量的操作使用synchronized
關(guān)鍵字包裹起來,這樣當(dāng)某個(gè)線程在進(jìn)行++操作時(shí),別的線程是無法同時(shí)進(jìn)行++的,只能等待前一個(gè)線程執(zhí)行完1000次后才能繼續(xù)執(zhí)行,這樣便能保證最終的結(jié)果為50000。
使用ReentrantLock
也能夠解決線程安全問題:
public class LockDemo { static int count = 0; public static void main(String[] args) throws InterruptedException { List<Thread> threadList = new ArrayList<>(); Lock lock = new ReentrantLock(); for (int i = 0; i < 50; i++) { Thread thread = new Thread(() -> { // 使用ReentrantLock關(guān)鍵字解決線程安全問題 lock.lock(); try { for (int j = 0; j < 1000; ++j) { count++; } } finally { lock.unlock(); } }); thread.start(); threadList.add(thread); } for (Thread thread : threadList) { thread.join(); } System.out.println(count); } }
這兩種鎖機(jī)制都是悲觀鎖的具體實(shí)現(xiàn),不管其它線程是否會(huì)同時(shí)修改,它都直接上鎖,保證了原子操作。
3、樂觀鎖應(yīng)用
由于線程的調(diào)度是極其耗費(fèi)操作系統(tǒng)資源的,所以,我們應(yīng)該盡量避免線程在不斷阻塞和喚醒中切換,由此產(chǎn)生了樂觀鎖。
在數(shù)據(jù)庫表中,我們往往會(huì)設(shè)置一個(gè)version
字段,這就是樂觀鎖的體現(xiàn),假設(shè)某個(gè)數(shù)據(jù)表的數(shù)據(jù)內(nèi)容如下:
+----+------+----------+ ------- + | id | name | password | version | +----+------+----------+ ------- + | 1 | zs | 123456 | 1 | +----+------+----------+ ------- +
它是如何避免線程安全問題的呢?
假設(shè)此時(shí)有兩個(gè)線程A、B想要修改這條數(shù)據(jù),它們會(huì)執(zhí)行如下的sql語句:
select version from e_user where name = 'zs'; update e_user set password = 'admin',version = version + 1 where name = 'zs' and version = 1;
首先兩個(gè)線程均查詢出zs用戶的版本號(hào)為1,然后線程A先執(zhí)行了更新操作,此時(shí)將用戶的密碼修改為了admin
,并將版本號(hào)加1,接著線程B執(zhí)行更新操作,此時(shí)版本號(hào)已經(jīng)為2了,所以更新肯定是失敗的,由此,線程B就失敗了,它只能重新去獲取版本號(hào)再進(jìn)行更新,這就是樂觀鎖,我們并沒有對(duì)程序和數(shù)據(jù)庫進(jìn)行任何的加鎖操作,但它仍然能夠保證線程安全。
4、CAS
仍然以最開始做加法的程序?yàn)槔?,在Java中,我們還可以采用一種特殊的方式來實(shí)現(xiàn)它:
public class LockDemo { static AtomicInteger count = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { List<Thread> threadList = new ArrayList<>(); for (int i = 0; i < 50; i++) { Thread thread = new Thread(() -> { for (int j = 0; j < 1000; ++j) { // 使用AtomicInteger解決線程安全問題 count.incrementAndGet(); } }); thread.start(); threadList.add(thread); } for (Thread thread : threadList) { thread.join(); } System.out.println(count); } }
為何使用AtomicInteger
類就能夠解決線程安全問題呢?
我們來查看一下源碼:
public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; }
當(dāng)count
調(diào)用incrementAndGet()
方法時(shí),實(shí)際上調(diào)用的是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; }
getAndAddInt()
方法中有一個(gè)循環(huán),關(guān)鍵的代碼就在這里,我們假設(shè)線程A此時(shí)進(jìn)入了該方法,此時(shí)var1
即為AtomicInteger
對(duì)象(初始值為0),var2
的值為12(這是一個(gè)內(nèi)存偏移量,我們可以不用關(guān)心),var4的值為1(準(zhǔn)備對(duì)count進(jìn)行加1操作)。
首先通過AtomicInteger
對(duì)象和內(nèi)存偏移量即可得到主存中的數(shù)據(jù)值:
var5 = this.getIntVolatile(var1, var2);
獲取到var5的值為0,然后程序會(huì)進(jìn)行判斷:
!this.compareAndSwapInt(var1, var2, var5, var5 + var4)
compareAndSwapInt()
是一個(gè)本地方法,它的作用是比較并交換,即:判斷var1的值與主存中取出的var5的值是否相同,此時(shí)肯定是相同的,所以會(huì)將var5+var4
的值賦值給var1,并返回true
,對(duì)true
取反為false
,所以循環(huán)就結(jié)束了,最終方法返回1。
這是一切正常的運(yùn)行流程,然而當(dāng)發(fā)生并發(fā)時(shí),處理情況就不太一樣了,假設(shè)此時(shí)線程A執(zhí)行到了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; }
線程A此時(shí)獲取到var1的值為0(var1即為共享變量AtomicInteger
),當(dāng)線程A正準(zhǔn)備執(zhí)行下去時(shí),線程B搶先執(zhí)行了,線程B此時(shí)獲取到var1的值為0,var5的值為0,比較成功,此時(shí)var1的值就變?yōu)?;這時(shí)候輪到線程A執(zhí)行了,它獲取var5的值為1,此時(shí)var1的值不等于var5的值,此次加1操作就會(huì)失敗,并重新進(jìn)入循環(huán),此時(shí)var1的值已經(jīng)發(fā)生了變化,此時(shí)重新獲取var5
的值也為1,比較成功,所以將var1的值加1變?yōu)?,若是在獲取var5之前別的線程又修改了主存中var1的值,則本次操作又會(huì)失敗,程序重新進(jìn)入循環(huán)。
這就是利用自旋的方式來實(shí)現(xiàn)一個(gè)樂觀鎖,因?yàn)樗鼪]有加鎖,所以省下了線程調(diào)度的資源,但也要避免程序一直自旋的情況發(fā)生。
5、手寫一個(gè)自旋鎖
public class LockDemo { private AtomicReference<Thread> atomicReference = new AtomicReference<>(); public void lock() { // 獲取當(dāng)前線程對(duì)象 Thread thread = Thread.currentThread(); // 自旋等待 while (!atomicReference.compareAndSet(null, thread)) { } } public void unlock() { // 獲取當(dāng)前線程對(duì)象 Thread thread = Thread.currentThread(); atomicReference.compareAndSet(thread, null); } static int count = 0; public static void main(String[] args) throws InterruptedException { LockDemo lockDemo = new LockDemo(); List<Thread> threadList = new ArrayList<>(); for (int i = 0; i < 50; i++) { Thread thread = new Thread(() -> { lockDemo.lock(); for (int j = 0; j < 1000; j++) { count++; } lockDemo.unlock(); }); thread.start(); threadList.add(thread); } // 等待線程執(zhí)行完畢 for (Thread thread : threadList) { thread.join(); } System.out.println(count); } }
使用CAS的原理可以輕松地實(shí)現(xiàn)一個(gè)自旋鎖,首先,AtomicReference
中的初始值一定為null
,所以第一個(gè)線程在調(diào)用lock()方法后會(huì)成功將當(dāng)前線程的對(duì)象放入AtomicReference
,此時(shí)若是別的線程調(diào)用lock()
方法,會(huì)因?yàn)樵摼€程對(duì)象與AtomicReference
中的對(duì)象不同而陷入循環(huán)的等待中,直到第一個(gè)線程執(zhí)行完++操作,調(diào)用了unlock()
方法,該線程才會(huì)將AtomicReference
值置為null
,此時(shí)別的線程就可以跳出循環(huán)了。
通過CAS機(jī)制,我們能夠在不添加鎖的情況下模擬出加鎖的效果,但它的缺點(diǎn)也是顯而易見的:
- 循環(huán)等待占用CPU資源
- 只能保證一個(gè)變量的原子操作
- 會(huì)產(chǎn)生ABA問題
到此這篇關(guān)于淺談Java鎖機(jī)制的文章就介紹到這了,更多相關(guān)Java鎖機(jī)制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Java多線程+鎖機(jī)制實(shí)現(xiàn)簡(jiǎn)單模擬搶票的項(xiàng)目實(shí)踐
- Java中的CAS鎖機(jī)制(無鎖、自旋鎖、樂觀鎖、輕量級(jí)鎖)詳解
- Java中的CAS無鎖機(jī)制實(shí)現(xiàn)原理詳解
- java鎖機(jī)制ReentrantLock源碼實(shí)例分析
- java synchronized 鎖機(jī)制原理詳解
- Java鎖機(jī)制Lock用法示例
- Java線程并發(fā)中常見的鎖機(jī)制詳細(xì)介紹
- Java 多線程同步 鎖機(jī)制與synchronized深入解析
- Java 并發(fā)編程中的鎖機(jī)制示例詳解
相關(guān)文章
feign post參數(shù)對(duì)象不加@RequestBody的使用說明
這篇文章主要介紹了feign post參數(shù)對(duì)象不加@RequestBody的使用說明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10SpringCloud Alibaba使用Seata處理分布式事務(wù)的技巧
在傳統(tǒng)的單體項(xiàng)目中,我們使用@Transactional注解就能實(shí)現(xiàn)基本的ACID事務(wù)了,隨著微服務(wù)架構(gòu)的引入,需要對(duì)數(shù)據(jù)庫進(jìn)行分庫分表,每個(gè)服務(wù)擁有自己的數(shù)據(jù)庫,這樣傳統(tǒng)的事務(wù)就不起作用了,那么我們?nèi)绾伪WC多個(gè)服務(wù)中數(shù)據(jù)的一致性呢?跟隨小編一起通過本文了解下吧2021-06-06SpringBoot項(xiàng)目中的視圖解析器問題(兩種)
SpringBoot官網(wǎng)推薦使用HTML視圖解析器,但是根據(jù)個(gè)人的具體業(yè)務(wù)也有可能使用到JSP視圖解析器,所以本文介紹了兩種視圖解析器,感興趣的可以了解下2020-06-06idea2023創(chuàng)建JavaWeb教程之右鍵沒有Servlet的問題解決
最近在寫一個(gè)javaweb項(xiàng)目,但是在IDEA中創(chuàng)建好項(xiàng)目后,在搭建結(jié)構(gòu)的時(shí)候創(chuàng)建servlet文件去沒有選項(xiàng),所以這里給大家總結(jié)下,這篇文章主要給大家介紹了關(guān)于idea2023創(chuàng)建JavaWeb教程之右鍵沒有Servlet問題的解決方法,需要的朋友可以參考下2023-10-10在SpringBoot: SpringBoot里面創(chuàng)建導(dǎo)出Excel的接口教程
這篇文章主要介紹了在SpringBoot: SpringBoot里面創(chuàng)建導(dǎo)出Excel的接口教程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-10-10詳解Java模擬棧的實(shí)現(xiàn)以及Stack類的介紹
棧是一種數(shù)據(jù)結(jié)構(gòu),它按照后進(jìn)先出的原則來存儲(chǔ)和訪問數(shù)據(jù)。Stack是一個(gè)類,表示棧數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn)。本文就來和大家介紹一下Java模擬棧的實(shí)現(xiàn)以及Stack類的使用,需要的可以參考一下2023-04-04