五、鎖的深入化
鎖是併發編程共享數據,保證數據一致性的工具。在Java中有多種實現,例如synchronized(重量級鎖)、ReentrantLock(輕量級鎖)等,這些鎖爲我們的來發提供了便利。下面我跟大家聊一聊Java中鎖的相關知識。
1.重入鎖
重入鎖的概念:重入鎖也叫遞歸鎖。就是說同一線程中,外層函數獲取了鎖,可以傳遞給內層函數去使用,可重入性可以避險死鎖現象。synchronized(重量級鎖)、ReentrantLock(輕量級鎖)都屬於重入鎖。下面寫一個可重入鎖的例子:
class Test implements Runnable{
@Override
public void run() {
set();
}
//synchronized要在代碼塊執行完畢後纔會釋放鎖
public synchronized void set(){
System.out.println("set方法");
get();
}
public synchronized void get(){
System.out.println("get方法");
}
}
//synchronized(重量級) 和Lock鎖(輕量級)——重入鎖(具有遞歸性)
public class test01 {
public static void main(String[] args) {
Test test = new Test();
Thread t1 = new Thread(test);
t1.start();
}
}
在這個例子中,set方法和get方法的鎖是同一個,在這裏我們假設synchronized鎖不具有可重入性,那麼get方法就必須要等待set方法釋放鎖後才能獲取鎖,這樣在set方法中調用get方法必然會造成死鎖現象(get方法一直在等待set方法執行完畢)。但是上面的代碼並沒有出錯,說明synchronized鎖具有可重入性,set方法中調用get方法,將set方法獲取的鎖傳遞給內層函數(get)。
2.讀寫鎖
假設程序中涉及到對一些共享資源的讀寫操作,並且在沒有做寫入操作是,允許兩個線程同時讀入資源。這時就需要用到讀寫鎖。讀寫鎖允許多個線程同時讀取資源,但是不允許多個線程同時進行寫入操作或者同時讀寫操作,也就是說:讀-讀能共存,讀-寫不能共存,寫-寫不能共存。下面寫一個讀寫鎖的例子:
package com.zhu.test;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
public class test03 {
private volatile Map<String,String> caChe = new HashMap<>();
//新建一個讀寫鎖
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
//寫鎖
private WriteLock writeLock = rwl.writeLock();
//讀鎖
private ReadLock readLock = rwl.readLock();
//寫入
public void put(String key,String value){
try {
writeLock.lock();
System.out.println("寫入put方法key:" + key + ",value:" + value + "開始");
//Thread.sleep(50);
caChe.put(key, value);
System.out.println("寫入put方法key:" + key + ",value:" + value + "結束");
} catch (Exception e) {
// TODO: handle exception
}finally {
writeLock.unlock();
}
}
//讀取
public String get(String key){
try {
readLock.lock();
String value = caChe.get(key);
System.out.println("讀取get方法key:" + key + ",value:" + value + "開始");
Thread.sleep(50);
caChe.put(key, value);
System.out.println("讀取get方法key:" + key + ",value:" + value + "結束");
return value;
} catch (Exception e) {
// TODO: handle exception
}finally {
readLock.unlock();
}
return null;
}
public static void main(String[] args) {
test03 t = new test03();
//寫入線程
Thread write = new Thread(new Runnable() {
public void run() {
for(int i = 0;i < 10;i++){
t.put("i", i+"");
}
}
});
//讀取線程
Thread read = new Thread(new Runnable() {
public void run() {
for(int i = 0;i < 10;i++){
t.get("i");
}
}
});
write.start();
read.start();
}
}
實驗結果:
通過實驗結果,我們可以發現,在寫入資源時,我們調用寫鎖的lock()方法,寫入結束調用unlock()方法;在讀取資源時,我們調用讀鎖的lock()方法,寫入結束調用unlock()方法,並且在寫入操作沒有完成之前,是不能進行讀取操作的。
3.樂觀鎖/悲觀鎖
樂觀鎖與悲觀鎖不是指具體的什麼類型的鎖,而是指看待併發同步的角度。悲觀鎖認爲對於同一個數據的併發操作,一定是會發生修改的,哪怕沒有修改,也會認爲修改。因此對於同一個數據的併發操作,悲觀鎖採取加鎖的形式。悲觀的認爲,不加鎖的併發操作一定會出問題。在Java中,synchronized的思想也是悲觀鎖。
樂觀鎖則認爲對於同一個數據的併發操作,是不會發生修改的。在更新數據的時候,會採用嘗試更新,不斷重新的方式更新數據。樂觀的認爲,不加鎖的併發操作是沒有事情的。從上面的描述我們可以看出,悲觀鎖適合寫操作非常多的場景,樂觀鎖適合讀操作非常多的場景,不加鎖會帶來大量的性能提升。悲觀鎖在Java中的使用,就是利用各種鎖。樂觀鎖在Java中的使用,是無鎖編程,常常採用的是CAS算法,典型的例子就是原子類,通過CAS自旋實現原子操作的更新。
4.cas無鎖機制
CAS:Compare and Swap,即比較再交換。
jdk5增加了併發包java.util.concurrent.*,其下面的類使用CAS算法實現了區別於synchronouse同步鎖的一種樂觀鎖。JDK 5之前Java語言是靠synchronized關鍵字保證同步的,這是一種獨佔鎖,也是是悲觀鎖。
5.CAS算法理解
(1)與鎖相比,使用比較交換(下文簡稱CAS)會使程序看起來更加複雜一些。但由於其非阻塞性,它對死鎖問題天生免疫,並且,線程間的相互影響也遠遠比基於鎖的方式要小。更爲重要的是,使用無鎖的方式完全沒有鎖競爭帶來的系統開銷,也沒有線程間頻繁調度帶來的開銷,因此,它要比基於鎖的方式擁有更優越的性能。
(2)無鎖的好處:
第一,在高併發的情況下,它比有鎖的程序擁有更好的性能;
第二,它天生就是死鎖免疫的。
就憑藉這兩個優勢,就值得我們冒險嘗試使用無鎖的併發。
(3)CAS算法的過程是這樣:它包含三個參數CAS(V,E,N): V表示要更新的變量,E表示預期值,N表示新值。僅當V值等於E值時,纔會將V的值設爲N,如果V值和E值不同,則說明已經有其他線程做了更新,則將主內存的值刷新到本地內存,再去做比較,一直重試。最後,CAS返回當前V的真實值。
V=需要更新變量,主內存
E=預望值,本地內存
N=新值
如果V=E(主內存值與本地內存值一致),說明:沒有被修改過,將V的值設置爲N。
如果V!=E(主內存值與本地內存值不一致),已經被修改。
(4)CAS操作是抱着樂觀的態度進行的,它總是認爲自己可以成功完成操作。當多個線程同時使用CAS操作一個變量時,只有一個會勝出,併成功更新,其餘均會失敗。失敗的線程不會被掛起,僅是被告知失敗,並且允許再次嘗試,當然也允許失敗的線程放棄操作。基於這樣的原理,CAS操作即使沒有鎖,也可以發現其他線程對當前線程的干擾,並進行恰當的處理。
(5)簡單地說,CAS需要你額外給出一個期望值,也就是你認爲這個變量現在應該是什麼樣子的。如果變量不是你想象的那樣,那說明它已經被別人修改過了。你就重新讀取,再次嘗試修改就好了。
(6)在硬件層面,大部分的現代處理器都已經支持原子化的CAS指令。在JDK 5.0以後,虛擬機便可以使用這個指令來實現併發操作和併發數據結構,並且,這種操作在虛擬機中可以說是無處不在。