首先看這樣一段代碼
static int count = 0;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for(int i =0;i<5000;i++){
count++;
}
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for(int i =0;i<5000;i++){
count--;
}
}
},"t2");
t1.start();
t2.start();
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("最終count的值 = {}",count);
}
對于共享變量count,在一個線程中循環(huán)5000次自加辈灼,在另一個線程中循環(huán)5000次自減,等兩個線程都運行結(jié)束之后,打印出count的值并不等于0造虎,
這是因為對于count++來說,在字節(jié)碼中的指令時這樣的
0: getstatic #2 // Field count:I
3: iconst_1
4: iadd
5: putstatic #2 // Field count:I
這里分為了四步操作
- 取出靜態(tài)變量count
- 取出數(shù)字1
- 執(zhí)行兩者相加
- 放入到靜態(tài)變量中
同理count--也是同樣的操作
另外JMM也定義了線程對變量的操作并不是直接在主內(nèi)存讀寫纷闺,而是讀取到線程的工作內(nèi)存中算凿,等工作內(nèi)存操作完畢之后再寫入到主內(nèi)存中來,這樣就會出現(xiàn)當一個線程操作完自加之后還沒有寫入到主內(nèi)存中時急但,發(fā)現(xiàn)了線程上下文切換澎媒,另一個線程讀取主內(nèi)存時 還是初始值,這時候完成了自減并寫入主內(nèi)存中波桩,發(fā)生上下文切換時戒努,第一個線程開始往里面寫入之前的值,就會造成數(shù)據(jù)的覆蓋,這也是為什么最終的數(shù)值并沒有如逾期一樣
臨界區(qū)
- 一個程序運行多個線程是沒有問題的
- 問題出在多個線程訪問共享資源
- 多個線程讀共享資源也沒有問題
- 在多個線程對共享資源讀寫操作時發(fā)生指令交錯储玫,就會出現(xiàn)問題
- 一段代碼塊內(nèi)如果存在對共享資源的多線程讀寫問題侍筛,這個代碼就叫做臨界區(qū)
如上面實例中 count++,count-- 就是臨界區(qū)
競態(tài)條件(Race Condition)
- 在多個線程在臨界區(qū)內(nèi)執(zhí)行,由于代碼的執(zhí)行序列不同而導(dǎo)致結(jié)果無法預(yù)測撒穷,稱之為發(fā)生的競態(tài)條件
- 為了避免臨界區(qū)的競態(tài)條件發(fā)生匣椰,可以使用如下解決方法
- 阻塞式:synchronized,Lock
- 非阻塞式:原子變量
Synchronized
synchronized 就是我們常說的對象鎖,它是采用互斥的方式讓同一時刻最多只有一個線程持有對象說端礼,其他線程再想獲取這個對象鎖時就會阻塞住禽笑,這樣保證了擁有鎖的線程可以安全的執(zhí)行臨界區(qū)代碼,不用擔(dān)心線程的上下文切換
值得注意的是在java中互斥和同步都可以采用synchronized關(guān)鍵字
- 互斥是保證臨界區(qū)的競態(tài)條件發(fā)生時蛤奥,同一時刻只能有一個線程執(zhí)行臨界區(qū)代碼
- 同步是由于線程執(zhí)行的先后佳镜,順序不同,需要一個線程等待其他線程運行到某個點
- synchronized 實際上就是用對象鎖保證了臨界區(qū)代碼的原子性
synchronized語法
synchronized(對象){
臨街區(qū)
}
在方法上的synchronized是這樣的
public class Test {
public synchronized void test(){
}
}
等價于
public class Test {
public void test(){
synchronized (this){
}
}
}
這兩個方法表示的是針對成員方法而言鎖住的對象實例
public class Test {
public static synchronized void test() {
}
}
等價于
public class Test {
public void test(){
synchronized (Test.class){
}
}
}
對于靜態(tài)方法而言凡桥,鎖住的是類對象
變量的線程安全
成員變量和靜態(tài)變量
- 如果沒有被共享蟀伸,是線程安全
- 如果被共享了
- 如果只有讀操作,是線程安全
- 如果有讀寫操作缅刽,需要考慮線程安全的問題
局部變量
局部變量自身是安全的
public class Test {
public void test(){
int i= 10;
i++;
}
}
這樣一個代碼啊掏,查看字節(jié)碼是這樣的
public void test();
Code:
0: bipush 10
2: istore_1
3: iinc 1, 1
6: return
可以看出它的i++的指令是iinc,這是因為局部變量屬于線程內(nèi)部的,當多個線程訪問時衰猛,會在每個線程的棧幀內(nèi)存中被創(chuàng)建多份迟蜜,因此不存在共享,是線程安全的
局部變量的引用
在驗證局部變量之前先看一下成員變量的情況
class UnSafeThread{
ArrayList list = new ArrayList();
public void method1(Thread thread,int loopNum){
for(int i = 0;i<loopNum;i++){
method2();
method3();
}
}
public void method2(){
list.add("1");
}
public void method3(){
list.remove(0); }
}
這個類有一個成員變量list啡省,當多個線程調(diào)用method1時小泉,對于method2和method3的調(diào)用都涉及到對成員變量的讀寫,這就有競態(tài)條件的發(fā)生冕杠,因此是線程不安全的
接下來看局部變量引用
- 未對外暴露局部變量引用
class SafeThread{
public void method1(Thread thread,int loopNum){
ArrayList list = new ArrayList();
for(int i = 0;i<loopNum;i++){
method2(list);
method3(list);
}
}
private void method2(ArrayList list){
list.add("1");
}
public void method3(ArrayList list){
list.remove(0);
}
}
這個類局部變量list相當于在每個線程的棧幀中都有副本微姊,因此多個線程訪問時,結(jié)果都是可預(yù)期的分预,因此是線程安全的
2.對外暴露局部變量引用
class SafeThread{
public void method1(Thread thread,int loopNum){
ArrayList list = new ArrayList();
for(int i = 0;i<loopNum;i++){
method2(list);
method3(list);
}
}
public void method2(ArrayList list){
list.add("1");
}
public void method3(ArrayList list){
list.remove(0);
}
}
還是原來的代碼兢交,只不過把method2和method3的修飾符修改為public,這樣一來,子類就可以重寫這兩個方法 如下
class ChildUnSafeThread extends SafeThread{
@Override
public void method3(ArrayList list) {
new Thread(new Runnable() {
@Override
public void run() {
log.debug("size = {}",list.size());
list.remove(0);
}
}).start();
}
}
這樣在子類的方法中新建一個線程笼痹,同樣也做到多線程都訪問共享資源,也會造成競態(tài)條件的發(fā)生配喳,那么這個類就不是線程安全的類
通過上面的例子也可以看出訪問修飾符 private,final 在一定程度上是可以保證線程的安全的
常見的線程安全的類
所謂線程安全就是指多個線程調(diào)用類的同一個實例的某個方法是安全的,這里需要注意的兩點
- 它們的每個方法都是原子的
- 但是它們的多個方法的組合不一定是原子的
這些常見的線程安全的類有:
- String
- Integer等包裝類
- StringBuffer
- Random
- Vector
- HashTable
- java.util.concurrent下的類
對于String,Integer這些不可變類肯定是線程安全的類凳干,因為它們實例的屬性是沒有修改過的晴裹,雖然String的subString和replace看上去改變了值,但是實際上它們內(nèi)部是又創(chuàng)建了新的對象
對于線程安全的類一般都是使用synchronized來加鎖救赐,正如上面所說涧团,雖然這些線程安全的類的每個方法都是原子的但是不代表多個方法組合起來還是線程安全的
比如對于HashTable來說,它的put和get方法都是線程安全的 但是如果結(jié)合起來使用就未必是線程安全的 比如下面的代碼
Hashtable<String,String> table = new Hashtable<>();
public void putIfNull(String key,String value){
if(table.get(key) ==null){
table.put(key, value);
}
}
上面的代碼在多線程訪問的情況下就是不安全的,當兩個線程t1和t2同時執(zhí)行這段代碼時 有可能t1執(zhí)行到get方法時發(fā)生了上下文切換泌绣,這時候t2執(zhí)行g(shù)et方法钮追,然后執(zhí)行put方法,再次發(fā)生上下文切換阿迈,再執(zhí)行線程t1的put方法這樣就把線程2的put的值給覆蓋掉了元媚,產(chǎn)生了不可預(yù)期的結(jié)果,因此是線程不安全的苗沧,如果想要線程安全刊棕,需要在putIfNull上面再添加一個synchronized 關(guān)鍵字
對象頭
32JVM對象頭結(jié)構(gòu)
- 普通對象頭結(jié)構(gòu)
普通對象的頭結(jié)構(gòu)是64bits也就是8個字節(jié),分別是四個字節(jié)的MarkWord和四個字節(jié)的Klass Word
如下圖所示
Mark word(32 bits) | Klass Word(32 bits) |
---|
- 數(shù)組對象頭結(jié)構(gòu)
對象數(shù)組與普通對象頭結(jié)構(gòu)的區(qū)別是在后面多了個四個字節(jié)的數(shù)組長度待逞,因此是12個字節(jié)
Mark word(32 bits) | Klass Word(32 bits) | array length(32bits) |
---|
對于基本數(shù)據(jù)類型int 來說鞠绰,它占用的是四個字節(jié),但是如果用到包裝類Integer的話飒焦,它不僅需要內(nèi)容的四個字節(jié) 還需要對象頭的八個字節(jié),其內(nèi)存占用是基本數(shù)據(jù)類型的3倍屿笼,這也是為什么能用基本類型就不用其包裝類的原因
- Mark Word 結(jié)構(gòu)
Mark Word 是用來存儲一個對象的狀態(tài)信息的結(jié)構(gòu)牺荠,通常根據(jù)它的后兩位可以分為五種狀態(tài)
- 正常狀態(tài)(01)
- 偏向鎖(01)
- 輕量級鎖(00)
- 重量級鎖(10)
- GC(11)
a. 首先看正常狀態(tài)的MarkWord的結(jié)構(gòu)
identity_hashCode(25 bits) | age(4 bits) | biased_lock(1 bits) | flag(2 bits) |
---|
其中flag在正常狀態(tài)下就是01
biased_locked: 是否是偏向鎖標記,正常狀態(tài)是 0驴一,如果是偏向鎖為1休雌,這也是為什么正常狀態(tài)和偏向鎖狀態(tài)在最后兩位都是01的原因
age:表示的是分帶年齡,因為是四個字節(jié)肝断,可以看出一個對象的最大的年齡是15
identity_hashCode:對象標識Hash碼
b. 偏向鎖狀態(tài)結(jié)構(gòu)
thread(23 bit) | epoch(2 bits) | age(4 bits) | biased_lock(1 bits) | flag(2 bits) |
---|
偏向鎖的結(jié)構(gòu)跟正常狀態(tài)的差不多杈曲,flag跟正常狀態(tài)是一樣的,都是01
biased_lock在變相說狀態(tài)下是1胸懈,
thread:持有偏向鎖的線程ID
epoch: 偏向時間戳
c. 輕量級鎖狀態(tài)結(jié)構(gòu)
ptr_to_lock_record(30 bits) | flag(2 bits) |
---|
ptr_to_lock_record: 指向棧中鎖記錄的指針
flag:此時為00
d. 重量級鎖狀態(tài)結(jié)構(gòu)
ptr_to_heavyweight_monitor(30 bits) | flag(2 bits) |
---|
ptr_to_heavyweight_monitor: 指向管程Monitor的指針
flag: 此時為10
e. GC狀態(tài)結(jié)構(gòu)
flag:此時為11
- klass Word
這一部分用于存儲對象的類型指針担扑,JVM通過這個指針確定對象是哪個類的實例
64位JVM對象頭
通過對比32位對象頭更好的理解64位對象頭
- 普通對象的 MarkWord和klassWord分別都是64位
- 數(shù)組對象的 MardWord和KlassWord,arrayLength分別都是64位
- 對于MardWord來說
- 正常狀態(tài)下結(jié)構(gòu)是這樣的
unused(25bits) | identity_hashcode(32bits) | unused(1bits) | age(4bits) | biased_lock(1bits) | flag(2bits) |
---|
- 偏向鎖狀態(tài)下的結(jié)構(gòu)
thread(54) | epoch(2bits) | unused(1bits) | age(4bits) | biased_lock(1bits) | flag(2bits) |
---|
- 對于輕量級鎖和重量級鎖指向棧中鎖記錄或者指向Monitor都是62位
- KlassWord 也是64位趣钱,如果應(yīng)用的對象過多涌献,使用64位的指針會浪費大量的內(nèi)存,為了節(jié)約內(nèi)存首有,可以使用選項+UseCompressedOops開啟指針壓縮燕垃,開啟指針壓縮后下列指針會被壓縮至32位
- 每個Class的屬性指針(即靜態(tài)變量)
- 每個對象的屬性指針(即對象變量)
- 普通對象數(shù)組的每個元素指針
- 如果對象是數(shù)組,那么對象頭還需要額外的空間用于存儲數(shù)組的長度井联,使用+UseCompressedOops同樣會壓縮該區(qū)域從64位至32位
Monitor
synchronized鎖住的是對象卜壕,當使用synchronized鎖住對象obj時,實際上是該對象obj與一個Monitor對象產(chǎn)生了聯(lián)系
一個Monitor包括三部分:
- Owner:表示是哪個線程持有鎖
- entrySet:但有多個線程訪問鎖時烙常,如果發(fā)現(xiàn)Owner另有其人轴捎,該線程就會進入EntrySet
- waitSet:wait的時候線程會進入此
實際上當?shù)谝粋€線程執(zhí)行到synchronized語句時,首先去判斷obj對應(yīng)的Monitor的Owner是否為空,若為空轮蜕,則將自己線程ID賦值給Owner,表示自己占有了鎖昨悼,若Owner不為空,表示已經(jīng)有別的線程占有了鎖跃洛,則此線程就會進入entrtset隊列中率触,等待占有Owner的線程釋放鎖,也就是Owner會為空汇竭,這時候entrySet的線程會有調(diào)度器調(diào)度葱蝗,選出一個線程作為Owner
針對Obj與Monitor的關(guān)聯(lián),可以通過字節(jié)碼來查看细燎,首先這樣一個簡單的語句
static Object obj = new Object();
public static void main(String[] args) {
synchronized (obj){
}
}
通過javap 反編譯之后可以看出
public static void main(java.lang.String[]);
Code:
0: getstatic
3: dup
4: astore_1
5: monitorenter
6: aload_1
7: monitorexit
8: goto 16
11: astore_2
12: aload_1
13: monitorexit
14: aload_2
15: athrow
16: return
Exception table:
from to target type
6 8 11 any
11 14 11 any
逐一解釋一下這些命令行
- 首先取到靜態(tài)變量也就是Obj對象
- 復(fù)制一份
- 把復(fù)制的Obj對象存起來
- 進入Monitor
- 把第四步存起來的Obj備份取出來
- 出Monitor
- 跳到16語句也就是return
11到14的語句的意思是如果在臨界區(qū)發(fā)生異常两曼,這時候在異常里面同樣取到之前存起來的對象用于解鎖,這樣可以做到即使發(fā)生異常玻驻,也會有渠道釋放鎖
輕量級鎖與鎖膨脹
對于剛才Monitor的理論其實是在競態(tài)條件發(fā)生時使用synchronized的一種現(xiàn)象悼凑,如果不存在同時多個線程同時調(diào)用synchronized代碼塊,則首先會使用輕量級鎖
可以使用org.openjdk.jol.info.ClassLayout來打印對象的頭信息璧瞬,其實我們最重要的是要看MarkWord的信息,當然對于自己JVM是32位和64位也必須清楚户辫,因為不同的JVM的位數(shù)是不同的可以通過System.getProperty("sun.arch.data.model")來查看
首先對于出現(xiàn)的MarkWord給予必要的解釋
01 00 00 00 (00000001 00000000 00000000 00000000) (1) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
這是64位JVM上得出的Object的MarkWord,首先明確的是這是一個小端存儲,也就是說我們分析是的數(shù)據(jù)是
00 00 00 00 00 00 00 01
根據(jù)上面對象頭MardWord 64位的結(jié)構(gòu)分析嗤锉,最后三位是001也就是說這是一個沒有偏向鎖的正常狀態(tài)的對象頭渔欢,雖然它的前面都是0,但是也需要再次的說明的下瘟忱,它的前25位表示沒有用奥额,緊跟著32表示HashCode,然后又1位表示沒有用到,后面四位就是年齡
對于當只有一個線程訪問synchronized的時候是否是輕量級鎖我們使用下面這個代碼來驗證
public class Monitor {
static Object obj = new Object();
public static void main(String[] args) {
synchronized (obj){
log.debug(ClassLayout.parseInstance(obj).toPrintable());
}
}
}
這個代碼的意思就是在鎖住Obj的時候打印出obj的對象頭信息访诱,看看是否符合輕量級鎖垫挨,打印的結(jié)果是
f8 48 bb 0b (11111000 01001000 10111011 00001011)
00 70 00 00 (00000000 01110000 00000000 00000000)
可以看出f8對應(yīng)的最后兩位是00,根據(jù)對象頭的結(jié)構(gòu)分析,這個就是輕量級鎖触菜,前面的62位指向的是線程中棧幀的鎖記錄地址
鎖記錄(Lock Record)
鎖記錄對象粗略的包含兩個東西
- 鎖記錄的值
- 指向?qū)ο蟮闹羔?/li>
加鎖過程
當一個線程執(zhí)行synchronized(obj){}的時候棒拂,這時候會檢查obj對象頭的MardWord是否是正常狀態(tài)(01),如果是正常狀態(tài)會在棧幀中創(chuàng)建一個鎖記錄對象玫氢,其中指向?qū)ο蟮闹羔樦赶蛄薿bj,并會把鎖記錄對象的地址與obj的MarkWord通過CAS進行交換
- 如果CAS交換成功帚屉,鎖記錄的值存儲的是obj的MarkWord,對象頭中存儲的是鎖記錄對象的地址和狀態(tài)00漾峡,這個也就是我們在對象頭中的輕量級鎖的結(jié)構(gòu)顯示的攻旦,后兩位是00表示輕量級鎖,前面的62位表示的鎖記錄對象地址
- 如果CAS交換失敗生逸,這里面分為兩種情況
a. 鎖重入:如果說執(zhí)行了如下代碼
public class Monitor {
static Object obj = new Object();
public static void main(String[] args) {
synchronized (obj){
method1();
}
}
public static void method1(){
synchronized (obj){
}
}
}
當線程的棧幀的鎖記錄對象再一次與obj進行CAS交換時牢屋,這時候obj的MarkWord存儲的是同一個線程的上一個鎖記錄的地址且预,因此當前的鎖記錄的值設(shè)置為null,并且指向?qū)ο蟮闹羔樛瑯又赶騩bj,這就是鎖沖入,這時候鎖記錄充當?shù)氖侵厝氪螖?shù)的計數(shù)器
b. 鎖膨脹:當線程t1的鎖記錄欲與obj對象MarkWord進行CAS交換時烙无,發(fā)現(xiàn)obj的MarkWord的后兩位已經(jīng)變?yōu)?0了锋谐,這表示已經(jīng)有別的線程持有了obj的輕量級鎖,這個時候它就申請了Monitor鎖,也就是重量級鎖截酷,它把Obj的MarkWord置為指向Monitor的地址涮拗,后兩位置為10,然后自己進入到entrySet進行等待(此時的Monitor的Owner應(yīng)該是那個持有輕量級鎖的線程)
解鎖
持有輕量級鎖的對象執(zhí)行完synchronized的代碼時迂苛,這時候需要解鎖三热,這時候相當于把鎖記錄的值與obj的MarkWord進行CAS交換,如果成功表示解鎖成功三幻,如果失敗也是對標加鎖的流程有兩種情況
- 如果是鎖重入就漾,這時候鎖記錄的值為null,直接去掉鎖記錄即可
- 如果發(fā)現(xiàn)obj的MarkWord的后兩位變成了10,表示有別的線程觸發(fā)了鎖膨脹念搬,這時候通過MarkWord的重量級鎖地址找到Monitor抑堡,然后置Owner為空,然后通知entrySet的線程朗徊,這樣相當于觸發(fā)了重量級鎖的解鎖過程
自旋優(yōu)化
- 線程阻塞會涉及到上下文的切換首妖,這會影響到性能,因為對于重量級鎖可以通過自旋進行優(yōu)化
- 當線程t1申請Monitor時荣倾,發(fā)現(xiàn)已經(jīng)有別的線程持有了該重量級鎖,t1并不是立即進入到阻塞隊列中,而是通過自旋重試骑丸,當在重試一定次數(shù)內(nèi)舌仍,如果別的線程釋放了鎖,則t1就申請到了鎖通危,如果別的線程沒有釋放鎖铸豁,則表示自旋失敗,進入阻塞
- 這種自旋在一定程度上對鎖進行了優(yōu)化菊碟,但是這要求的基礎(chǔ)必須是多核CPU节芥,對于單核CPU沒有意思
- 自旋優(yōu)化從1.7以后是默認的,是JVM層面的優(yōu)化
偏向鎖
- 對于大部分情況只有一個線程執(zhí)行synchronized代碼的情況逆害,如果對于鎖重入的情況头镊,需要多次進行CAS,這其實也是會影響性能,因此從1.6之后魄幕,引入了偏向鎖的優(yōu)化相艇,也就是說如果一個線程執(zhí)行到了synchronized(obj)的情況,它會把線程id設(shè)置到obj的MarkWord上纯陨,這也是我們在上面對象頭中分析的那樣坛芽,這些下一次該線程發(fā)現(xiàn)是本線程id留储,就不會再進行CAS了,這樣的優(yōu)化對于特種情況的性能有提升
- 偏向鎖的后兩位與對象的正常狀態(tài)一樣都是01,但是倒數(shù)第三位是1咙轩,一個對象默認是開啟偏向鎖的获讳,只不過是在啟動的時候會延遲,因此為了驗證偏向鎖可以在對象創(chuàng)建前Sleep幾秒活喊,這樣就能很清晰的看到了
- 也可以通過-XX:BiasedLockingStartupDelay=0 關(guān)閉偏向鎖延遲丐膝,這樣創(chuàng)建出來的對象都具有偏向鎖特征
- 可以通過-XX:-UseBiasedLocking關(guān)閉偏向鎖,不管在延遲之前創(chuàng)建的對象還是關(guān)閉偏向鎖創(chuàng)建的對象胧弛,都是正常狀態(tài)的,當執(zhí)行synchronized的時候尤误,獲取的是輕量級鎖
- 如果創(chuàng)建了對象,調(diào)用了對象的hashCode方法结缚,這個時候會消除偏向鎖,這是因為對于對于偏向鎖來說沒有多余的空間存儲hashCode
- 具有偏向鎖特征的對象剛創(chuàng)建出來的時候MarkWord的后三位是101,前面都是0,當執(zhí)行完synchroized的時候這時候前面的0會被補上線程id,執(zhí)行完之后釋放鎖损晤,這個線程Id也還是在的,這就是偏向鎖的特性红竭,下次線程訪問的時候不再進行CAS,但是不管在什么時候一旦調(diào)用了obj.hashCode()方法尤勋,這個偏向鎖就會立刻被消除了,變成了正常狀態(tài)的對象頭茵宪,MarkWord的后三位也就變成了001
偏向鎖撤銷,批量重偏向和批量撤銷
- 對于偏向鎖最冰,如果有兩個線程不同時段的訪問synchronized(obj)的時候,這個時候偏向鎖會升級到輕量級鎖也就是00稀火,這種情況叫做偏向鎖撤銷
- 但是如果兩個線程同時訪問的synchronized(obj)的話暖哨,這時候就有了競爭,這時候偏向鎖就升級為重量級鎖
- 對于偏向鎖撤銷最形象的例子就是凰狞,首先t1先訪問synchronized(obj)篇裁,這時候obj偏向的是線程t1,當t1執(zhí)行完之后赡若,注意這個一定是執(zhí)行完达布,否則就是重量級鎖了,當t1執(zhí)行完之后,t2執(zhí)行synchronized(obj)逾冬,這個時候obj的狀態(tài)就由偏向鎖升級為輕量級鎖了
- 如果一個類的多個對象偏向了線程t1,這時候另一個線程t2在線程t1執(zhí)行完畢之后(此處一定要注意兩個線程是錯開時間執(zhí)行的)訪問了該類的對象的鎖黍聂,這時候就會出現(xiàn)偏向鎖取消,當取消到一定的閾值(20)的時候,這時候JVM會覺得偏向線程t1可能是錯的身腻,這時候剩下對象就會批量的偏向于t2,這個就是批量重偏向产还,如果要驗證這個問題畔裕,最好注意幾點
- 兩個線程一定要錯開時間
- 某個類比如Object的對象已經(jīng)要多于20(比如創(chuàng)建40個對象)捶闸,最好用ArrayList管理
- 可以看到從20到39的對象在線程t2中并不是被偏向鎖取消了百新,而是批量的都重偏向t2了
- 重新創(chuàng)建的對象并不受影響互墓,還是偏向于訪問它的線程
- 如果一個類的多個對象的偏向鎖被取消多次退唠,超過閾值(40),這時候JVM就會覺得競爭激烈不應(yīng)該有偏向鎖丰涉,這個時候就會取消批量撤銷偏向鎖,就連新創(chuàng)建的對象也不具備偏向鎖褂乍,直接是正常狀態(tài)的帖渠,也就說后三位是001,驗證這個問題的代碼需要注意幾點
- 需要三個線程t1,t2,t3
- 需要的對象個數(shù)多于20個倔叼,這個的基本情況就是在t1線程中各個對象都偏向于t1汗唱,在t2線程中前面20個相當于給取消了,后面的都偏向于t2線程丈攒,這個過程就是批量重偏向哩罪,在t3線程中相當于又給20個偏向取消,這時候達到了批量撤銷的閾值巡验,這時候后面的全部都給撤銷了偏向鎖际插,重新創(chuàng)建的對象也是正常對象,不具有偏向鎖特征
鎖消除優(yōu)化
- 對于執(zhí)行synchronized的代碼塊不管是輕量級鎖显设,重量級鎖還是偏向鎖框弛,都會對性能有一定的影響,但是下面的代碼
static int x =0;
public void methed1(){
x++;
}
public void method2(){
Object obj = new Object();
synchronized (obj){
x++;
}
}
這兩個方法的一個有鎖捕捂,一個沒有鎖瑟枫,但是兩個方法的執(zhí)行時間所差無幾,這是因為代碼運行的時候有個JIT會對字節(jié)碼的熱點代碼進行及時編譯,它會再一次的進行優(yōu)化,因為在method2的鎖的對象是局部變量指攒,JIT就認為這不會有競爭慷妙,就會在優(yōu)化的時候進行鎖消除,因此看上去是和無鎖的狀態(tài)是一樣的
- 這種優(yōu)化默認是開啟的允悦,可以通過-XX:-EliminateLocks 關(guān)閉鎖消除膝擂,那么再去看上面的代碼就會發(fā)現(xiàn)性能上相差不少
并發(fā)度與活躍性
多把鎖
在上面的理論與示例中都是共享的同一把鎖,這樣并發(fā)度就降低了,就像是租房子隙弛,一個三室一廳的房子一次只租給一個人,可能實際上那個人的需求只是一個房間,因此可以把套房分開各個房間單獨租出去架馋,這樣各個房間是單獨的鎖,有效的利用了資源避免了浪費
如下代碼
private Object studyRoom = new Object();
private Object sleepRoom = new Object();
public void study(){
synchronized (studyRoom){
log.debug("學(xué)習(xí)");
try {
Thread.sleep(1000);
log.debug("學(xué)習(xí)完了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void sleep(){
synchronized (sleepRoom){
log.debug("睡一會");
try {
Thread.sleep(1000);
log.debug("睡醒了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
死鎖
如果如上的情況有多把鎖驶鹉,就有可能出現(xiàn)死鎖問題绩蜻,比如有兩個線程t1持有鎖A,準備請求鎖B
t2線程持有鎖B,準備請求鎖A,那么t1線程一直等待t2線程釋放鎖B铣墨,t2線程一直等待t1線程釋放鎖A,那么就會出現(xiàn)兩個線程一直無限期的等待下去室埋,這就造成了死鎖
活鎖
如果兩個線程互相改變對方的結(jié)束條件那么也會造成兩個線程結(jié)束不了,一直運行下去伊约,這種情況就是活鎖,如下代碼所示:
static int count =10;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while(count>=0){
count--;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("count = {}",count);
}
}
},"t1").start();
new Thread(new Runnable() {
@Override
public void run() {
while(count<=20){
count++;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("count = {}",count);
}
}
},"t2").start();
}
饑餓線程
表示在多個線程中,因為加鎖的邏輯問題導(dǎo)致的某些線程經(jīng)骋ο或者大部分情況不被調(diào)度,那么這個線程就是饑餓線程