synchronized 關鍵字
1. 前言
本節內容主要是對 synchronized 關鍵字的使用進行講解,具體內容點如下:
- 了解 synchronized 關鍵字的概念,從總體層面對 synchronized 關鍵字進行了解,是我們本節課程的基礎知識;
- 了解 synchronized 關鍵字的作用,知道 synchronized 關鍵字使用的意義,使我們學習本節內容的出發點;
- 掌握 synchronized 關鍵字的 3 中使用方式,使我們本節課程的核心內容,所有的內容講解都是圍繞這一知識點進行的;
- 了解 synchronized 關鍵字的內存語義,將 synchronized 關鍵字與 Java 的線程內存模型進行關聯,更好的了解 synchronized 關鍵字的作用及意義,為本節重點內容。
2. synchronized 關鍵字介紹
概念:synchronized 同步塊是 Java 提供的一種原子性內置鎖,Java 中的每個對象都可以把它當作一個同步鎖來使用,這些 Java 內置的使用者看不到的鎖被稱為內部鎖,也叫作監視器鎖。
線程的執行:代碼在進入 synchronized 代碼塊前會自動獲取內部鎖,這時候其他線程訪問該同步代碼塊時會被阻塞掛起。拿到內部鎖的線程會在正常退出同步代碼塊或者拋出異常后或者在同步塊內調用了該內置鎖資源的 wait 系列方法時釋放該內置鎖。
內置鎖:即排它鎖,也就是當一個線程獲取這個鎖后,其他線程必須等待該線程釋放鎖后才能獲取該鎖。
Tips:由于 Java 中的線程是與操作系統的原生線程一一對應的,所以當阻塞一個線程時,需要從用戶態切換到內核態執行阻塞操作,這是很耗時的操作,而 synchronized 的使用就會導致上下文切換。
后續章節我們會引入 Lock 接口和 ReadWriteLock 接口,能在一定場景下很好地避免 synchronized 關鍵字導致的上下文切換問題。
3. synchronized 關鍵字的作用
作用:在并發編程中存在線程安全問題,使用 synchronized 關鍵字能夠有效的避免多線程環境下的線程安全問題,線程安全問題主要考慮以下三點:
- 存在共享數據,共享數據是對多線程可見的,所有的線程都有權限對共享數據進行操作;
- 多線程共同操作共享數據。關鍵字 synchronized 可以保證在同一時刻,只有一個線程可以執行某個同步方法或者同步代碼塊,同時 synchronized 關鍵字可以保證一個線程變化的可見性;
- 多線程共同操作共享數據且涉及增刪改操作。如果只是查詢操作,是不需要使用 synchronized 關鍵字的,在涉及到增刪改操作時,為了保證數據的準確性,可以選擇使用 synchronized 關鍵字。
4. synchronized 的三種使用方式
Java 中每一個對象都可以作為鎖,這是 synchronized 實現同步的基礎。synchronized 的三種使用方式如下:
- 普通同步方法(實例方法):鎖是當前實例對象 ,進入同步代碼前要獲得當前實例的鎖;
- 靜態同步方法:鎖是當前類的 class 對象 ,進入同步代碼前要獲得當前類對象的鎖;
- 同步方法塊:鎖是括號里面的對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。
接下來會對這三種使用方式進行詳細的講解,也是本節課程的核心內容。
5. synchronized 作用于實例方法
為了更加深刻的體會 synchronized 作用于實例方法的使用,我們先來設計一個場景,并根據要求,通過代碼的實例進行實現。
場景設計:
- 創建兩個線程,分別設置線程名稱為 threadOne 和 threadTwo;
- 創建一個共享的 int 數據類型的 count,初始值為 0;
- 兩個線程同時對該共享數據進行增 1 操作,每次操作 count 的值增加 1;
- 對于 count 數值加 1 的操作,請創建一個單獨的 increase 方法進行實現;
- increase 方法中,先打印進入的線程名稱,然后進行 1000 毫秒的 sleep,每次加 1 操作后,打印操作的線程名稱和 count 的值;
- 運行程序,觀察打印結果。
結果預期:因為 increase 方法有兩個打印的語句,不會出現 threadOne 和 threadTwo 的交替打印,一個線程執行完 2 句打印之后,才能給另外一個線程執行。
實例:
public class DemoTest extends Thread {
//共享資源
static int count = 0;
/**
* synchronized 修飾實例方法
*/
public synchronized void increase() throws InterruptedException {
sleep(1000);
count++;
System.out.println(Thread.currentThread().getName() + ": " + count);
}
@Override
public void run() {
try {
increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
DemoTest test = new DemoTest();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
t1.setName("threadOne");
t2.setName("threadTwo");
t1. start();
t2. start();
}
結果驗證:
threadTwo 獲取到鎖,其他線程在我執行完畢之前,不可進入。
threadTwo: 1
threadOne 獲取到鎖,其他線程在我執行完畢之前,不可進入。
threadOne: 2
從結果可以看出,threadTwo 進入該方法后,休眠了 1000 毫秒,此時線程 threadOne 依然沒有辦法進入,因為 threadTwo 已經獲取了鎖,threadOne 只能等待 threadTwo 執行完畢后才可進入執行,這就是 synchronized 修飾實例方法的使用。
Tips:仔細看 DemoTest test = new DemoTest () 這就話,我們創建了一個 DemoTest 的實例對象,對于修飾普通方法,synchronized 關鍵字的鎖即為 test 這個實例對象。
6. synchronized 作用于靜態方法
Tips:對于 synchronized 作用于靜態方法,鎖為當前的 class,要明白與修飾普通方法的區別,普通方法的鎖為創建的實例對象。為了更好地理解,我們對第 5 點講解的代碼進行微調,然后觀察打印結果。
代碼修改:其他代碼不變,只修改如下部分代碼。
- 新增創建一個實例對象 testNew ;
- 將線程 2 設置為 testNew 。
public static void main(String[] args) throws InterruptedException {
DemoTest test = new DemoTest();
DemoTest testNew = new DemoTest();
Thread t1 = new Thread(test);
Thread t2 = new Thread(testNew);
t1.setName("threadOne");
t2.setName("threadTwo");
t1. start();
t2. start();
}
結果驗證:
threadTwo 獲取到鎖,其他線程在我執行完畢之前,不可進入。
threadOne 獲取到鎖,其他線程在我執行完畢之前,不可進入。
threadTwo: 1
threadOne: 2
結果分析:我們發現 threadTwo 和 threadOne 同時進入了該方法,為什么會出現這種問題呢?
因為我們此次的修改是新增了 testNew 這個實例對象,也就是說,threadTwo 的鎖是 testNew ,threadOne 的鎖是 test。
兩個線程持有兩個不同的鎖,不會產生互相 block。相信講到這里,同學對實例對象鎖的作用也了解了,那么我們再次將 increase 方法進行修改,將其修改成靜態方法,然后輸出結果。
代碼修改:
public static synchronized void increase() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "獲取到鎖,其他線程在我執行完畢之前,不可進入。" );
sleep(1000);
count++;
System.out.println(Thread.currentThread().getName() + ": " + count);
}
結果驗證:
threadOne獲取到鎖,其他線程在我執行完畢之前,不可進入。
threadOne: 1
threadTwo獲取到鎖,其他線程在我執行完畢之前,不可進入。
threadTwo: 2
結果分析:我們看到,結果又恢復了正常,為什么會這樣?
關鍵的原因在于,synchronized 修飾靜態方法,鎖為當前 class,即 DemoTest.class。
public class DemoTest extends Thread {}
無論 threadOne 和 threadTwo 如何進行 new 實例對象的創建,也不會改變鎖是 DemoTest.class 的這一事實。
7. synchronized 作用于同步代碼塊
Tips:對于 synchronized 作用于同步代碼,鎖為任何我們創建的對象,只要是個對象即可,如 new Object () 可以作為鎖,new String () 也可作為鎖,當然如果傳入 this,那么此時代表當前對象。
我們將代碼恢復到第 5 點的知識,然后在第 5 點知識的基礎上,再次對代碼進行如下修改:
代碼修改:
/**
* synchronized 修飾實例方法
*/
static final Object objectLock = new Object(); //創建一個對象鎖
public static void increase() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "獲取到鎖,其他線程在我執行完畢之前,不可進入。" );
synchronized (objectLock) {
sleep(1000);
count++;
System.out.println(Thread.currentThread().getName() + ": " + count);
}
}
代碼解析:我們創建了一個 objectLock 作為對象鎖,除了第一句打印語句,讓后三句代碼加入了 synchronized 同步代碼塊,當 threadOne 進入時,threadTwo 不可進入后三句代碼的執行。
結果驗證:
threadOne 獲取到鎖,其他線程在我執行完畢之前,不可進入。
threadTwo 獲取到鎖,其他線程在我執行完畢之前,不可進入。
threadOne: 1
threadTwo: 2
8. 小結
本節內容的核心即 synchronized 關鍵字的 3 種使用方式,這是必須要掌握的問題。除此之外,不同的使用方法獲取到的鎖的類型是不一樣的,這是本節內容的重點,也是必須要掌握的知識。
對 synchronized 關鍵字的熟練使用,是并發編程中的一項重要技能。