Java中的鎖
從不同的角度看碘裕,Java中有許多類型的鎖,下面是它們的簡(jiǎn)單介紹胶背。
-
從是否鎖住同步資源來看
-
1.1樂觀鎖
樂觀鎖認(rèn)為所有拿到共享數(shù)據(jù)的線程都不會(huì)修改數(shù)據(jù)杭措,只會(huì)查看數(shù)據(jù),因此在獲取共享數(shù)據(jù)時(shí)不會(huì)加鎖伟墙,適合讀操作多的場(chǎng)景翘鸭,不加鎖的特點(diǎn)能夠使其讀操作的性能大幅提升。
Java中是通過使用無鎖編程來實(shí)現(xiàn)戳葵,最常采用的是CAS算法就乓,Java原子類中的遞增操作就通過CAS自旋實(shí)現(xiàn)的。
-
1.1.1 CAS
CAS全稱 Compare And Swap拱烁。在不使用鎖的情況下實(shí)現(xiàn)多線程之間的變量同步生蚁。
涉及到三個(gè)操作數(shù):需要讀寫的內(nèi)存值 oldValue,進(jìn)行比較的值 oldValue2戏自,要寫入的新值 newValue邦投。
JDK通過CPU的cmpxchg指令,去比較寄存器中的 oldValue2 和 內(nèi)存中的值 oldValue擅笔。如果相等尼摹,就把要寫入的新值 newValue 存入內(nèi)存中。如果不相等剂娄,就將內(nèi)存值 oldValue 賦值給寄存器中的值 oldValue2蠢涝。然后通過Java代碼中的while循環(huán)再次調(diào)用cmpxchg指令進(jìn)行重試,直到設(shè)置成功為止阅懦。
-
1.2 悲觀鎖
悲觀鎖認(rèn)為自己在使用數(shù)據(jù)的時(shí)候一定有別的線程來修改數(shù)據(jù)和二,因此在獲取數(shù)據(jù)的時(shí)候會(huì)先加鎖,確保數(shù)據(jù)不會(huì)被別的線程修改耳胎,適合寫操作多的場(chǎng)景惯吕,先加鎖可以保證寫操作時(shí)數(shù)據(jù)正確惕它。
-
從同步資源的競(jìng)爭(zhēng)程度來看
2.1 偏向鎖
當(dāng)一段同步代碼長(zhǎng)時(shí)間只被同一個(gè)線程訪問時(shí),說明該塊同步代碼競(jìng)爭(zhēng)程度非常低废登,沒必要頻繁的加鎖解鎖消耗系統(tǒng)資源淹魄,所以可以讓線程來訪問同步代碼自動(dòng)獲取鎖,這便是偏向鎖堡距。
只有存在其他線程競(jìng)爭(zhēng)偏向鎖的時(shí)候甲锡,持有偏向鎖的線程才會(huì)釋放掉偏向鎖;偏向鎖在JDK1.6之后是自動(dòng)開啟的羽戒,可以通過-XX:-UseBiasedLocking來進(jìn)行操作缤沦。
- 2.2 輕量級(jí)鎖
當(dāng)偏向鎖被其他線程競(jìng)爭(zhēng)時(shí),便會(huì)升級(jí)為輕量級(jí)鎖易稠;另外缸废,競(jìng)爭(zhēng)偏向鎖未成功時(shí),該線程會(huì)進(jìn)行自旋操作而不是直接阻塞驶社,從而提高系統(tǒng)性能企量。
- 2.3 重量級(jí)鎖
當(dāng)競(jìng)爭(zhēng)偏向鎖的線程自旋達(dá)到一定次數(shù),或者在它自旋的時(shí)候亡电,又來了一個(gè)競(jìng)爭(zhēng)鎖的線程梁钾,這時(shí),輕量級(jí)鎖會(huì)升級(jí)為重量級(jí)鎖逊抡;升級(jí)成重量級(jí)鎖之后姆泻,會(huì)將除了持有鎖之外的線程都阻塞(自旋的停止自旋)。
- 2.4 各量級(jí)鎖的轉(zhuǎn)換規(guī)則
鎖升級(jí)的順序是偏向鎖-->輕量級(jí)鎖-->重量級(jí)鎖冒嫡,鎖只可以升級(jí)不可以降級(jí)拇勃。
-
從申請(qǐng)鎖的順序來看
3.1 公平鎖
公平鎖是指多個(gè)線程按照申請(qǐng)鎖的順序來獲取鎖,線程直接進(jìn)入隊(duì)列中排隊(duì)孝凌,隊(duì)列中的第一個(gè)線程才能獲得鎖方咆。公平鎖的優(yōu)點(diǎn)是等待鎖的線程不會(huì)餓死。缺點(diǎn)是整體吞吐效率相對(duì)非公平鎖要低蟀架,等待隊(duì)列中除第一個(gè)線程以外的所有線程都會(huì)阻塞瓣赂,CPU喚醒阻塞線程的開銷比非公平鎖大。
例如:你去食堂吃飯片拍,你看到有人排列煌集,你便自覺的去隊(duì)尾排列,打飯的阿姨在打完一個(gè)飯的時(shí)候會(huì)按照順序叫下一個(gè)同學(xué)來打飯捌省。
- 3.2 非公平鎖
非公平鎖是多個(gè)線程加鎖時(shí)直接嘗試獲取鎖苫纤,獲取不到才會(huì)到等待隊(duì)列的隊(duì)尾等待。但如果此時(shí)鎖剛好可用,那么這個(gè)線程可以無需阻塞直接獲取到鎖卷拘,所以非公平鎖有可能出現(xiàn)后申請(qǐng)鎖的線程先獲取鎖的場(chǎng)景喊废。非公平鎖的優(yōu)點(diǎn)是可以減少喚起線程的開銷,整體的吞吐效率高栗弟,因?yàn)榫€程有幾率不阻塞直接獲得鎖污筷,CPU不必喚醒所有線程。缺點(diǎn)是處于等待隊(duì)列中的線程可能會(huì)餓死乍赫,或者等很久才會(huì)獲得鎖瓣蛀。
例如:你去食堂打飯,雖然已經(jīng)有人在排隊(duì)了耿焊,但是阿姨剛打完上一個(gè)同學(xué)的飯揪惦,還沒來得及叫下一個(gè)人遍搞,你可以直接去插隊(duì)打飯罗侯,阿姨就不會(huì)給隊(duì)首的同學(xué)打飯,而是先給你打飯溪猿。
-
從鎖的共享程度來看
4.1 共享鎖
共享鎖是指該鎖可被多個(gè)線程所持有钩杰。JDK中的ReentrantReadWriteLock的ReadLock是共享鎖。
- 4.2 排他鎖
排他鎖诊县,是指該鎖一次只能被一個(gè)線程所持有讲弄。JDK中的synchronized和JUC中Lock的實(shí)現(xiàn)類就是互斥鎖。
-
從鎖是否可以被一個(gè)線程重復(fù)獲取來看
可重入鎖
可重入鎖依痊,是指在同一個(gè)線程在外層方法獲取鎖的時(shí)候避除,再進(jìn)入該線程的內(nèi)層方法會(huì)自動(dòng)獲取鎖(前提鎖對(duì)象得是同一個(gè)對(duì)象或者class),不會(huì)因?yàn)橹耙呀?jīng)獲取過還沒釋放而阻塞胸嘁。Java中ReentrantLock和synchronized都是可重入鎖瓶摆,可重入鎖的一個(gè)優(yōu)點(diǎn)是可一定程度避免死鎖。
示例代碼
/**
* 重入鎖示例代碼
*/
public class ReentrantLockDemo {
public synchronized void getLock1(){
System.out.println("獲取對(duì)象鎖的method1");
getLock2();
}
public synchronized void getLock2(){
System.out.println("獲取對(duì)象鎖的method2");
}
public static void main(String[] args) {
new ReentrantLockDemo().getLock1();
}
}
在執(zhí)行g(shù)etLock1方法時(shí)性宏,已經(jīng)獲取了對(duì)象鎖群井,執(zhí)行g(shù)etLock2方法時(shí)并未釋放鎖;而getLock2方法也是需要獲取對(duì)象鎖的毫胜,方法卻可以正常執(zhí)行书斜,這便可以說明synchronized是可重入鎖。
- 不可重入鎖
不可重入鎖則不會(huì)讓一個(gè)線程獲取兩次酵使,相對(duì)可重入鎖來說荐吉,更容易造成死鎖。
volatile關(guān)鍵字
volatile關(guān)鍵字可以保證順序性以及可見性口渔,無法保證原子性稍坯,所以單純的使用volatile關(guān)鍵字是無法實(shí)現(xiàn)多線程安全的,但是如果設(shè)計(jì)巧妙的話,也可以較為輕量的解決多線程安全問題瞧哟,典型就是以“雙重校驗(yàn)鎖”的方式實(shí)現(xiàn)單例模式混巧。
/**
* 雙重校驗(yàn)鎖 實(shí)現(xiàn)單例模式
*/
public class SinglePattern {
private static volatile SinglePattern singlePattern;
private SinglePattern(){}
public static SinglePattern getInstance(){
if(singlePattern == null){
synchronized(SinglePattern.class){
if(singlePattern == null){
singlePattern = new SinglePattern();
}
}
}
return singlePattern;
}
}
java內(nèi)存模型
java內(nèi)存模型(Java Memory Model,JMM)是java虛擬機(jī)規(guī)范定義的勤揩,用來屏蔽掉java程序在各種不同的硬件和操作系統(tǒng)對(duì)內(nèi)存的訪問的差異咧党,這樣就可以實(shí)現(xiàn)java程序在各種不同的平臺(tái)上都能達(dá)到內(nèi)存訪問的一致性,下面簡(jiǎn)單介紹下與java內(nèi)存模型相關(guān)的happen-before原則陨亡。
- 單線程happen-before原則:在同一個(gè)線程中傍衡,書寫在前面的操作happen-before后面的操作。
- 鎖的happen-before原則:同一個(gè)鎖的unlock操作happen-before此鎖的lock操作负蠕。
- volatile的happen-before原則:對(duì)一個(gè)volatile變量的寫操作happen-before對(duì)此變量的任意操作
- happen-before的傳遞性原則:如果A操作happen-before B操作蛙埂,B操作happen-before C操作,那么A操作happen-before C 操作
- 線程啟動(dòng)的happen-before原則:同一個(gè)線程的start方法happen-before此線程的其他方法遮糖。
- 線程中斷的happen-before原則:對(duì)縣城interrupt方法的調(diào)用happen-before被中斷線程的檢測(cè)到中斷發(fā)送的代碼绣的。
- 線程終結(jié)的happen-before原則:線程中的所有操作都happen-before線程的終止檢測(cè)。
- 對(duì)象創(chuàng)建的happen-before原則:對(duì)象的初始化完成先于他的finalize方法的調(diào)用欲账。
ThreadLocal
ThreadLocal屡江,顧名思義,它不是一個(gè)線程赛不,而是線程的一個(gè)本地化對(duì)象惩嘉。當(dāng)工作于多線程中的對(duì)象使用ThreadLocal維護(hù)變量時(shí),ThreadLocal為每個(gè)使用該變量的線程分配一個(gè)獨(dú)立的變量副本踢故。所以每一個(gè)線程都可以獨(dú)立地改變自己的副本文黎,而不會(huì)影響其他線程所對(duì)應(yīng)的副本。從線程的角度看殿较,這個(gè)變量就像是線程的本地變量耸峭,這也是類名中“Local”所要表達(dá)的意思。
Java的鎖的理念是“時(shí)間換空間”斜脂,而ThreadLocal的理念是“用空間換時(shí)間”抓艳,下面寫個(gè)例子演示下TheadLocal的用法。
public class ThreadLocalDemo implements Runnable{
//設(shè)置一個(gè)threadLocal變量存儲(chǔ)Integer
private ThreadLocal<Integer> formatter = new ThreadLocal();
@Override
public void run() {
System.out.println("Thread Name= "+Thread.currentThread().getId()+" 當(dāng)前取到的值為 "+formatter.get());
formatter.set(new Integer(new Random().nextInt(100)));
System.out.println("Thread Name= "+Thread.currentThread().getId()+" 設(shè)置后的值為 = "+formatter.get());
}
public static void main(String[] args) throws InterruptedException {
ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
Thread thread1 = new Thread(threadLocalDemo);
Thread thread2 = new Thread(threadLocalDemo);
Thread thread3 = new Thread(threadLocalDemo);
Thread thread4 = new Thread(threadLocalDemo);
Thread thread5 = new Thread(threadLocalDemo);
Thread thread6 = new Thread(threadLocalDemo);
thread1.start();
thread1.join();
thread2.start();
thread2.join();
thread3.start();
thread3.join();
thread4.start();
thread4.join();
thread5.start();
thread5.join();
thread6.start();
}
}