前言
鎖的種類很多,我們今天就來梳理一下。Java中的好多鎖系列之悲觀鎖、樂觀鎖。
一:悲觀鎖
悲觀的認為所有的線程都會導致數據錯誤,每一個線程都需要排隊等待。優點:數據一致性,缺點:效率低
1.1:synchronized
-
jdk內置的鎖,熟悉native的朋友會知道,synchronized是調用了jr.jar里的放
-
靈魂的拷問,他是怎么實現的。答案對象頭object head,通過Object Header對象頭
Java對象的對象頭由 Mark Word 和 Klass pointer 兩部分組成,
mark word存儲了同步狀態、標識、hashcode、GC狀態等等。
Klass pointer存儲對象的類型指針,該指針指向它的類元數據
值得注意的是,如果應用的對象過多,使用64位的指針將浪費大量內存。64位的JVM比32位的JVM多耗費50%的內存。
我們現在使用的64位 JVM會默認使用選項 +UseCompressedOops 開啟指針壓縮,將指針壓縮至32位。
以64位操作系統為例,對象頭存儲內容圖例。
簡單介紹一下各部分的含義 lock: 鎖狀態標記位,該標記的值不同,整個mark word表示的含義不同。
biased_lock:偏向鎖標記,為1時表示對象啟用偏向鎖,為0時表示對象沒有偏向鎖。
age:Java GC標記位對象年齡,4位最大是15,所以minor Gc的次數就是15次。
identity_hashcode:對象標識Hash碼,采用延遲加載技術。當對象使用HashCode()計算后,並會將結果寫到該對象頭中。當對象被鎖定時,該值會移動到線程Monitor中。
thread:持有偏向鎖的線程ID和其他信息。這個線程ID並不是JVM分配的線程ID號,和Java Thread中的ID是兩個概念。
epoch:偏向時間戳。 ptr_to_lock_record:指向棧中鎖記錄的指針。 ptr_to_heavyweight_monitor:指向線程Monitor的指針。
輸出的第一行內容和鎖狀態內容對應 unused:1 | age:4 | biased_lock:1 | lock:2 0 0000 0 01 代表A對象正處於無鎖狀態
第三行中表示的是被指針壓縮為32位的klass pointer 第四行則是我們創建的A對象屬性信息 1字節的boolean值 第五行則代表了對象的對齊字段 為了湊齊64位的對象,對齊字段
-
對象鎖:synchronized
鎖在某一個實例對象上,如果該類是單例,那么該鎖也具有全局鎖的概念。
synchronized是對類的當前實例(當前對象)進行加鎖,防止其他線程同時訪問該類的該實例的所有synchronized塊,注意這里是“類的當前實例”, 類的兩個不同實例就沒有這種約束了。
import java.util.concurrent.TimeUnit;
public class Test1 {
public synchronized void start() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",start");
}
public synchronized void end() {
System.out.println(Thread.currentThread().getName() + ",end");
}
public static void main(String args[]) {
Test1 test = new Test1();
new Thread(test::start, "線程A").start();
new Thread(test::end, "線程B").start();
}
}
執行結果:
-
類鎖:static synchronized
該鎖針對的是類,無論實例多少個對象,那么線程都共享該鎖。
public synchronized void start() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",start");
}
public synchronized void end() {
System.out.println(Thread.currentThread().getName() + ",end");
}
public static void main(String args[]) {
StaticTest a = new StaticTest();
StaticTest b = new StaticTest();
new Thread(a::start, "線程A").start();
new Thread(b::end, "線程c").start();
}
執行結果:
線程A,end 線程c,start
更改start、end方法為static后,執行結果:
線程c,start 線程A,end
-
synchronized+static synchronized
public class StaticTest {
public synchronized void start() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",start");
}
public static synchronized void end() {
System.out.println(Thread.currentThread().getName() + ",end");
}
public static void main(String args[]) {
StaticTest a = new StaticTest();
StaticTest b = new StaticTest();
new Thread(() -> a.start(), "線程c").start();
new Thread(() -> a.end(), "線程A").start();
}
}
執行結果:
線程A,end 線程c,start
synchronized 與static synchronized 相當於兩幫派,各自管各自,相互之間就無約束了,可以被同時訪問。
1.2:Lock
未完待續
二:樂觀鎖
2.1:AtomicInteger
public class VersionTest {
final static int LOOP = 10000;
int count = 0;
public void add() {
count++;
System.out.println(Thread.currentThread().getName() + " add number :" + count);
}
public void subtract() {
count--;
System.out.println(Thread.currentThread().getName() + " subtract number :" + count);
}
public static void main(String args[]) throws InterruptedException {
VersionTest test = new VersionTest();
for (int i = 0; i < VersionTest.LOOP; i++) {
new Thread(() -> test.add()).start();
new Thread(() -> test.subtract()).start();
}
TimeUnit.SECONDS.sleep(1);
System.out.println(test.count);
}
}
執行結果:
Thread-19996 add number :1
Thread-19997 subtract number :0
Thread-19998 add number :1
Thread-19999 subtract number :0
Thread-19962 add number :1
Thread-19971 subtract number :0
Thread-19975 subtract number :-1
Thread-19881 subtract number :-2
-2
public class AtomicIntegerTest {
final static int LOOP = 10000;
AtomicInteger count = new AtomicInteger(0);
public void add() {
count.addAndGet(1);
System.out.println(Thread.currentThread().getName() + " add number :" + count.get());
}
public void subtract() {
count.decrementAndGet();
System.out.println(Thread.currentThread().getName() + " subtract number :" + count.get());
}
public static void main(String args[]) throws InterruptedException {
AtomicIntegerTest test = new AtomicIntegerTest();
for (int i = 0; i < AtomicIntegerTest.LOOP; i++) {
new Thread(() -> test.add()).start();
new Thread(() -> test.subtract()).start();
}
TimeUnit.SECONDS.sleep(1);
System.out.println(test.count.get());
}
}
執行結果:
Thread-19989 subtract number :0 Thread-19988 add number :1 Thread-19973 subtract number :0 Thread-19990 add number :1 Thread-19999 subtract number :0 Thread-19996 add number :1 Thread-19995 subtract number :0 0
2.2:CAS算法
compare and swap比較與交換,實現原理:
當前主內存變量的值V,線程本地變量預期值A(主內存變量V的副本),線程本地待更新值B。當需要更新變量值的時候,會先獲取到內存變量值V然后跟預期值A進行比較,如果相同則更新為B,如果不同,線程本地變量預期值A更新為主內存變量V的值。
2.2.1:ABA問題
當前主內存變量的值V,線程本地變量預期值A,線程本地待更新值B
A1線程、A2線程獲取主內存變量100,A1線程將主內存100更新為更新值80,A3線程獲取主內存變量80,將主內存80更改為更新值100,此時A2的預期值和主內存比較相等,認為主內存的值沒有改變過,更新自己的待更新值。
這兒說一下自己的看法,好多博客說的存錢問題如: 假設有個線程A去判斷賬戶里的錢此時是15,滿足條件,直接+20,這時候卡里余額是35.但是此時不巧,正好在連鎖店里,這個客人正在消費,又消費了20,此時卡里余額又為15,線程B去執行掃描賬戶的時候,發現它又小於20,又用過cas給它加了20。
如上的舉例論證一度讓我懷疑人生,我覺得最后的結果沒有問題的,即使加版本號多線程依然會再加20的
鏈表的例子才是正解:
現有一個用單向鏈表實現的堆棧,棧頂為A,這時線程T1已經知道A.next為B,然后希望用CAS將棧頂替換為B;
在T1執行上面這條指令之前,線程T2介入,將A、B出棧,再pushD、C、A;
所以場景很重要!
2.2.2:解決ABA問題
Java並發包提供了一個帶有標記的原子引用類AtomicStampedReference,它通過控制變量值的版本來保證CAS的正確性。每次在執行數據的修改操作時,都會帶上一個版本號,一旦版本號和數據的版本號一致就可以執行修改操作並對版本號執行+1操作,否則就執行失敗。