Java 開(kāi)發(fā)做了 N 年,精通了 Java 的這幾種鎖!

小編導(dǎo)讀
最近在開(kāi)發(fā)項(xiàng)目中遇到了資源同步的問(wèn)題,導(dǎo)致服務(wù)器卡頓,優(yōu)化的時(shí)候使用了鎖的機(jī)制,就突然想寫(xiě)一個(gè)鎖的文章,本篇文章簡(jiǎn)短,但是精髓具在。
有問(wèn)題和建議的小伙伴可以在最后的留言中說(shuō)出你的問(wèn)題和建議。
好了,現(xiàn)在直入正題,還是技術(shù)文章老規(guī)矩,先從概念說(shuō)起,了解概念再去實(shí)找,想想都興奮。
01
概念
說(shuō)到Java中的鎖要聯(lián)系到多線程。
多線程確實(shí)給我們?cè)谛噬辖o我們帶來(lái)了很大的便利,便利的同時(shí)不得不考慮多個(gè)線程之間對(duì)資源競(jìng)爭(zhēng)引起的安全問(wèn)題。
同步關(guān)鍵字synchronized是我們比較熟悉的用來(lái)解決線程安全的一個(gè)關(guān)鍵字,但是鎖(Lock)是一個(gè)在資源競(jìng)爭(zhēng)激勵(lì)的情況下性能更優(yōu)于synchronized的方法。
再看一些多并發(fā)文章中,會(huì)提及各種各樣鎖如公平鎖,樂(lè)觀鎖,讀寫(xiě)鎖等等,這篇文章會(huì)說(shuō)一些常用的鎖,把主要的鎖講透,會(huì)用。
02
分類
按照鎖的特性和設(shè)計(jì)來(lái)劃分,分為如下幾類:
1、公平鎖/非公平鎖
2、可重入鎖
3、獨(dú)享鎖/共享鎖
4、互斥鎖/讀寫(xiě)鎖
5、樂(lè)觀鎖/悲觀鎖
6、分段鎖
7、偏向鎖/輕量級(jí)鎖/重量級(jí)鎖
8、自旋鎖(java.util.concurrent包下的幾乎都是利用鎖)
從底層角度看常見(jiàn)的鎖也就兩種:Synchronized和Lock接口以及ReadWriteLock接口(讀寫(xiě)鎖)
03
介紹
我們來(lái)說(shuō)一下每一個(gè)鎖的概念。
1、公平鎖/非公平鎖
公平鎖指多個(gè)線程按照申請(qǐng)鎖的順序來(lái)依次獲取鎖。
非公平鎖指多個(gè)線程獲取鎖的順序并不是按照申請(qǐng)鎖的順序來(lái)獲取,有可能后申請(qǐng)鎖的線程比先申請(qǐng)鎖的線程優(yōu)先獲取到鎖,此極大的可能會(huì)造成線程饑餓現(xiàn)象,遲遲獲取不到鎖。
由于ReentrantLock是通過(guò)AQS來(lái)實(shí)現(xiàn)線程調(diào)度,可以實(shí)現(xiàn)公平鎖,但是synchroized是非公平的,無(wú)法實(shí)現(xiàn)公平鎖。
2、可重入鎖
可重入鎖又名遞歸鎖,是指在同一個(gè)線程,在外層方法獲取鎖的時(shí)候,在進(jìn)入內(nèi)層方法會(huì)自動(dòng)獲取鎖。
3、獨(dú)享鎖/共享鎖
獨(dú)享鎖是指該鎖一次只能被一個(gè)線程所持有。
共享鎖是指該鎖可被多個(gè)線程所持有。
4、互斥鎖/讀寫(xiě)鎖
上面講的獨(dú)享鎖/共享鎖就是一種廣義的說(shuō)法,互斥鎖/讀寫(xiě)鎖就是具體的實(shí)現(xiàn)。
互斥鎖在Java中的具體實(shí)現(xiàn)就是ReentrantLock
讀寫(xiě)鎖在Java中的具體實(shí)現(xiàn)就是ReadWriteLock
5、樂(lè)觀鎖/悲觀鎖
悲觀鎖認(rèn)為對(duì)于同一個(gè)數(shù)據(jù)的并發(fā)操作,一定是會(huì)發(fā)生修改的,哪怕沒(méi)有修改,也會(huì)認(rèn)為修改。因此對(duì)于同一個(gè)數(shù)據(jù)的并發(fā)操作,悲觀鎖采取加鎖的形式。悲觀的認(rèn)為,不加鎖的并發(fā)操作一定會(huì)出問(wèn)題。
樂(lè)觀鎖則認(rèn)為對(duì)于同一個(gè)數(shù)據(jù)的并發(fā)操作,是不會(huì)發(fā)生修改的。在更新數(shù)據(jù)的時(shí)候,會(huì)采用嘗試更新。樂(lè)觀的認(rèn)為,不加鎖的并發(fā)操作是沒(méi)有事情的。
6、分段鎖
分段鎖其實(shí)是一種鎖的設(shè)計(jì),并不是具體的一種鎖,對(duì)于ConcurrentHashMap而言,其并發(fā)的實(shí)現(xiàn)就是通過(guò)分段鎖的形式來(lái)實(shí)現(xiàn)高效的并發(fā)操作。
7、偏向鎖
偏向鎖是指一段同步代碼一直被一個(gè)線程所訪問(wèn),那么該線程會(huì)自動(dòng)獲取鎖。降低獲取鎖的代價(jià)。
8、自旋鎖(java.util.concurrent包下的幾乎都是利用鎖)
自旋鎖是指嘗試獲取鎖的線程不會(huì)立即阻塞,而是采用循環(huán)的方式去嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗,缺點(diǎn)是循環(huán)會(huì)消耗CPU。
04
實(shí)戰(zhàn)
其實(shí)上面說(shuō)的很多概念,讓我沒(méi)有使用前我也是一臉懵的,畢竟使用很少的情況下,只是慣性的去使用,沒(méi)有多想其他,更別說(shuō)有的幾乎沒(méi)有使用過(guò),第一次聽(tīng)說(shuō)的都有。
遇到這種使用很少或者沒(méi)有使用過(guò)的,最好通過(guò)實(shí)例來(lái)認(rèn)識(shí)。
我這里不做更多的介紹,只介紹常用的幾種。如果有需要更深入的了解的,加我好友我們暢聊。
再說(shuō)其他幾種鎖之前我說(shuō)先說(shuō) Lock接口與synchronized關(guān)鍵字。
1,synchronized關(guān)鍵字
很多人都指導(dǎo) synchronized關(guān)鍵字 是一種同步鎖。
它的原理是:一個(gè)線程訪問(wèn)一個(gè)對(duì)象中的synchronized(this)同步代碼塊時(shí),其他試圖訪問(wèn)該對(duì)象的線程將被阻塞。
import java.util.ArrayList;import java.util.List;public class Test {public static void main(String[] args) {System.out.println("使用關(guān)鍵字synchronized");SyncThread syncThread = new SyncThread();Thread thread1 = new Thread(syncThread, "SyncThread1");Thread thread2 = new Thread(syncThread, "SyncThread2");thread1.start();thread2.start();}}class SyncThread implements Runnable {private static int count;public SyncThread() {count = 0;}public void run() {synchronized (this){for (int i = 0; i < 5; i++) {try {System.out.println("線程name:"+Thread.currentThread().getName() + ":" + (count++));Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}}public int getCount() {return count;}}
分析:1,當(dāng)兩個(gè)并發(fā)線程(thread1和thread2)訪問(wèn)同一個(gè)對(duì)象(syncThread)中的synchronized代碼塊時(shí),在同一時(shí)刻只能有一個(gè)線程得到執(zhí)行,另一個(gè)線程受阻塞,必須等待當(dāng)前線程執(zhí)行完這個(gè)代碼塊以后才能執(zhí)行該代碼塊。
2,Thread1和thread2是互斥的,因?yàn)樵趫?zhí)行synchronized代碼塊時(shí)會(huì)鎖定當(dāng)前的對(duì)象,只有執(zhí)行完該代碼塊才能釋放該對(duì)象鎖,下一個(gè)線程才能執(zhí)行并鎖定該對(duì)象。
修飾一個(gè)方法
Synchronized修飾一個(gè)方法很簡(jiǎn)單,就是在方法的前面加synchronized。
將上面的代碼修改如下:
public synchronized void run() {{for (int i = 0; i < 5; i++) {try {System.out.println("線程name:"+Thread.currentThread().getName() + ":" + (count++));Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}}
分析:
1,輸入結(jié)果你會(huì)發(fā)現(xiàn)兩個(gè)線程是交替的
2,雖然可以使用synchronized來(lái)定義方法,但synchronized并不屬于方法定義的一部分,因此,synchronized關(guān)鍵字不能被繼承。
如果在父類中的某個(gè)方法使用了synchronized關(guān)鍵字,而在子類中覆蓋了這個(gè)方法,在子類中的這個(gè)方法默認(rèn)情況下并不是同步的,而必須顯式地在子類的這個(gè)方法中加上synchronized關(guān)鍵字才可以
2,Lock接口
JDK1.5之后并發(fā)包中新增了Lock接口以及相關(guān)實(shí)現(xiàn)類來(lái)實(shí)現(xiàn)鎖功能。
Lock lock=new ReentrantLock();lock.lock();try{}finally{lock.unlock();}
分析:上面的代碼時(shí)Lock接口的簡(jiǎn)單使用比較簡(jiǎn)單。
我們還以一個(gè)售票的機(jī)制來(lái)看 Lock接口鎖時(shí)怎么實(shí)現(xiàn)的?
import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/*** 一、用于解決多線程安全問(wèn)題的方式:* 1.同步代碼塊 synchronized 隱式鎖* 2.同步方法 synchronized 隱式鎖* 3.同步鎖Lock (jdk1.5以后) 顯示鎖* 注意:顯示鎖,需要通過(guò)lock()方式上鎖,必須通過(guò)unlock()方式進(jìn)行釋放鎖*/public class TestLock {public static void main(String[] args) {TicketCar ticketCar = new TicketCar();new Thread(ticketCar, "1號(hào)窗口").start();new Thread(ticketCar, "2號(hào)窗口").start();new Thread(ticketCar, "3號(hào)窗口").start();}}class TicketCar implements Runnable {private int tick = 100;private Lock lock = new ReentrantLock();@Overridepublic void run() {while (true) {lock.lock();try {if (tick > 0) {try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 完成售票,余票為 " + --tick);}} finally {lock.unlock();}}}}
好了 上面說(shuō)了 我們工作中常用的兩種方式,下面我再簡(jiǎn)單聊幾種比較常用的。
3,讀寫(xiě)鎖
關(guān)于使用讀寫(xiě)鎖就是在讀的時(shí)候上讀鎖,寫(xiě)的時(shí)候上寫(xiě)鎖,主要用來(lái)解決synchronized不能解決的問(wèn)題(兩個(gè)線程同時(shí)讀,理論上是可以并行的,但是synchronized是加了鎖的)。
ReadWriteLock接口中有兩個(gè)方法,分別是readLock和writeLock。源碼如下:
public interface ReadWriteLock {/*** 返回讀鎖*/Lock readLock();/*** 返回寫(xiě)鎖*/Lock writeLock();}
關(guān)于讀寫(xiě)鎖我們做一個(gè)操作,在寫(xiě)操作的時(shí)候不允許讀操作,讀寫(xiě)分開(kāi)。
import java.util.HashMap;import java.util.Map;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantReadWriteLock;public class threadTest { static Map<String, Object> map = new HashMap<String, Object>(); static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); static Lock r = rwl.readLock(); static Lock w = rwl.writeLock(); // 獲取一個(gè)key對(duì)應(yīng)的value public static final Object get(String key) { Object object = null; try { r.lock(); System.out.println("正在做讀的操作,key:" + key + " 開(kāi)始"); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } object = map.get(key); System.out.println("正在做讀的操作,key:" + key + " 結(jié)束"); System.out.println(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); }finally{ r.unlock(); } return object; } // 設(shè)置key對(duì)應(yīng)的value,并返回舊有的value public static final Object put(String key, Object value) { Object object = null; try { w.lock(); System.out.println("正在做寫(xiě)的操作,key:" + key + ",value:" + value + "開(kāi)始."); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } object = map.put(key, value); System.out.println("正在做寫(xiě)的操作,key:" + key + ",value:" + value + "結(jié)束."); System.out.println(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); }finally{ w.unlock(); } return object; } public static void main(String[] args) { new Thread(new Runnable() { public void run() { for (int i = 0; i < 10; i++) { threadTest.put(i + "", i + ""); } } }).start(); new Thread(new Runnable() { public void run() { for (int i = 0; i < 10; i++) { threadTest.get(i + ""); } } }).start(); }}
1,調(diào)用讀寫(xiě)方法的時(shí)候 添加了 鎖 lock ,只有在finally執(zhí)行玩調(diào)用unlock,解鎖后才能允許其他鎖的執(zhí)行。
4,可重入鎖
對(duì)于Java ReentrantLock而言, 他的名字就可以看出是一個(gè)可重入鎖,其名字是Re entrant Lock重新進(jìn)入鎖。
對(duì)于Synchronized而言,也是一個(gè)可重入鎖??芍厝腈i的一個(gè)好處是可一定程度避免死鎖。
5,公平鎖/非公平鎖
/*** 公平鎖與非公平鎖*/public class FairAndUnFairThread {public static void main(String[] args) throws InterruptedException {//默認(rèn)非公平鎖final Lock lock = new ReentrantLock(true);final MM m = new MM(lock);for (int i=1;i<=10 ;i++){String name = "線程"+i;Thread tt = new Thread(new Runnable() {@Overridepublic void run() {for(int i=0;i<2;i++){m.testReentrant();}}},name);tt.start();}}}class MM {Lock lock = null;MM(Lock lock){this.lock = lock;}public void testReentrant(){lock.lock();try{Thread.sleep(1);System.out.println(Thread.currentThread().getName() );} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}public synchronized void testSync(){System.out.println(Thread.currentThread().getName());}}
1,這種公平也不是絕對(duì)的 不一定就是按照順序,可能因?yàn)镃PU準(zhǔn)備原因,可能會(huì)有一些不公平的。
講了這么多鎖,趕緊在開(kāi)發(fā)工具中試試把,多驗(yàn)證幾次什么都明白了。
本文 Github ( 碼云Gitee同步) https://github.com/ProceduralZC/JavaDevGuide/tree/master/code/JavaBasic 已收錄,歡迎 star。
— END —
