請(qǐng)說(shuō)intern方法。和猜測(cè)下面代碼的運(yùn)行結(jié)果糯而。
public static void main(String[] args) {
String str1 = new StringBuffer("58").append("tongcheng").toString();
String str2 = new StringBuffer("ja").append("va").toString();
System.out.println(str1);
System.out.println(str1.intern());
System.out.println(str1 == str1.intern());
System.out.println(str2);
System.out.println(str2.intern());
System.out.println(str2 == str2.intern());
}
首先這個(gè)intern方法是一個(gè)native本地方法天通,它的作用是如果字符串常量池中已經(jīng)包含一個(gè)等于此String對(duì)象的字符串,則返回代表池中這個(gè)字符串的String對(duì)象的引用熄驼。否則將此String對(duì)象包含的字符串添加到常量池中像寒,并返回此String對(duì)象的引用。
而上面的代碼其實(shí)考點(diǎn)就是這兩個(gè)等于的結(jié)果瓜贾。答案是第一個(gè)是true诺祸,第二個(gè)是false。
而且這里只有java會(huì)false阐虚。因?yàn)樵趈dk中有個(gè)類sun.misc.Version.加載這個(gè)類的時(shí)候會(huì)自動(dòng)把一個(gè)launcher_num的靜態(tài)變量注入序臂。這個(gè)變量的值就是java蚌卤。
所以說(shuō)上面代碼執(zhí)行之前实束,常量池中已經(jīng)存在java字符串了。所以str2的指向和java字符串的指向是不一樣的逊彭。所以false咸灿。
這個(gè)題是深入理解JVM虛擬機(jī)第三版的原題:
談?wù)凙QS和LockSupport
這里可以先聊聊可重入鎖:可重入鎖又叫遞歸鎖。是指在同一個(gè)線程的外層方法中獲取鎖的時(shí)候侮叮,再進(jìn)入該線程的內(nèi)層方法會(huì)自動(dòng)獲取鎖(前提鎖的是同一個(gè)對(duì)象)避矢。不會(huì)因?yàn)橹耙呀?jīng)過(guò)去過(guò)鎖還沒(méi)釋放而阻塞。
java中ReentrantLock和Synchronized都是可重入鎖囊榜∩笮兀可重入鎖的一個(gè)優(yōu)點(diǎn)是一定程度避免死鎖。
Synchronized
下面是一個(gè)簡(jiǎn)單的可重入demo:
上面代碼中m1卸勺,m2都是由synchronized鎖住的砂沛。在進(jìn)入m1的時(shí)候持有鎖了,方法里調(diào)用m2直接進(jìn)入m2了曙求。這個(gè)時(shí)候m1的鎖還沒(méi)釋放碍庵。因?yàn)閙2也是這個(gè)鎖映企,所以能靠這把未釋放的鎖進(jìn)入m2。證明了可重入性静浴。
同理堰氓,其實(shí)這里同步代碼塊更明了:
沒(méi)有死鎖本身就說(shuō)明了synchronized的同步塊可重入。下面貼上完整demo代碼:
public class LockDemo {
public synchronized void m1() {
System.out.println("進(jìn)入到m1方法苹享!"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
m2();
try {
TimeUnit.SECONDS.sleep(5);
} catch (Exception e) {
e.printStackTrace();
}
}
public synchronized void m2() {
System.out.println("進(jìn)入到m2方法双絮!"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
try {
TimeUnit.SECONDS.sleep(5);
} catch (Exception e) {
e.printStackTrace();
}
}
public void m3() {
synchronized (this) {
System.out.println("進(jìn)入m3的一層同步塊");
synchronized (this) {
System.out.println("進(jìn)入m3的二層同步塊");
synchronized (this) {
System.out.println("進(jìn)入m3的三層同步塊");
}
}
}
}
public static void main(String[] args) {
LockDemo lockDemo = new LockDemo();
lockDemo.m3();
new Thread(()->lockDemo.m1()).start();
new Thread(()->lockDemo.m2()).start();
}
}
可重入鎖實(shí)現(xiàn)原理:每個(gè)鎖對(duì)象擁有一個(gè)鎖計(jì)數(shù)器和一個(gè)指向持有該鎖的線程的指針。
當(dāng)執(zhí)行monitor-enter的時(shí)候得问,如果該對(duì)象計(jì)數(shù)器為0說(shuō)明當(dāng)前鎖沒(méi)有被其他線程鎖占有掷邦,java虛擬機(jī)會(huì)將該鎖對(duì)象的持有線程設(shè)置為當(dāng)前線程。并將計(jì)數(shù)器+1.
在目標(biāo)鎖對(duì)象不為0的情況下:
- 如果鎖的持有者是當(dāng)前線程椭赋。則鎖對(duì)象的計(jì)數(shù)器+1.
- 如果鎖對(duì)象不是當(dāng)前線程則要等待鎖對(duì)象的計(jì)數(shù)器歸0抚岗,才能獲得鎖。
當(dāng)執(zhí)行monitor-exit的時(shí)候哪怔,java虛擬機(jī)將鎖對(duì)象的計(jì)數(shù)器-1.計(jì)數(shù)器歸0代表鎖已經(jīng)被釋放宣蔚。
ReentrantLock
測(cè)試可重入的demo如下:
public class LockDemo {
Lock lock = new ReentrantLock();
public String getTime() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
}
public void m1() {
lock.lock();
try {
System.out.println("進(jìn)入m1方法"+getTime());
m2();
TimeUnit.SECONDS.sleep(5);
} catch (Exception e) {
}finally {
lock.unlock();
}
}
public void m2() {
lock.lock();
try {
System.out.println("進(jìn)入m2方法"+getTime());
TimeUnit.SECONDS.sleep(5);
} catch (Exception e) {
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
LockDemo lockDemo = new LockDemo();
lockDemo.m1();
}
}
如上測(cè)試,這兩個(gè)打印語(yǔ)句是同時(shí)調(diào)用的认境。說(shuō)明不用等m1的鎖釋放胚委,就能進(jìn)入到m2的方法中,也說(shuō)明了ReentrantLock的可重入叉信。而lock和synchronized最主要的區(qū)別的synchronized是隱式獲取鎖和釋放鎖亩冬。而reentrantLock的是要手動(dòng)lock,unlock的硼身。而lock和unlock就是syncronized原理是+1硅急,-1的操作。記住加幾次鎖就要放幾次鎖佳遂。否則會(huì)導(dǎo)致鎖無(wú)法釋放营袜。這個(gè)我就不測(cè)試了。畢竟是很基礎(chǔ)的知識(shí)點(diǎn)丑罪。
說(shuō)完了可重入鎖荚板,下面直接說(shuō)LockSupport.
LockSupport
如果覺(jué)得這個(gè)類很陌生,我們學(xué)習(xí)的最佳方式就是官網(wǎng):所以可以去jdk文檔中找找看:
一句話總結(jié):是線程等待喚醒機(jī)制(wait/notify)的改良加強(qiáng)版.
LockSupport中park()和unpark()的作用分別是阻塞線程和解除阻塞線程.
這塊其實(shí)之前學(xué)juc的時(shí)候就學(xué)到過(guò).下面一張圖表示java線程中等待喚醒機(jī)制的發(fā)展進(jìn)步:
我們目前知道的三個(gè)版本的等待喚醒:
-
Object的wait/notify
這種做法的局限性:兩者必須在synchronized方法/同步塊中執(zhí)行.wait和notify的順序必須嚴(yán)格執(zhí)行.如果先notify再wait,那么會(huì)無(wú)限制等待下去. -
Condition的await和signal
這種做法的局限性:必須和lock/unlock之間使用.同時(shí)也必須先await在signal.否則await會(huì)無(wú)限制等下去(不設(shè)置時(shí)間的.).
這兩種情況都有兩個(gè)約束:1.先持有鎖.2.先等待再喚醒.
-
LockSupport的park和unpark
這兩個(gè)方法底層調(diào)用的unsafe類.具體用法如下demo:
public class LockDemo {
public static void main(String[] args) {
Thread a = new Thread(()->{
System.out.println("進(jìn)入到a線程");
LockSupport.park();
System.out.println("a線程被喚醒");
},"a");
a.start();
new Thread(()->{
System.out.println("進(jìn)入到b線程");
LockSupport.unpark(a);
},"b") .start();
}
}
首先看這個(gè)代碼很明顯:阻塞喚醒不需要先獲取鎖.
其次我們可以代碼測(cè)試一下順序:
先喚醒后等待也支持.當(dāng)然了這個(gè)要一對(duì)一的.就是先unpark一次,就可以解park一次,如果unpark一次,park兩次,也還是要等待的:
接下來(lái)我們簡(jiǎn)單看下LockSupport源碼:
歸根結(jié)底LockSupport是一個(gè)線程阻塞工具類.所有的方法都是靜態(tài)方法.可以讓線程在任意位置阻塞.阻塞之后也有對(duì)應(yīng)的喚醒方法.其本質(zhì)上調(diào)用的是Unsafe類的native方法.
其實(shí)現(xiàn)原理是這樣的:每個(gè)使用它的線程都有一個(gè)許可(permit)關(guān)聯(lián).permit相當(dāng)于1,0的開(kāi)關(guān).默認(rèn)是0.
調(diào)用一次unpark就加1變成1.
調(diào)用一次park就把1變成0.同時(shí)park立即返回.
注意的是只有0,1兩種狀態(tài).也就是連續(xù)調(diào)用unpark多次,也只能讓許可證變成1.能解一次park而已.
形象點(diǎn)理解:
- 線程阻塞需要消耗憑證permit.這個(gè)憑證最多只有一個(gè).
- 調(diào)用park時(shí):
- 如果有憑證.則消耗這個(gè)憑證并且正常退出
- 如果沒(méi)憑證,則要等待有憑證才可以退出
- 調(diào)用unpark時(shí),會(huì)增加一個(gè)憑證,但是憑證的上限是1.
AQS詳解
AQS全稱是AbstractQueuedSynchronizer。中文翻譯過(guò)來(lái)其實(shí)就是三個(gè)單詞:抽象的吩屹,隊(duì)列跪另,同步器。
AQS是用來(lái)構(gòu)建鎖或者其他同步器組件的重量級(jí)基礎(chǔ)框架以及整個(gè)JUC體系的基石煤搜。通過(guò)內(nèi)置的先進(jìn)先出(first in first out,簡(jiǎn)稱FIFO)隊(duì)列來(lái)完成資源獲取線程的排隊(duì)工作免绿。并通過(guò)一個(gè)int類型變量表示持有鎖的狀態(tài)。
為什么說(shuō)AQS是juc體系的基石呢宅楞?
簡(jiǎn)單來(lái)說(shuō)针姿,ReentrantLock袱吆,CountDownLatch,ReentrantReadWriteLock距淫,Semaphore這些類绞绒,都用到了搶鎖放鎖等。這些都用到了AQS榕暇,如下源碼截圖:
鎖和同步器的關(guān)系蓬衡?
鎖-面向鎖的使用者。定義了使用層的api彤枢,隱藏了實(shí)現(xiàn)細(xì)節(jié)狰晚,調(diào)用即可。
同步器-面向鎖的實(shí)現(xiàn)者缴啡。提出了統(tǒng)一規(guī)范并簡(jiǎn)化了鎖的實(shí)現(xiàn)壁晒。屏蔽了同步狀態(tài)管理,阻塞線程排隊(duì)和通知业栅,喚醒機(jī)制等秒咐。
有阻塞就需要排隊(duì),而實(shí)現(xiàn)排隊(duì)必然需要有某種形式的隊(duì)列來(lái)進(jìn)行管理碘裕。
一堆線程搶一個(gè)鎖的時(shí)候携取,搶到資源的線程處理業(yè)務(wù)邏輯,搶不到的排隊(duì)帮孔。但是等待線程仍然保留著獲取鎖的可能并且獲取鎖的流程還在繼續(xù)雷滋。這就是排隊(duì)等候機(jī)制。
如果共享資源被占用文兢,就需要一定的阻塞等待喚醒機(jī)制來(lái)保證鎖分配晤斩。這個(gè)機(jī)制主要用的是CLH隊(duì)列的變體實(shí)現(xiàn)。將暫時(shí)獲取不到鎖的線程加入到隊(duì)列中禽作,這個(gè)隊(duì)列就是AQS的抽象表現(xiàn)尸昧。
它將請(qǐng)求共享資源的線程封裝成隊(duì)列的節(jié)點(diǎn)揩页。通過(guò)CAS旷偿,自旋以及LockSupport.park()的方式,維護(hù)state變量的狀態(tài)爆侣,使并發(fā)達(dá)到同步的控制效果萍程。
AQS底層使用了一個(gè)volatile的int類型的成員變量來(lái)表示同步狀態(tài)。通過(guò)內(nèi)置的FIFO隊(duì)列來(lái)完成資源獲取的排隊(duì)工作兔仰。將每條要搶占資源的線程封裝成一個(gè)Node結(jié)點(diǎn)來(lái)實(shí)現(xiàn)鎖的分配茫负。通過(guò)CAS完成對(duì)state值的修改。
由上面兩個(gè)代碼說(shuō)明了AQS的等待隊(duì)列的數(shù)據(jù)類型是Node乎赴,而Node中裝的是Thread忍法。
AQS類中有個(gè)volatile修飾的state變量潮尝。當(dāng)state是0的時(shí)候說(shuō)明沒(méi)線程占有資源。大于等于1的時(shí)候說(shuō)明有線程占有資源饿序。再有后來(lái)的線程是要排隊(duì)的勉失。我們繼續(xù)看AQS是源碼會(huì)發(fā)現(xiàn)雖然方法很多,看似挺復(fù)雜的原探。但是AQS本身最外層的屬性就是一個(gè)state變量和一個(gè)clh變種的雙端隊(duì)列乱凿。如下截圖:
而我們之所以說(shuō)Node是雙端隊(duì)列也很容易看出來(lái),我們可以看Node的源碼:
Node其實(shí)可以看成一個(gè)單獨(dú)的類咽弦。雖然是內(nèi)部的徒蟆。然后其屬性也都是很有用的。除了上面簡(jiǎn)單說(shuō)的頭尾節(jié)點(diǎn)型型,還有別的段审,首先作為雙向鏈表,上一個(gè)下一個(gè)元素的指針必有的闹蒜,其次首尾節(jié)點(diǎn)上面就說(shuō)了戚哎,都是鏈表的基本知識(shí),就不說(shuō)了嫂用。還有一個(gè)屬性比較有用:waitStatus:表示的是排隊(duì)的每一個(gè)節(jié)點(diǎn)的狀態(tài)型凳。
- 0是初始化Node的時(shí)候的默認(rèn)值。
- 1 表示線程獲取鎖的請(qǐng)求已經(jīng)取消了
- -2 表示節(jié)點(diǎn)在等待隊(duì)列中嘱函,等著喚醒
- -3 當(dāng)前線程處于shared情況下該字段才會(huì)使用
-
-1表示線程已經(jīng)準(zhǔn)備好就等著資源釋放了
源碼中狀態(tài)注釋
AQS源碼解讀
現(xiàn)在為止簡(jiǎn)單的理解了下AQS的體系和大致類結(jié)構(gòu)屬性甘畅。下面一步一步源碼解讀:
還是從ReentrantLock說(shuō)起,Lock接口的實(shí)現(xiàn)類往弓,基本都是通過(guò)聚合了一個(gè)隊(duì)列同步器的子類完成線程訪問(wèn)控制的疏唾。
這句話我們看著代碼更好理解:
上面兩段代碼就可以看出來(lái):lock.lock本質(zhì)上是在lock類中聚合一個(gè)AQS的實(shí)現(xiàn)類,然后調(diào)用lock和unlock都是調(diào)用這個(gè)AQS的實(shí)現(xiàn)類的方法來(lái)實(shí)現(xiàn)(unlock是調(diào)用sysn.release(1);)的函似。
然后注意槐脏,我們知道ReentrantLock默認(rèn)是非公平鎖的,可以創(chuàng)建的時(shí)候傳參設(shè)置為公平鎖撇寞,那么這個(gè)公平還是非公平對(duì)于AQS的實(shí)現(xiàn)類有什么區(qū)別呢比搭?
講真钾虐,這個(gè)就好像是在套娃。根據(jù)傳參的不同去實(shí)現(xiàn)不同的配置的Sync類型,Sync類型又是AQS的實(shí)現(xiàn)類蜂挪。其實(shí)這些只要看代碼雖然不一定能理解人家為什么這么寫(xiě)数冬,但是還挺好看懂的非洲。這兩種實(shí)現(xiàn)有什么不同呢段誊?繼續(xù)在源碼中找答案:
因?yàn)轱@示原因就這么看,明顯是兩個(gè)方法,我們?nèi)?duì)比這兩個(gè)方法的區(qū)別:
很容易能看出來(lái)兩個(gè)方法只有一個(gè)區(qū)別:公平鎖多了一個(gè)判斷懂从,方法如下:
很明顯這個(gè)方法是判斷當(dāng)前隊(duì)列是不是有元素授段。如果這個(gè)鎖的等待隊(duì)列中已經(jīng)有了線程,則方法放回true番甩,在嘗試獲取鎖的時(shí)候條件是非true也就是false畴蒲。因?yàn)檫@里用的是&&,一個(gè)false則全部false对室,所以不往下走了模燥,直接返回false,當(dāng)前線程要進(jìn)入等待隊(duì)列去排隊(duì)掩宜。
而非公平鎖則不用進(jìn)行這個(gè)判斷蔫骂,直接嘗試獲取鎖。獲取到了就true牺汤,獲取不到走false辽旋。
下面我們用debug的方式一步一步走一下代碼:
從lock開(kāi)始:
也就是如果當(dāng)前是0.state是0說(shuō)明當(dāng)前資源沒(méi)人占用,這個(gè)時(shí)候設(shè)置值為1并且返回true檐迟,調(diào)用設(shè)置當(dāng)前線程為資源擁有者:
而如果當(dāng)前資源有線程占有了补胚,則cas返回false,所以走else分支追迟,調(diào)用acquire方法:
繼續(xù)往下走這個(gè)方法:
這個(gè)方法直接看是就拋了個(gè)異常溶其,我們可以點(diǎn)進(jìn)實(shí)現(xiàn)類里看,因?yàn)槲覀冏铋_(kāi)始就是非公平鎖敦间,所以看NonfairSync的實(shí)現(xiàn):
其實(shí)代碼邏輯也挺簡(jiǎn)單的瓶逃,先看看資源狀態(tài)是不是0,是0則試圖用cas搶資源廓块。不是0判斷線程是不是當(dāng)前資源持有線程厢绝,是的話返回true(這里證明了鎖的可重入)。不是返回false带猴。
現(xiàn)在如果資源被占用且不是當(dāng)前線程占用的昔汉,這個(gè)方法肯定是返回false,這個(gè)時(shí)候繼續(xù)往下走代碼:因?yàn)樵赼cquire方法tryAcquire是取反的拴清,返回false靶病,!false是true贷掖,則繼續(xù)下一個(gè)判斷:
這個(gè)方法也分兩步:一個(gè)是acquireQueued方法嫡秕,一個(gè)是addWaiter方法。addWaiter其實(shí)很明顯苹威,因?yàn)閍cquireQueued方法的兩個(gè)參數(shù)第一個(gè)是Node,第二個(gè)是int驾凶。而addWaiter方法的結(jié)果作為第一個(gè)參數(shù)牙甫。所以我們可以合理的猜測(cè)addWaiter方法是將當(dāng)前線程轉(zhuǎn)化為Node方法掷酗。猜測(cè)完畢下面我們用代碼去確定:
分析代碼,addWaiter第一行代碼就是創(chuàng)建了一個(gè)Node對(duì)象窟哺,并且把當(dāng)前線程當(dāng)參數(shù)構(gòu)建的Node泻轰。然后把這個(gè)Node對(duì)象掛到雙端隊(duì)列上去。掛的邏輯是pre指向之前的末尾元素且轨。而tail指向的是當(dāng)前元素浮声。因?yàn)槭请p向隊(duì)列,所以之前的最后一個(gè)節(jié)點(diǎn)的下一個(gè)指向新添加的旋奢。就是一個(gè)很簡(jiǎn)單的邏輯泳挥。然后這里有兩個(gè)分支:一個(gè)是添加到隊(duì)列,還有一個(gè)是單獨(dú)的enq(node)方法至朗,區(qū)別是如果隊(duì)列存在則添加屉符。如果隊(duì)列不存在則先創(chuàng)建再添加。
然后重點(diǎn)就是入隊(duì)的方法:
這個(gè)方法的重點(diǎn)其實(shí)是要看下紅框里的兩個(gè)方法:前面的比較眼熟了應(yīng)該锹引,還是嘗試獲取鎖矗钟。問(wèn)題是獲取不到鎖會(huì)走下面的兩個(gè)方法:
這個(gè)比較容易理解,就是判斷當(dāng)前這個(gè)線程還是不是在等著呢嫌变。其實(shí)重點(diǎn)在第二個(gè)方法:
這個(gè)線程想搶鎖吨艇,但是沒(méi)搶到,所以阻塞了腾啥。(注意之前的方法是自旋的秸应。也就是這個(gè)線程被喚醒以后還會(huì)重復(fù)這個(gè)方法的操作)。
至于什么時(shí)候會(huì)被喚醒碑宴。其實(shí)猜也能猜到软啼,肯定是獲取這個(gè)資源的線程釋放鎖以后喚醒所有隊(duì)列中的線程。我們也可以順著代碼去找一下喚醒的步驟:
而且這個(gè)方法中如果是正常情況下延柠,state會(huì)變成0.并且這里還有個(gè)判斷:
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
如果當(dāng)前線程未持有鎖則不可以釋放祸挪。所以說(shuō)如果沒(méi)有l(wèi)ock先unlock會(huì)報(bào)錯(cuò)。就是這句代碼起的作用贞间。
繼續(xù)往下說(shuō)這個(gè)tryRelease方法如果c等于0說(shuō)明鎖徹底被釋放贿条,會(huì)返回true,然后代碼往下走增热,走到了另一個(gè)方法:
這里的兩點(diǎn)就是unpark整以。之前所有等待的線程都被park了,現(xiàn)在在解鎖以后峻仇,這個(gè)unpark就把之前掛起來(lái)的等待線程叫醒了公黑。
并且注意入隊(duì)我們知道了,但是出隊(duì)的過(guò)程是在一個(gè)很意想不到的地方:也就是掛起線程的哪個(gè)自旋結(jié)束后。因?yàn)樽孕杏袃蓚€(gè)分支:一個(gè)是搶占成功了還有一個(gè)是搶占失敗了凡蚜,而如果搶占成功了的話會(huì)直接將當(dāng)前線程出隊(duì)的人断。
至此,所有的邏輯都串起來(lái)了朝蜘。AQS的大部分邏輯都是這樣的恶迈。中間可能有一些方法略過(guò)了或者沒(méi)說(shuō),但是總體流程就是這樣谱醇。
非公平鎖是每次tryAcquire時(shí)如果當(dāng)前資源處于0暇仲,沒(méi)有被占有的狀態(tài),每個(gè)線程都有機(jī)會(huì)去獲取鎖副渴,而公平鎖在tryAcquire中哪怕資源沒(méi)被占有奈附,也只有隊(duì)首的元素有資格去獲取鎖。
本篇筆記就記到這里佳晶,如果稍微幫到你了記得點(diǎn)個(gè)喜歡點(diǎn)個(gè)關(guān)注桅狠,也祝大家工作順順利利,生活健健康康轿秧!其實(shí)偶爾我會(huì)覺(jué)得看源碼是種很有意思的事中跌,去看人家的邏輯,流程走向菇篡,代碼的書(shū)寫(xiě)等漩符。我記得都說(shuō)看一本好書(shū)開(kāi)拓視野,回味無(wú)窮驱还。其實(shí)一個(gè)好的源碼也可以如此嗜暴。愿我們?cè)谇笏鞯穆飞弦煌鶡o(wú)前吧!