Java中synchronized關鍵字作用及用法

概念

在上篇文章介紹Volatile關鍵字的時候提到,synchronized 可以保障原子性和可見性。因爲 synchronized 無論是同步的方法還是同步的代碼塊,都會先把主內存的數據拷貝到工作內存中,同步代碼塊結束,會把工作內存中的數據更新到主內存中,這樣主內存中的數據一定是最新的。更重要的是禁用了亂序重組以及保證了值對存儲器的寫入,這樣就可以保證可見性。

背景

現在可以多個線程對同一片存儲空間進行訪問,這時存儲空間裏面的數據叫做共享數據。線程併發給我們帶來效率的同時,也帶了一些數據安全性的問題,數據安全性是一個很嚴重的問題,多個線程同時訪問同一片數據區,很有可能把裏面的數據弄的混亂。 所以Java語言提供了專門機制以解決這種數據安全性問題,有效避免了同一個數據對象被多個線程同時訪問,從而導致數據的錯亂的問題。

synchronized關鍵字用法

  • synchronized關鍵字可以作爲函數的修飾符(也就是常說的同步方法)
  • synchronized關鍵字可以作爲函數內的語句(也就是常說的同步代碼塊)

示例
同步方法的寫法

public synchronized void test(){}

同步代碼塊的寫法

public void test(){
    synchronized(this){
        System.out.println("Test");
    }
}

代碼synchronized(this)中的this的含義會在後面詳解。

synchronized關鍵字的作用域

  • 對象實例: 可以防止多個線程同時訪問這個對象的synchronized方法,如果一個對象有多個synchronized方法,只要一個線程訪問了其中的一個synchronized方法,其它線程就不能同時訪問這個對象中任何一個synchronized方法。這時,不同的對象實例的
    synchronized方法是不相干擾的。也就是說,其它線程照樣可以同時訪問相同類的另一個對象實例中的synchronized方法。
  • 類: 可以防止多個線程同時訪問這個類所創建的對象中的synchronized方法。它可以對這個類創建的所有對象實例起作用。

synchronized關鍵字用法及含義

synchronized 方法

它的作用域默認是當前對象,這時鎖就是對象,誰拿到這個鎖誰就可以運行它所控制的那段代碼。如果這個對象有多個synchronized方法,其它線程就不能同時訪問這個對象中任何一個synchronized方法。
示例

public class SynchronizedTest {

    /**
     * 同步方法1
     */
    public synchronized void printA(){
        System.out.println("AAAAAAAAAAAAAAAAA");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    /**
     * 同步方法2
     */
    public synchronized void printB(){
        System.out.println("BBBBBBBBBBBBBBBBB");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        // 創建一個對象實例
        SynchronizedTest synchronizedTest = new SynchronizedTest();

        /**
         * 線程1,執行該實例的printA方法
         */
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronizedTest.printA();
            }
        }).start();

        /**
         * 線程2,執行該實例的printB方法
         */
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronizedTest.printB();
            }
        }).start();
    }
}

這個示例代碼很簡單,在一個類裏面有兩個打印字符串的方法,然後在main函數裏面啓動兩個線程去分別調用這個兩個方法。

代碼執行後會出現兩種種打印情況
第一種:

AAAAAAAAAAAAAAAAA
(這裏會等待三秒)
BBBBBBBBBBBBBBBBB
(這裏會等待三秒,然後進程退出)

第二種:

BBBBBBBBBBBBBBBBB
(這裏會等待三秒)
AAAAAAAAAAAAAAAAA
(這裏會等待三秒,然後進程退出)

爲什麼會這樣呢?
因爲在main函數裏面的兩個線程都調用了start()方法後,並不是按照誰先調用start()方法,就先執行哪個線程,而是需要等待CPU的調度,那麼CPU先調度誰呢?這我也不知道,因爲CPU是隨機的。

