前言
經過前面的兩篇文章的介紹,可以清楚知道,synchronized可以用於修飾一個方法 或者 代碼塊,線程要訪問這些臨界區代碼,則要先獲取對應的 對象監視器 ,從而使多個線程互斥訪問臨界區。
然而,區別是不是同一個對象監視器,是根據對象監視器的內存地址是否一樣。這就意味着,想要某些線程在同一個 對象監視器 上競爭臨界區代碼,那么就必須保證他們獲取的對象監視器是同一個。
如果使用以下的幾個類作為對象監視器,那么你要注意了,你的對象監視器可能在你沒察覺的情況下改變了,出現了意想不到的結果。
一、幾個需要注意的類:
1、String 類
大家都知道,為了節省內存,JVM中為String類維持了一個常量池,一旦String的對象值改變時,就會替換成新的對象實例,這個不多說。
2、五個基本類型的自動裝箱的情況:
自動裝箱(autoboxing):把一個基本數據類型直接賦給對應的包裝類變量, 或者賦給 Object 變量;
自動拆箱:把包裝類對象直接賦給一個對應的基本類型變量;
同樣,JVM 為了節省內存,當滿足以下情況,包裝類所對應的基本類型的值相同,將使用同一個對象
,如果包裝類對應的基本類型的值不相同,則是不同的對象:
1) 是通過裝箱創建的對象,而非構造方法創建的;
2)在int 及 int 以下的 基本類型 ,且 其值實際存儲空間 不能超過 一個字節。這樣的基本類型所對應的包裝類型;
換句話說:就是Byte 、Boolean、Char(0~127范圍的字符)、Short (基本類型值:-128127)、Integer(基本類型值:-128127) 五種有限制的包裝類型;
注意:
- String 常量池的由編譯時字面常量生成的,如果想直接創建一個不是維護在常量池的新對象,使用構造方法new String()便可!
- 當Short、Integer類的對象所對應的基本類型的值 超過一個字節范圍,如 129,那么 無論是裝箱,還是用構造方法,將會是獨立的新對象,不是維護在常量池中。
Integer a = 2;
Integer b = 2;
//a 與 b 都是經過自動裝箱的機制得到的,所對應的基本類型值為2,值相等,所以都是指向同一個對象
if(a==b){
System.out.println("a與b的內存地址相等,是同一個對象");
}
//c 是通過構造方法得到的,所以是直接創建一個對象,而不是使用常量池中的值
Integer c = new Integer(2);
if(a != c){
System.out.println("a與c的內存地址不想等,不是同一個對象");
}
二、異常情況舉例
欸!說了這么多,該進入正題,如果使用了以上所說的類作為對象監視器,可能出現哪些意外的情況呢?下面列舉兩種常見情況:
例一:導致在不是對象監視器上調用 wait、notify方法
Integer in = 3;
synchronized (in) {
//自動裝箱得到新的值,即in指向了新的對象了,不是指向對象監視器
in += 3;
try {
//in 已經是新的對象,而調用wait()方法前,必須獲取對象監視器,否則拋出異常
in.wait(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
運行結果:
例二:預期協作的幾個線程不是使用同一個對象監視器,導致協作失敗
public static void main(String[] args) {
Integer b = 2;
Thread thread_1 = new Thread("thread_1"){
@Override
public void run() {
synchronized (b) {
if(b<10){
try {
b.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("線程thread_1運行結束,b的值是:"+b);
}
}
};
thread_1.start();
Thread thread_2 = new Thread(new MyRunable(b),"thread_1");
thread_2.start();
}
class MyRunable implements Runnable{
Integer b;
public MyRunable(Integer b){
//再次自動裝箱來修該值,那么b就指向新的對象
this.b = b+10;
}
@Override
public void run() {
//線程1、2的對象監視器已經不一樣了,所以,線程2將無法按照預期喚醒線程1
synchronized (b) {
b.notify();
System.out.println("線程thread_2運行結束!");
}
}
}
運行結果:
線程2正常結束,線程1無法被喚醒
總結
- 這些類比較特殊,需要謹慎使用的原因,就是因為JVM為它們維護了一個常量池,在你以為是改變值的時候,其實已經換了一個新的對象,導致對象監視器前后不一致;
- 安全使用這些類的方法,便是設為常量,用 final 修飾,保證一直都是同一個對象監視器。