淺談Java鎖機制
1、悲觀鎖和樂觀鎖
我們可以將鎖大體分為兩類:
- 悲觀鎖
- 樂觀鎖
顧名思義,悲觀鎖總是假設(shè)最壞的情況,每次獲取數(shù)據(jù)的時候都認為別的線程會修改,所以每次在拿數(shù)據(jù)的時候都會上鎖,這樣其它線程想要修改這個數(shù)據(jù)的時候都會被阻塞直到獲取鎖。比如MySQL數(shù)據(jù)庫中的表鎖、行鎖、讀鎖、寫鎖等,Java中的synchronized和ReentrantLock等。
而樂觀鎖總是假設(shè)最好的情況,每次獲取數(shù)據(jù)的時候都認為別的線程不會修改,所以并不會上鎖,但是在修改數(shù)據(jù)的時候需要判斷一下在此期間有沒有別的線程修改過數(shù)據(jù),如果沒有修改過則正常修改,如果修改過則這次修改就是失敗的。常見的樂觀鎖有版本號控制、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個線程,并在線程中對共享變量count進行++操作,所以如果不發(fā)生線程安全問題,最終的結(jié)果應(yīng)該是50000,但該程序中一定存在線程安全問題,運行結(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)鍵字包裹起來,這樣當某個線程在進行++操作時,別的線程是無法同時進行++的,只能等待前一個線程執(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);
}
}
這兩種鎖機制都是悲觀鎖的具體實現(xiàn),不管其它線程是否會同時修改,它都直接上鎖,保證了原子操作。
3、樂觀鎖應(yīng)用
由于線程的調(diào)度是極其耗費操作系統(tǒng)資源的,所以,我們應(yīng)該盡量避免線程在不斷阻塞和喚醒中切換,由此產(chǎn)生了樂觀鎖。
在數(shù)據(jù)庫表中,我們往往會設(shè)置一個version字段,這就是樂觀鎖的體現(xiàn),假設(shè)某個數(shù)據(jù)表的數(shù)據(jù)內(nèi)容如下:
+----+------+----------+ ------- + | id | name | password | version | +----+------+----------+ ------- + | 1 | zs | 123456 | 1 | +----+------+----------+ ------- +
它是如何避免線程安全問題的呢?
假設(shè)此時有兩個線程A、B想要修改這條數(shù)據(jù),它們會執(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;
首先兩個線程均查詢出zs用戶的版本號為1,然后線程A先執(zhí)行了更新操作,此時將用戶的密碼修改為了admin,并將版本號加1,接著線程B執(zhí)行更新操作,此時版本號已經(jīng)為2了,所以更新肯定是失敗的,由此,線程B就失敗了,它只能重新去獲取版本號再進行更新,這就是樂觀鎖,我們并沒有對程序和數(shù)據(jù)庫進行任何的加鎖操作,但它仍然能夠保證線程安全。
4、CAS
仍然以最開始做加法的程序為例,在Java中,我們還可以采用一種特殊的方式來實現(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;
}
當count調(diào)用incrementAndGet()方法時,實際上調(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()方法中有一個循環(huán),關(guān)鍵的代碼就在這里,我們假設(shè)線程A此時進入了該方法,此時var1即為AtomicInteger對象(初始值為0),var2的值為12(這是一個內(nèi)存偏移量,我們可以不用關(guān)心),var4的值為1(準備對count進行加1操作)。
首先通過AtomicInteger對象和內(nèi)存偏移量即可得到主存中的數(shù)據(jù)值:
var5 = this.getIntVolatile(var1, var2);
獲取到var5的值為0,然后程序會進行判斷:
!this.compareAndSwapInt(var1, var2, var5, var5 + var4)
compareAndSwapInt()是一個本地方法,它的作用是比較并交換,即:判斷var1的值與主存中取出的var5的值是否相同,此時肯定是相同的,所以會將var5+var4的值賦值給var1,并返回true,對true取反為false,所以循環(huán)就結(jié)束了,最終方法返回1。
這是一切正常的運行流程,然而當發(fā)生并發(fā)時,處理情況就不太一樣了,假設(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此時獲取到var1的值為0(var1即為共享變量AtomicInteger),當線程A正準備執(zhí)行下去時,線程B搶先執(zhí)行了,線程B此時獲取到var1的值為0,var5的值為0,比較成功,此時var1的值就變?yōu)?;這時候輪到線程A執(zhí)行了,它獲取var5的值為1,此時var1的值不等于var5的值,此次加1操作就會失敗,并重新進入循環(huán),此時var1的值已經(jīng)發(fā)生了變化,此時重新獲取var5的值也為1,比較成功,所以將var1的值加1變?yōu)?,若是在獲取var5之前別的線程又修改了主存中var1的值,則本次操作又會失敗,程序重新進入循環(huán)。
這就是利用自旋的方式來實現(xiàn)一個樂觀鎖,因為它沒有加鎖,所以省下了線程調(diào)度的資源,但也要避免程序一直自旋的情況發(fā)生。
5、手寫一個自旋鎖
public class LockDemo {
private AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void lock() {
// 獲取當前線程對象
Thread thread = Thread.currentThread();
// 自旋等待
while (!atomicReference.compareAndSet(null, thread)) {
}
}
public void unlock() {
// 獲取當前線程對象
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的原理可以輕松地實現(xiàn)一個自旋鎖,首先,AtomicReference中的初始值一定為null,所以第一個線程在調(diào)用lock()方法后會成功將當前線程的對象放入AtomicReference,此時若是別的線程調(diào)用lock()方法,會因為該線程對象與AtomicReference中的對象不同而陷入循環(huán)的等待中,直到第一個線程執(zhí)行完++操作,調(diào)用了unlock()方法,該線程才會將AtomicReference值置為null,此時別的線程就可以跳出循環(huán)了。
通過CAS機制,我們能夠在不添加鎖的情況下模擬出加鎖的效果,但它的缺點也是顯而易見的:
- 循環(huán)等待占用CPU資源
- 只能保證一個變量的原子操作
- 會產(chǎn)生ABA問題
到此這篇關(guān)于淺談Java鎖機制的文章就介紹到這了,更多相關(guān)Java鎖機制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot中注解實現(xiàn)定時任務(wù)的兩種方式
這篇文章主要介紹了SpringBoot中注解實現(xiàn)定時任務(wù)的兩種方式,SpringBoot 定時任務(wù)是一種在SpringBoot應(yīng)用中自動執(zhí)行任務(wù)的機制,通過使用Spring框架提供的@Scheduled注解,我們可以輕松地創(chuàng)建定時任務(wù),需要的朋友可以參考下2023-10-10
java案例實戰(zhàn)之字符串轉(zhuǎn)換為二進制
最近遇到個需求,要求編寫一個程序,從鍵盤錄入一個字符串,將字符串轉(zhuǎn)換為二進制數(shù),下面這篇文章主要給大家介紹了關(guān)于java字符串轉(zhuǎn)換為二進制的相關(guān)資料,需要的朋友可以參考下2023-06-06
java正則表達式匹配網(wǎng)頁所有網(wǎng)址和鏈接文字的示例
這篇文章主要介紹了java正則表達式匹配網(wǎng)頁所有網(wǎng)址和鏈接文字java正則表達式匹配,需要的朋友可以參考下2014-03-03
JetBrains IntelliJ IDEA 2020安裝與使用教程詳解
這篇文章主要介紹了JetBrains IntelliJ IDEA 2020安裝與使用教程,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-06-06
Java和MySQL數(shù)據(jù)庫中關(guān)于小數(shù)的保存問題詳析
在Java和MySQL中小數(shù)的精度可能會受到限制,如float類型的小數(shù)只能精確到6-7位,double類型也只能精確到15-16位,這篇文章主要給大家介紹了關(guān)于Java和MySQL數(shù)據(jù)庫中關(guān)于小數(shù)的保存問題,需要的朋友可以參考下2024-01-01
scala當中的文件操作和網(wǎng)絡(luò)請求的實現(xiàn)方法
這篇文章主要介紹了scala當中的文件操作和網(wǎng)絡(luò)請求的實現(xiàn)方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-06-06
java優(yōu)先隊列PriorityQueue中Comparator的用法詳解
這篇文章主要介紹了java優(yōu)先隊列PriorityQueue中Comparator的用法詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-02-02