如果CPU先調用了線程1,因爲printA()方法是synchronized修飾的,所以線程1在執行printA()方法前,先看看有沒有誰把synchronizedTest對象鎖住了。目前來看沒有鎖,那線程1就把該對象鎖起來,並執行printA()方法,然後睡眠3秒鐘,如果這時候,CPU又調度了線程2,那麼線程2去執行的synchronized修改的printB()方法前,看到synchronizedTest對象已經被鎖住了,拿不到鎖,於是就只能等到synchronizedTest對象鎖被釋放後才能執行printB()方法了。

再假設,此時等待synchronizedTest對象鎖的線程有很多,有線程1、2…10,這麼多線程在等待,那麼synchronizedTest對象鎖被釋放後,下一次對象鎖會被誰拿到,也是要看CPU的心情了,不知道誰纔是那個天選之子呢…

如果CPU先調用了線程2,後面的等待流程是一樣的。

這個例子說明了synchronized關鍵字在對象實例的作用域,防止多個線程同時訪問這個對象的synchronized方法,如果一個對象有多個synchronized方法,只要一個線程訪問了其中的某一個synchronized方法,其它線程就不能同時訪問這個對象中其他任何一個synchronized方法了。

那在對象實例的作用域概念後面還有一句話“這時,不同的對象實例的 synchronized方法是不相干擾的。也就是說,其它線程照樣可以同時訪問相同類的另一個對象實例中的synchronized方法。
現在來驗證一下這句話,只需要修改上面代碼中main函數的兩句話,修改後如下:

public static void main(String[] args) {
	/**
	 * 線程1,執行該實例的printA方法
	 */
	new Thread(new Runnable() {
		@Override
		public void run() {
			// 創建一個對象實例
			SynchronizedTest synchronizedTest1 = new SynchronizedTest();
			synchronizedTest1.printA();
		}
	}).start();

	/**
	 * 線程2,執行該實例的printB方法
	 */
	new Thread(new Runnable() {
		@Override
		public void run() {
			// 創建一個對象實例
			SynchronizedTest synchronizedTest2 = new SynchronizedTest();
			synchronizedTest2.printB();
		}
	}).start();
}

修改的地方是把創建對象實例的地方,放在線程裏面去了,此時就有兩個不同的對象實例了,現在來看看執行結果呢。
也會有兩種打印情況
第一種:

AAAAAAAAAAAAAAAAA
BBBBBBBBBBBBBBBBB
(等待睡眠時間結束,退出進程)

第二種:

BBBBBBBBBBBBBBBBB
AAAAAAAAAAAAAAAAA
(等待睡眠時間結束,退出進程)

兩個線程並行發生,這就印證了上面這句話:不同的對象實例的 synchronized方法是不相干擾的,也就是說,其它線程照樣可以同時訪問相同類的另一個對象實例中的synchronized方法。

思考時間?

如果我們在示例1的基礎上,增加一個普通成員方法的打印方法:

/**
 * 普通成員方法1
 */
public  void printC(){
    System.out.println("CCCCCCCCCCCCCCCCC");
}

在main函數裏面增加一個線程去執行這個方法:

/**
* 線程3,執行該實例的printC方法
*/
new Thread(new Runnable() {
	@Override
	public void run() {
   		synchronizedTest.printC();
	}
}).start();

看看現在的程序執行結果會是什麼樣的呢?可能會有6中不同的打印哦,自己試試吧,想想爲什麼。

synchronized 代碼塊

鎖對象

synchronized關鍵字還可以用於方法中的某個代碼塊中,表示只對這個代碼塊裏的資源實行互斥訪問。
示例

public class SynchronizedObjTest {
    /**
     * 同步方法1
     */
    public  void printA(){
        synchronized (this){
            System.out.println("AAAAAAAAAAAAAAAAA");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    /**
     * 同步方法2
     */
    public void printB(){
        synchronized (this){
            System.out.println("BBBBBBBBBBBBBBBBB");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        SynchronizedObjTest synchronizedObjTest = new SynchronizedObjTest();
        /**
         * 線程1,執行該實例的printA方法
         */
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronizedObjTest.printA();
            }
        }).start();

        /**
         * 線程2,執行該實例的printB方法
         */
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronizedObjTest.printB();
            }
        }).start();
    }
}

