內置鎖(三)synchronized的幾個要注意的對象監視器


前言

   經過前面的兩篇文章的介紹,可以清楚知道,synchronized可以用於修飾一個方法 或者 代碼塊,線程要訪問這些臨界區代碼,則要先獲取對應的 對象監視器 ,從而使多個線程互斥訪問臨界區。
   然而,區別是不是同一個對象監視器,是根據對象監視器的內存地址是否一樣。這就意味着,想要某些線程在同一個 對象監視器 上競爭臨界區代碼,那么就必須保證他們獲取的對象監視器是同一個。
   如果使用以下的幾個類作為對象監視器,那么你要注意了,你的對象監視器可能在你沒察覺的情況下改變了,出現了意想不到的結果。

一、幾個需要注意的類:

1、String 類

大家都知道,為了節省內存,JVM中為String類維持了一個常量池,一旦String的對象值改變時,就會替換成新的對象實例,這個不多說。

2、五個基本類型的自動裝箱的情況:

自動裝箱(autoboxing):把一個基本數據類型直接賦給對應的包裝類變量, 或者賦給 Object 變量;
自動拆箱:把包裝類對象直接賦給一個對應的基本類型變量;
同樣,JVM 為了節省內存,當滿足以下情況,包裝類所對應的基本類型的值相同,將使用同一個對象
,如果包裝類對應的基本類型的值不相同,則是不同的對象:
1) 是通過裝箱創建的對象,而非構造方法創建的;
2)在int 及 int 以下的 基本類型 ,且 其值實際存儲空間 不能超過 一個字節。這樣的基本類型所對應的包裝類型;
換句話說:就是Byte 、Boolean、Char(0~127范圍的字符)、Short (基本類型值:-128127)、Integer(基本類型值:-128127) 五種有限制的包裝類型;

注意:

  1. String 常量池的由編譯時字面常量生成的,如果想直接創建一個不是維護在常量池的新對象,使用構造方法new String()便可!
  2. 當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 修飾,保證一直都是同一個對象監視器。




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM