java多線程中的鎖分類多種多樣,其中有一種主要的分類方式就是樂觀和悲觀進行劃分的。
一、樂觀鎖概念
說是寫樂觀鎖的概念,但是通常樂觀鎖和悲觀鎖的概念都要一塊寫。對比着來才更有意義。
1、悲觀鎖概念
悲觀鎖:總是假設最壞的情況,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞,直到它拿到鎖。
就比如說java裏面的同步機制synchronized關鍵字就是一個悲觀鎖,當一個變量或者是方法使用了synchronized修飾時,其他的線程想要拿到這個變量或者是方法的時候將就需要等到別的線程釋放。
數據庫裏面也用到了這種悲觀鎖的機制。比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。這樣其他的線程就不能同步操作,必須要等到他釋放纔可以。
2、樂觀鎖概念
樂觀鎖:總是假設最好的情況,每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,只在更新的時候會判斷一下在此期間別人有沒有去更新這個數據。
注意“在此期間”的含義是拿到數據到更新數據的這段時間。因爲沒有加鎖,所以別的線程可能會更改。還有一點那就是樂觀鎖其實是不加鎖的。
現在要實現的就是一個樂觀鎖機制,既然樂觀鎖是不加鎖的,而且還要保證數據的一致性。如何來實現呢?舉個例子:java中的Atomic包下的一系列類就是使用了樂觀鎖機制。我們挑出來一個看看官方是如何實現的,然後按照這樣的實現機制我們自己就可以實現。
3、樂觀鎖實現案例
java併發機制中主要有三個特性需要我們去考慮,原子性、可見性和有序性。AtomicInteger的作用就是爲了保證原子性。如何保證原子性呢?我們使用案例說明:
public class Test {
//一個變量a
private static volatile int a = 0;
public static void main(String[] args) {
Test test = new Test();
Thread[] threads = new Thread[5];
//定義5個線程,每個線程加10
for (int i = 0; i < 5; i++) {
threads[i] = new Thread(() -> {
try {
for (int j = 0; j < 10; j++) {
System.out.println(a++);
Thread.sleep(500);
}
} catch (Exception e) {
e.printStackTrace();
}
});
threads[i].start();
}
}
}
這個例子很簡單:
我們定義了一個變量a,初始值是0,然後使用5個線程去增加,每個線程增加10,按道理來說5個線程一共增加了50,但是運行一下就知道答案不到50,原因就在於裏面那個加一操作:a++;
對於a++的操作,其實可以分解爲3個步驟。
(1)從主存中讀取a的值
(2)對a進行加1操作
(3)把a重新刷新到主存
比如說有的線程已經把a進行了加1操作,但是還沒來得及重刷入到主存,其他的線程就重新讀取了舊值。這才造成了錯誤。解決辦法就可以使用AtomicInteger:
public class Test3 {
//使用AtomicInteger定義a
static AtomicInteger a = new AtomicInteger();
public static void main(String[] args) {
Test3 test = new Test3();
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++) {
threads[i] = new Thread(() -> {
try {
for (int j = 0; j < 10; j++) {
//使用getAndIncrement函數進行自增操作
System.out.println(a.incrementAndGet());
Thread.sleep(500);
}
} catch (Exception e) {
e.printStackTrace();
}
});
threads[i].start();
}
}
}
現在我們使用AtomicInteger定義a,然後使用incrementAndGet進行自增操作,最後的結果就總是50了。爲了什麼AtomicInteger有這樣的特點呢?我們來分析一下:
4、樂觀鎖案例分析
AtomicInteger是一個樂觀鎖,也就是說我們只要看一下AtomicInteger是如何實現這樣的機制和原理,我們就可以找出其他樂觀鎖實現的一般機制。想要找出來答案我們還要從AtomicInteger的incrementAndGet方法說起。因爲這個方法實現了鎖一樣的功能。這裏使用的是jdk1.8的版本,不同的版本會有出入。
/**
* Atomically increments by one the current value.
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
這裏我們可以看到自增操作主要是使用了unsafe的getAndAddInt方法。因爲不是專門介紹AtomicInteger,所以不會對源碼進行相信的分析。
(1)Unsafe:Unsafe是位於sun.misc包下的一個類,Unsafe類使Java語言擁有了類似C語言指針一樣操作內存空間的能力。也就是說我們直接操作了內存空間進行了加1操作。
(2) unsafe.getAndAddInt:其內部又調用了Unsafe.compareAndSwapInt方法。這個機制叫做CAS機制,
CAS 即比較並替換,實現併發算法時常用到的一種技術。CAS操作包含三個操作數——內存位置、預期原值及新值。執行CAS操作的時候,將內存位置的值與預期原值比較,如果相匹配,那麼處理器會自動將該位置值更新爲新值,否則,處理器不做任何操作。
我們使用一個例子來解釋相信你會更加的清楚。
比如說給你兒子訂婚。你兒子就是內存位置,你原本以爲你兒子是和楊貴妃在一起了,結果在訂婚的時候發現兒子身邊是西施。這時候該怎麼辦呢?你一氣之下不做任何操作。如果兒子身邊是你預想的楊貴妃,你一看很開心就給他們訂婚了,也叫作執行操作。現在你應該明白了吧。
但是這樣的CAS機制會帶來一個比較常見的問題。那就是ABA問題,舉個例子,你看到桌子上有100塊錢,然後你去幹其他事了,回來之後看到桌子上依然是100塊錢,你就認爲這100塊沒人動過,其實在你走的那段時間,別人已經拿走了100塊,後來又還回來了。這就是ABA問題。
那這時候又該如何解決ABA問題呢?既然有人動了,那我們對數據加一個版本控制字段,只要有人動過這個數據,就把版本進行增加,我們看到桌子上有100塊錢版本是1,回來後發現桌子上100沒變,但是版本確是2,就立馬明白100塊有人動過。
5、樂觀鎖思想
OK,上面說了這麼多,其實就是想說一句話那就是樂觀鎖可以由CAS機制+版本機制來實現。
樂觀鎖假設認爲數據一般情況下不會產生併發衝突,所以在數據進行提交更新的時候,纔會正式對數據是否產生併發衝突進行檢測,如果發現併發衝突了,則讓返回用戶錯誤的信息,讓用戶決定如何去做。
(1)CAS機制:當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗。CAS 有效地說明了“ 我認爲位置 V 應該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可“。
(2)版本機制:CAS機制保證了在更新數據的時候沒有被修改爲其他數據的同步機制,版本機制就保證了沒有被修改過的同步機制(意思是上面的ABA問題)。
基於這個思想我們就可以實現一個樂觀鎖。下面我們寫一下代碼。這個代碼在我自己電腦上親測通過。
二、實現一個樂觀鎖
第一步:定義我們要操作的數據
public class Data {
//數據版本號
static int version = 1;
//真實數據
static String data = "java的架構師技術棧";
public static int getVersion(){
return version;
}
public static void updateVersion(){
version = version + 1;
}
}
第二步:定義一個樂觀鎖
public class OptimThread extends Thread {
public int version;
public String data;
//構造方法和getter、setter方法
public void run() {
// 1.讀數據
String text = Data.data;
println("線程"+ getName() + ",獲得的數據版本號爲:" + Data.getVersion());
println("線程"+ getName() + ",預期的數據版本號爲:" + getVersion());
System.out.println("線程"+ getName()+"讀數據完成=========data = " + text);
// 2.寫數據:預期的版本號和數據版本號一致,那就更新
if(Data.getVersion() == getVersion()){
println("線程" + getName() + ",版本號爲:" + version + ",正在操作數據");
synchronized(OptimThread.class){
if(Data.getVersion() == this.version){
Data.data = this.data;
Data.updateVersion();
System.out.println("線程" + getName() + "寫數據完成=========data = " + this.data);
return ;
}
}
}else{
// 3. 版本號不正確的線程,需要重新讀取,重新執行
println("線程"+ getName() + ",獲得的數據版本號爲:" + Data.getVersion());
println("線程"+ getName() + ",預期的版本號爲:" + getVersion());
System.err.println("線程"+ getName() + ",需要重新執行。==============");
}
}
}
第三步:測試
public class Test {
public static void main(String[] args) {
for (int i = 1; i <= 2; i++) {
new OptimThread(String.valueOf(i), 1, "fdd").start();
}
}
}
定義了兩個線程,然後進行讀寫操作
第四步:輸出結果
這個結果可以看到在讀數據的時候只要發現沒有變化即可,但是更新數據的時候要判斷當前的版本號和預期的版本號是否一致,如果一致那就更新,如果不一致,那就說明更新失敗。