這個是在學習工作中的一些總結,若有不對之處歡迎大家指出。侵刪!
需要源碼聯繫QQ:1352057131
得之在俄頃,積之在平日。
Lock的簡介、地位和作用
鎖是一種工具,用於控制對共享資源的訪問
Lock和Synchronized這兩個是最常見的鎖,他們都可以達到線程安全的目的,但是 在使用上和功能上又有較大的不同。
Lock並不是用來代替Synchronized的,而是當使用Synchronized不合適或不足以滿足要求的時候來提供高級功能。
Lock接口最常見的實現類是ReentrantLock。
通常情況下,lock只允許一個線程來訪問這個共享資源,不過有的時候,一些特殊的實現也可以允許併發訪問,比如ReadWritelock裏面的readlock
Synchronized的缺陷
1、效率低:鎖的釋放情況少,試圖獲得鎖市不能設定超時,不能中斷一個正在試圖獲得鎖的線程。
2、不夠靈活(讀寫鎖更靈活):加鎖和釋放鎖的時機單一。
3、無法知道是否成功獲取鎖。
常用方法:
1、lock():這是最常見的獲取鎖的方式,如果鎖已經被其他線程獲取,則進行等待,lock不會像Synchronized那樣在異常時釋放鎖,所以我們就必須在finally裏面手動釋放鎖。
2、tryLock():用來嘗試獲取鎖,獲取成功就返回true,失敗就返回false,該方法會立即返回,不會等待拿鎖。
3、tryLock(long time,TimeUnit unit):與tryLock()區別在於設置獲取鎖的時間,超時就直接返回false
4、unlock():解鎖
鎖的可見性保證
Lock的加解鎖和Synchronized有同樣的內存語義即下一個加鎖前可以看到前一個解鎖後的所有語句
鎖的分類
悲觀鎖(互斥同步鎖)和樂觀鎖
悲觀鎖的實現
Synchronized和lock相關的類。
悲觀鎖的劣勢
1、阻塞和喚醒帶來的性能劣勢。
2、永久阻塞:如果持有鎖的線程被永久阻塞,比如遇到了無限循環、死鎖等問題。
樂觀鎖的實現
一般都是利用CAS算法來實現的,典型例子有:Git 原子類 併發容器 數據庫等。
ReentrantLock用法
public class LockTest {
static ExecutorService ExecutorService = Executors.newScheduledThreadPool(2);
//創建鎖
private static ReentrantLock lock = new ReentrantLock();
public static class Run implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"拿到鎖");
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i <10 ; i++) {
lock.lock();
try {
ExecutorService.submit(new Run());
Thread.sleep(1000);
}finally {
System.out.println("釋放鎖");
lock.unlock();
}
}
}
}
樂觀鎖與悲觀鎖的開銷對比
悲觀鎖的原始開銷高於樂觀鎖,但是特點是一勞永逸,臨界區持鎖時間就算越來越差也不會對互斥鎖的開銷造成影響;相反,雖然樂觀鎖一開始的開銷比悲觀鎖小,如果自旋時間很長或者不停的重試,那麼消耗資源越來越多。
悲觀鎖和樂觀鎖的使用場景
樂觀鎖:適合併發寫入少,大部分是讀取的場景。
悲觀鎖:適合併發寫入多的情況,適用於臨界區持鎖時間比較長的情況,悲觀鎖可以避免大量的無用自旋等消耗,典型情況:
1、臨界區有IO操作
2、臨界區代碼複雜或者循環量大
3、臨界區競爭非常激烈
可重入鎖與非可重入鎖
可重入
同一個線程可以多次獲得同一把鎖。
好處
避免死鎖,提高封裝性
Synchronized和reentrantlock是可重入鎖
源碼對比
可重入鎖示例(汽車上牌照)
public class LockTest {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
System.out.println("該鎖被拿到第"+lock.getHoldCount()+"次");
lock.lock();
System.out.println("該鎖被拿到第"+lock.getHoldCount()+"次");
lock.lock();
System.out.println("該鎖被拿到第"+lock.getHoldCount()+"次");
lock.lock();
System.out.println("該鎖被拿到第"+lock.getHoldCount()+"次");
lock.lock();
}
}
輸出結果:
該鎖被拿到第0次
該鎖被拿到第1次
該鎖被拿到第2次
該鎖被拿到第3次
公平鎖與非公平鎖
非公平鎖的好處
提高效率
避免喚醒帶來的空檔期
示例
public class LockTest {
public static void main(String[] args) {
LockQueue lockQueue =new LockQueue();
ExecutorService executorService = Executors.newFixedThreadPool(2);
for (int i = 0; i <2 ; i++) {
executorService.submit(new Run(lockQueue));
}
executorService.shutdown();
}
}
class Run implements Runnable{
private LockQueue lockQueue;
public Run(LockQueue lockQueue) {
this.lockQueue = lockQueue;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"開始打印");
lockQueue.printTest();
System.out.println(Thread.currentThread().getName()+"結束打印");
}
}
class LockQueue{
//創建鎖 傳入一個false則爲非公平鎖
private ReentrantLock lock = new ReentrantLock(false);
public void printTest(){
lock.lock();
try {
//設置打印時間
int time = new Random().nextInt(100);
System.out.println(Thread.currentThread().getName()+"正在打印需要"+time+"秒");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
lock.unlock();
}
lock.lock();
try {
//設置打印時間
int time = new Random().nextInt(100);
System.out.println(Thread.currentThread().getName()+"打印需要"+time+"秒");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
lock.unlock();
}
}
}
特例
tryLock()方法不遵守設定的公平規則,當有線程執行tryLock()方法,一旦有線程釋放了鎖,那麼這個正在執行tryLock()方法的線程就能立即獲取到鎖,即使在它之前已經有其他線程在隊列裏等待了。
公平鎖與非公平鎖優缺點
源碼對比
共享鎖和排它鎖
理解
排它鎖:又稱爲獨佔鎖、獨享鎖(寫鎖就是排它鎖)
共享鎖:又稱爲讀鎖,獲得共享鎖後只可以讀不可以修改或刪除
讀寫鎖的作用
多個讀操作是可以同時進行的,並不會發生線程安全問題;在讀的地方使用讀鎖,寫的地方使用寫鎖,如果沒有寫鎖的情況下,讀是無阻塞的,這樣就提高了程序的執行效率。
讀寫鎖的規則
要麼多個線程讀,寫操作阻塞;要麼一個線程寫,其餘線程阻塞。
示例
public class LockTest {
public static void main(String[] args) {
ReadAndWrite readAndWrite = new ReadAndWrite();
Thread thread0 = new Thread(new RunRead(readAndWrite));
Thread thread1 = new Thread(new RunRead(readAndWrite));
Thread thread2 = new Thread(new RunWrite(readAndWrite));
Thread thread3 = new Thread(new RunWrite(readAndWrite));
thread0.start();
thread1.start();
thread2.start();
thread3.start();
}
}
//讀
class RunRead implements Runnable{
private ReadAndWrite readAndWrite;
public RunRead(ReadAndWrite readAndWrite) {
this.readAndWrite = readAndWrite;
}
@Override
public void run() {
readAndWrite.read();
}
}
//寫
class RunWrite implements Runnable{
private ReadAndWrite readAndWrite;
public RunWrite(ReadAndWrite readAndWrite) {
this.readAndWrite = readAndWrite;
}
@Override
public void run() {
readAndWrite.write();
}
}
class ReadAndWrite{
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
//獲得讀鎖
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
//獲得寫鎖
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
//讀的方法
public void read(){
readLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"正在讀取");
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"讀取完畢");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName()+"釋放了讀鎖");
readLock.unlock();
}
}
//寫的方法
public void write(){
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"正在寫");
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"寫完畢");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName()+"釋放了讀鎖");
writeLock.unlock();
}
}
}
讀鎖的插隊策略
在非公平鎖中,讀鎖插隊的時候看頭結點是否爲寫鎖,若爲寫鎖則排隊,這樣可以有效的防止線程飢餓;在公平鎖中不存在插隊。
鎖的升降級
寫鎖可以降級,讀鎖不能升級,因爲讀鎖升級可能會造成死鎖。
注意
如果當前線程擁有寫鎖,然後將其釋放,最後再獲取到讀鎖,這種分段完成的過程不能稱之爲鎖降級。
鎖降級的應用場景
對於數據比較敏感, 需要在對數據修改以後, 獲取到修改後的值, 並進行接下來的其它操作
示例:
public class LockTest {
public static void main(String[] args) {
ReadAndWrite readAndWrite = new ReadAndWrite();
Thread thread0 = new Thread(new RunRead(readAndWrite));
Thread thread1 = new Thread(new RunRead(readAndWrite));
Thread thread2 = new Thread(new RunWrite(readAndWrite));
Thread thread3 = new Thread(new RunRead(readAndWrite));
thread0.start();
thread1.start();
thread2.start();
thread3.start();
}
}
//讀
class RunRead implements Runnable{
private ReadAndWrite readAndWrite;
public RunRead(ReadAndWrite readAndWrite) {
this.readAndWrite = readAndWrite;
}
@Override
public void run() {
readAndWrite.read();
}
}
//寫
class RunWrite implements Runnable{
private ReadAndWrite readAndWrite;
public RunWrite(ReadAndWrite readAndWrite) {
this.readAndWrite = readAndWrite;
}
@Override
public void run() {
readAndWrite.write();
}
}
class ReadAndWrite{
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);
//獲得讀鎖
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
//獲得寫鎖
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
//讀的方法
public void read(){
readLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"正在讀取");
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"讀取完畢");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName()+"釋放了讀鎖");
readLock.unlock();
}
}
//寫的方法
public void write(){
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"正在寫");
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"寫完畢");
System.out.println(Thread.currentThread().getName()+"準備降級爲寫鎖");
readLock.lock();
System.out.println(Thread.currentThread().getName()+"降級爲寫鎖成功");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName()+"釋放了讀寫鎖");
writeLock.unlock();
readLock.unlock();
}
}
}
自旋鎖與阻塞鎖
自旋鎖
是指當一個線程在獲取鎖的時候,如果鎖已經被其它線程獲取,那麼該線程將循環等待,然後不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖纔會退出循環(atomic裏面的都是基於自旋鎖)。
阻塞鎖
與自旋鎖相反,阻塞鎖如果遇到沒拿到鎖的情況,會直接把線程阻塞直到被喚醒。
自旋鎖示例
public class LockTest {
private AtomicReference<Thread> atomicReference = new AtomicReference<>();
//加鎖
public void lock(){
//拿到當前線程引用
Thread current = Thread.currentThread();
while (!atomicReference.compareAndSet(null,current)){
System.out.println(Thread.currentThread().getName()+"獲取失敗,正在嘗試");
}
}
//解鎖
public void unlock(){
Thread current = Thread.currentThread();
atomicReference.compareAndSet(current,null);
}
public static void main(String[] args) {
LockTest lockTest = new LockTest();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"開始嘗試獲取自旋鎖");
lockTest.lock();
System.out.println(Thread.currentThread().getName()+"獲得了自旋鎖");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println(Thread.currentThread().getName()+"解鎖成功");
lockTest.unlock();
}
}
};
Thread thread0 = new Thread(runnable);
Thread thread1 = new Thread(runnable);
thread0.start();
thread1.start();
}
}
當第一個線程thread0獲取鎖的時候,能夠成功獲取到,不會進入while循環,如果此時線程thread0沒有釋放鎖,另一個線程thread1又來獲取鎖,此時由於不滿足CAS,所以就會進入while循環,不斷判斷是否滿足CAS,直到thread0線程調用unlock方法釋放了該鎖。
自旋鎖的優缺點
優點:自旋鎖不會使線程狀態發生切換,一直處於用戶態,即線程一直都是active的;不會使線程進入阻塞狀態,減少了不必要的上下文切換,執行速度快
缺點:在自旋的過程中一直消耗cpu,如果鎖被佔用的時間很長,那麼自旋的線程只會白白浪費處理器資源。
自旋鎖適用場景
一般用於多核服務器,在併發不是很高的情況下,比阻塞效率高。
自旋鎖適用於臨界區比較短小的情況下。
鎖的優化
Java虛擬機對鎖的優化
自旋鎖和自適應
鎖消除
鎖優化
寫代碼時優化
縮小同步代碼塊
儘量不要鎖住方法,儘量使用代碼塊
減少鎖的次數
鎖中不要包含鎖,容易造成死鎖
選擇合適的鎖類型和工具類