同步鎖java
- 鎖是用來控制多個線程訪問共享資源的方式
- 通常來講,一個鎖可以防止多個線程同時訪問共享資源,
- 在Lock接口出現以前,Java應用程序只能依靠synchronized關鍵字來實現同步鎖的功能,
- 在java5之後,增長了JUC(java.util.concurrent)的併發包且提供了Lock接口用來實現鎖的功能,
- 它提供了與synchroinzed關鍵字相似的同步功能,
- 只是它比synchronized更靈活,可以顯式的獲取和釋放鎖。
Lock的初步使用面試
- Lock是一個接口,核心的兩個方法lock和unlock,
- 它有不少的實現,好比ReentrantLock、ReentrantReadWriteLock;
ReentrantLock緩存
- 重入鎖,表示支持從新進入的鎖,
- 也就是說,若是當前線程t1經過調用lock方法獲取了鎖以後,
- 再次調用lock,是不會再阻塞去獲取鎖的,
- 直接增長重試次數就好了。
public class AtomicDemo {
private static int count = 0;
static Lock lock = new ReentrantLock();
public static void inc() {
lock.lock();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
lock.unlock();
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
AtomicDemo.inc();
}).start();
}
Thread.sleep(3000);
System.out.println("result:" + count);
}
}
ReentrantReadWriteLock安全
- 咱們之前理解的鎖,基本都是排他鎖,
- 也就是這些鎖在同一時刻只容許一個線程進行訪問,
- 而讀寫鎖在同一時刻能夠容許多個線程訪問,
- 可是在寫線程訪問時,全部的讀線程和其餘寫線程都會被阻塞。
- 讀寫鎖維護了一對鎖,一個讀鎖、一個寫鎖;
- 通常狀況下,讀寫鎖的性能都會比排它鎖好,
- 在讀多於寫的狀況下,讀寫鎖可以提供比排它鎖更好的併發性和吞吐量.
public class LockDemo {
static Map<String, Object> cacheMap = new HashMap<>();
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
static Lock read = rwl.readLock();
static Lock write = rwl.writeLock();
public static final Object get(String key) {
System.out.println("開始讀取數據");
read.lock(); //讀鎖
try {
return cacheMap.get(key);
} finally {
read.unlock();
}
}
public static final Object put(String key, Object value) {
write.lock();
System.out.println("開始寫數據");
try {
return cacheMap.put(key, value);
} finally {
write.unlock();
}
}
}
- 在這個案例中,經過hashmap來模擬了一個內存緩存,
- 當執行讀操做的時候,須要獲取讀鎖,在併發訪問的時候,讀鎖不會被阻塞,
- 在執行寫操做是,線程必需要獲取寫鎖,當已經有線程持有寫鎖的狀況下,
- 當前線程會被阻塞,只有當寫鎖釋放之後,其餘讀寫操做才能繼續執行。
- 使用讀寫鎖提高讀操做的併發性,也保證每次寫操做對全部的讀寫操做的可見性
- l 讀鎖與讀鎖能夠共享
- l 讀鎖與寫鎖不能夠共享(排他)
- l 寫鎖與寫鎖不能夠共享(排他)
Lock和synchronized的簡單對比數據結構
- 經過咱們對Lock的使用以及對synchronized的瞭解,基本上能夠對比出這兩種鎖的區別了。
- 由於這個也是在面試過程當中比較常見的問題::
- Ø 從層次上,一個是關鍵字、一個是類, 這是最直觀的差別
- Ø 從使用上,lock具有更大的靈活性,能夠控制鎖的釋放和獲取;
- 而synchronized的鎖的釋放是被動的,當出現異常或者同步代碼塊執行完之後,纔會釋放鎖
- Ø lock能夠判斷鎖的狀態、而synchronized沒法作到
- Ø lock能夠實現公平鎖、非公平鎖;
AQS(AbstractQueuedSynchronizer)多線程
- Lock之因此能實現線程安全的鎖,
- 主要的核心是AQS(AbstractQueuedSynchronizer),
- AbstractQueuedSynchronizer提供了一個FIFO隊列,
- 能夠看作是一個用來實現鎖以及其餘須要同步功能的框架。
- AQS的使用依靠繼承來完成,
- 子類經過繼承自AQS並實現所需的方法來管理同步狀態。
- 例如常見的ReentrantLock,CountDownLatch等AQS的兩種功能
- 從使用上來講,AQS的功能能夠分爲兩種:獨佔和共享。
- 獨佔鎖模式下,每次只能有一個線程持有鎖,
- 好比前面給你們演示的ReentrantLock就是以獨佔方式實現的互斥鎖
- 共享鎖模式下,容許多個線程同時獲取鎖,併發訪問共享資源,
- 好比ReentrantReadWriteLock。
- 很顯然,獨佔鎖是一種悲觀保守的加鎖策略,它限制了讀/讀衝突,
- 若是某個只讀線程獲取鎖,則其餘讀線程都只能等待,
- 這種狀況下就限制了沒必要要的併發性,
- 由於讀操做並不會影響數據的一致性。
- 共享鎖則是一種樂觀鎖,它放寬了加鎖策略,
AQS的內部實現併發
- 同步器(AQS)依賴內部的同步隊列(一個FIFO雙向隊列)來完成同步狀態的管理,
- 當前線程獲取同步狀態失敗時,
- 同步器會將當前線程以及 等待狀態 等信息
- 構形成爲一個節點(Node)並將其加入同步隊列,
- 同時會阻塞當前線程,
- 當同步狀態釋放時,會把首節點中的線程喚醒,
Node的主要屬性以下app
static final class Node {
int waitStatus; //表示節點的狀態,包含cancelled(取消);condition 表示節點在等待condition 也就是在condition隊列中
Node prev; //前繼節點
Node next; //後繼節點
Node nextWaiter; //存儲在condition隊列中的後繼節點
Thread thread; //當前線程
}
- AQS類底層的數據結構是使用雙向鏈表,是隊列的一種實現。
- 包括一個head節點和一個tail節點,
- 分別表示頭結點和尾節點,
- 其中頭結點不存儲Thread,僅保存next結點的引用。
- 當一個線程成功地獲取了同步狀態(或者鎖),
- 其餘線程將沒法獲取到同步狀態,
- 轉而被構形成爲節點並加入到同步隊列中,
- 而這個加入隊列的過程必需要保證線程安全,
- 所以同步器提供了一個基於CAS的設置尾節點的方法:compareAndSetTail(Node expect,Nodeupdate),
- 它須要傳遞參數當前線程「認爲」的尾節點和當前節點,只有設置成功後,
- 當前節點才正式與以前的尾節點創建關聯。
- 同步隊列遵循FIFO,
- 首節點的線程在釋放同步狀態時,
- 將會喚醒後繼節點,
- 然後繼節點將會在獲取同步狀態成功時
- 設置首節點是經過獲取同步狀態成功的線程來完成的,
- 因爲只有一個線程可以成功獲取到同步狀態,
- 所以設置頭節點的方法並不須要使用CAS來保證,
- 它只須要將首節點設置成爲原首節點的後繼節點
- 並斷開原首節點的next引用便可
compareAndSet框架
- AQS中,除了自己的鏈表結構之外,
- 還有一個很關鍵的功能,就是CAS,
- 這個是保證在多線程併發的狀況下、保證線程安全的前提下
- 去把線程加入到AQS中的方法,能夠簡單理解爲樂觀鎖
private final boolean compareAndSetHead(Node update) {
return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
- 首先,用到了unsafe類,
- Unsafe類是在sun.misc包下,不屬於Java標準。
- 可是不少Java的基礎類庫,包括一些被普遍使用的高性能開發庫都是基於Unsafe類開發的,
- 好比Netty、Hadoop、Kafka等;
- Unsafe可認爲是Java中留下的後門,提供了一些低層次操做,
- 而後調用了compareAndSwapObject這個方法
這裏傳入了一個headOffset,這個headOffset是什麼呢?oop
- 在下面的代碼中,經過unsafe.objectFieldOffset
unsafe.objectFieldOffset
- headOffset這個是指類中相應字段在該類的偏移量,
- 在這裏具體便是指head這個字段
- 在AQS類的內存中相對於該類首地址的偏移量。
- 一個Java對象能夠當作是一段內存,
- 每一個字段都得按照必定的順序放在這段內存裏,
- 經過這個方法能夠準確地告訴你
- 用於在後面的compareAndSwapObject中,
這個方法在unsafe.cpp文件中,代碼以下::
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapObject(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jobject e_h, jobject x_h))
UnsafeWrapper("Unsafe_CompareAndSwapObject");
oop x = JNIHandles::resolve(x_h); // 新值
oop e = JNIHandles::resolve(e_h); // 預期值
oop p = JNIHandles::resolve(obj);
HeapWord* addr = (HeapWord *)index_oop_from_field_offset_long(p, offset);// 在內存中的具體位置
oop res = oopDesc::atomic_compare_exchange_oop(x, addr, e, true);// 調用了另外一個方法,實際上就是經過cas操做來替換內存中的值是否成功
jboolean success = (res == e); // 若是返回的res等於e,則斷定知足compare條件(說明res應該爲內存中的當前值),但實際上會有ABA的問題
if (success) // success爲true時,說明此時已經交換成功(調用的是最底層的cmpxchg指令)
update_barrier_set((void*)addr, x); // 每次Reference類型數據寫操做時,都會產生一個Write Barrier暫時中斷操做,配合垃圾收集器
return success;
UNSAFE_END
- 因此其實compareAndSet這個方法,
- 最終調用的是unsafe類的compareAndSwap,
- 這個指令會對內存中的共享數據作原子的讀寫操做。
- 1. 首先, cpu會把內存中將要被更改的數據與指望值作比較
- 2. 而後,當兩個值相等時,cpu纔會將內存中的對象替換爲新的值。
- 3. 最後,返回操做執行結果
- 很顯然,這是一種樂觀鎖的實現思路。