synchronized學習

funnyok發表於2021-09-09

 現代軟體開發中併發已經成為一項基礎能力,而Java精心設計的高效併發機制,正是構建大規模應用的基礎之一。本文中我們將學習synchronized關鍵字的基本用法。

  synchronized是Java內建的同步機制,也稱為Intrinsic Locking,它提供了互斥的語義和可見性,當任務要執行被synchronized關鍵字保護的程式碼片段的時候,它將檢查鎖是否可用,然後獲取鎖,執行程式碼,釋放鎖;同時,其他試圖獲取鎖的執行緒只能等待或者阻塞在那裡。

 

synchronized用法

  synchronized可以加在普通方法前、程式碼塊上、靜態方法前、類上,加在不同的地方鎖是不一樣的,如下:

  • 加在普通方法上,鎖是當前例項物件;

private synchronized void f(){
    // doSomething
}

   注意:synchronized關鍵字是不能繼承的,也就是說,基類的方法 synchronized fun(){} 在繼承類中並不自動是 synchronized fun(){} ,而是變成了 fun(){} 。繼承時,需要顯式的指定它的某個方法為 synchronized 方法。

 

  • 加在靜態方法和類上,鎖是當前類的class物件;

圖片描述

public synchronized class F{
    // doSomething
}public class E{    public static synchronized void f(){
        // doSomething
    }
}

圖片描述

 

  • 同步方法塊,鎖是括號裡面的物件,可以是普通物件,也可以是class物件;

圖片描述

public class F{    public void f(){
        synchronized(this){            // doSomething
        }
    }    public void e(){
        synchronized(Object.class){            // doSomething
        }
    }
}

圖片描述

 

  我們先看一個簡單的示例:

圖片描述

public class SynLockTest {
    
    private static int index = 0;    
    public static void runTest() {
        Thread t1 = new Thread(new Runnable() {            public void run() {                for(int i=0 ; i<1000 ; i++) {
                    synchronized(SynLockTest.class) {                        index++;
                    }
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {            public void run() {                for(int i=0 ; i<1000 ; i++) {
                    synchronized(SynLockTest.class) {                        index++;
                    }                    
                }
            }
        });
        t1.start();
        t2.start();
    }    
    public static void main(String[] args) throws Exception{
        runTest();
        Thread.sleep(2000);
        System.out.println(index);
    }
}

圖片描述

  如上程式碼中,主執行緒會啟動兩個子執行緒(t1、t2),每個執行緒的任務是一樣的,都是對共享變數index自增1000次,接著主執行緒休眠2s,再輸出index的值,程式碼中對自增操作進行了同步(synchronized程式碼塊包圍),同步鎖是SynLockTest這個類的class物件,最終程式輸出結果將是2000,如果這裡不進行同步或者將同步程式碼塊中的鎖改為this,輸出結果大多數情況下應該是小於2000的,這是為什麼呢?

 

  首先,Java中的自增操作並不是一次完成的,虛擬機器在執行的時候首先要讀取index的值,然後將index的值加1,最後將index的值更新,這三步是分開進行的,如果執行緒t1讀取了index的值,這時候執行緒t1的時間片用完了,被掛起,t2開始執行。。。吭哧吭哧一堆自增,結束之後,t1繼續執行,這時t1進行一次自增之後會更新index的值,注意,這裡更新的是t1之前所持有的index的值,相當於把t2剛才所做的操作全部覆蓋了,相當於t2白做了,所以最終輸出結果小於2000,因為部分自增的結果被覆蓋了。

  再說把鎖換成this之後,這時雖然自增操作雖然被同步塊保護了,但是這裡獲取的鎖是匿名類這個物件(Runnable)的鎖,而t1和t2中的這個匿名類是不一樣的(都是new出來的),所以並沒有互斥效果,也就相當於和沒有加鎖一個效果。

 

synchronized作用

  synchronized的作用是透過互斥來實現執行緒安全,關於執行緒安全,需要保證幾個基本特性,本文簡單介紹一下(詳細可以參考Java記憶體模型一文):

 

  • 原子性,簡單說就是相關操作不會中途被其他執行緒干擾,一般透過同步機制實現。

  • 可見性,是一個執行緒修改了某個共享變數,其狀態能夠立即被其他執行緒知曉,通常被解釋為將執行緒本地狀態反映到主記憶體上,volatile就是負責保證可見性的。

  • 有序性,是保證執行緒內序列語義,避免指令重排等。

 

  關於原子性,可參考如上例子,在自增操作上加上同步控制,保證同一時刻只能有一個執行緒執行自增操作,並且執行的過程不會被其他執行緒打斷。

  關於可見性,執行緒在獲取到鎖時,JVM會把該執行緒對應的本地記憶體置為無效,並且會從主記憶體中讀取共享變數。執行緒釋放鎖時,JVM會把該執行緒對應的本地記憶體中的共享變數立即重新整理到主記憶體中。透過這種方式來保證變數的可見性。

  關於有序性,被同步的程式碼,同一時刻只能有一個執行緒會執行,而Java本身是能保證這一點的(執行緒內表現為序列的語義,Within-Thread As-If-Serial Sematics),所以說在這個層面上synchronized是能實現有序性的。

   這一部分關於synchronized如何實現原子性、可見性、有序性只是簡單介紹,後面會從底層實現來詳細總結synchronized是如何實現這些功能的。

 

總結

  1. synchronized的基本用法,修飾程式碼塊,以及分別加什麼鎖;

  2. synchronized的作用,可以保證原子性、可見性和有序性的;

  綜上,synchronized是萬精油,使用起來很方便,直接在要保護的程式碼塊上加上synchronized修飾即可,可讀性很高。雖然早期synchronized的效能問題多為人詬病,但是現代JDK對synchronized進行了很大最佳化,在通用場景下,我們無需過多關注這點。因此,一般以synchronized關鍵字入手,只有在效能調優時才考慮替換為Lock物件或採用原子類。

  本文只是簡單總結了synchronized的用法及作用,並未涉及其底層原理,這部分內容會在後面撰文詳述。

原文出處:https://www.cnblogs.com/volcano-liu/p/10131149.html  

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3137/viewspace-2819090/,如需轉載,請註明出處,否則將追究法律責任。

相關文章