這段代碼和示例1極爲相似,不同的地方在於鎖的寫法,synchronized (this),中的this代表着當前對象,那麼它的作用域就是當前對象,這時鎖就是對象,誰拿到這個鎖誰就可以運行它所控制的那段代碼。如果這個對象有多個synchronized方法,其它線程就不能同時訪問這個對象中任何一個synchronized方法。

那麼synchronized 代碼塊這種做法對於同一個類的不同的對象實例的 synchronized 代碼塊會不會相互干擾呢?答案是不會的,就像synchronized方法一樣。不同的對象實例是不同的鎖,也就不會相互干擾。

鎖class

可以防止多個線程同時訪問這個類所創建的對象中的synchronized方法。它可以對這個類創建的所有對象實例起作用。
鎖class只需要將上面代碼中的this,換成“類名.class”就行了
示例

public class SynchronizedClassTest {
    /**
     * 同步方法1
     */
    public  void printA(){
        synchronized (SynchronizedClassTest.class){
            System.out.println("AAAAAAAAAAAAAAAAA");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    /**
     * 同步方法2
     */
    public void printB(){
        synchronized (SynchronizedClassTest.class){
            System.out.println("BBBBBBBBBBBBBBBBB");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        SynchronizedClassTest synchronizedClassTest = new SynchronizedClassTest();
        /**
         * 線程1,執行該實例的printA方法
         */
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronizedClassTest.printA();
            }
        }).start();

        /**
         * 線程2,執行該實例的printB方法
         */
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronizedClassTest.printB();
            }
        }).start();
    }
}

程序會出現兩種種打印情況
第一種:

AAAAAAAAAAAAAAAAA
(這裏會等待三秒)
BBBBBBBBBBBBBBBBB
(這裏會等待三秒,然後進程退出)

第二種:

BBBBBBBBBBBBBBBBB
(這裏會等待三秒)
AAAAAAAAAAAAAAAAA
(這裏會等待三秒,然後進程退出)

現在將對象實例放在線程中去創建,使其生成兩個不同的對象實例

public static void main(String[] args) {
    /**
     * 線程1,執行該實例的printA方法
     */
    new Thread(new Runnable() {
        @Override
        public void run() {
            SynchronizedClassTest synchronizedClassTest1 = new SynchronizedClassTest();
            synchronizedClassTest1.printA();
        }
    }).start();

    /**
     * 線程2,執行該實例的printB方法
     */
    new Thread(new Runnable() {
        @Override
        public void run() {
            SynchronizedClassTest synchronizedClassTest2 = new SynchronizedClassTest();
            synchronizedClassTest2.printB();
        }
    }).start();
}

修改後程序會出現兩種種打印情況
第一種:

AAAAAAAAAAAAAAAAA
(這裏會等待三秒)
BBBBBBBBBBBBBBBBB
(這裏會等待三秒,然後進程退出)

第二種:

BBBBBBBBBBBBBBBBB
(這裏會等待三秒)
AAAAAAAAAAAAAAAAA
(這裏會等待三秒,然後進程退出)

修改前和修改後的打印情況是一致的。這就印證了這句話:synchronized鎖class可以防止多個線程同時訪問這個類所創建的對象中的synchronized方法。它可以對這個類創建的所有對象實例起作用。

在Java中還有一條隱式規則

  • 當修飾靜態方法的時候,鎖定的是當前類的Class對象。
  • 當修飾非靜態方法的時候,鎖定的是當前實例對象this。

技 術 無 他, 唯 有 熟 爾。
知 其 然, 也 知 其 所 以 然。
踏 實 一 些, 不 要 着 急, 你 想 要 的 歲 月 都 會 給 你。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章