天天看點

Java并發程式設計:深入了解synchronized關鍵字

目錄

1:synchronized簡介

2:synchronized使用方式

3:synchronized可重入性

4:synchronized可見性

5:synchronized實作原理

6:synchronized的缺陷

7:synchronized的使用注意

1:synchronized簡介

在多線程環境中對同一資源同時操作可能會導緻結果的不确定性。java内置了synchronized關鍵字來解決這種問題。synchronized通常也被稱為重量級鎖,盡管從JDK1.5新增了Lock,但synchronized憑借java自帶的高封裝性依舊被廣泛的使用。synchronized可以把任何一個非null的對象作為鎖。synchronized有兩種主要用法:第一種是對象鎖,包括方法鎖(預設鎖對象為this目前的執行個體對象)和同步代碼塊鎖(自己指定的鎖對象)。第二種是類鎖,指synchronized修飾靜态的方法或指定鎖為Class對象。

2:synchronized使用方式

首先,我先寫一段代碼

public class SynchronizedMethod1 implements Runnable {

    static SynchronizedMethod1 synchronizedMethod = new SynchronizedMethod1();

    static int i = 0;

    @Override

    public void run() {

        for (int j = 0; j < 100000; j++) {

            i++;

        }

    }

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(synchronizedMethod);

        Thread t2 = new Thread(synchronizedMethod);

        t1.start();

        t2.start();

        t1.join();

        t2.join();

        System.out.println("最終的結果是:" + i);

    }

}

這段代碼大家應該可以看出來是有問題的,沒有保證對共享變量i的原子操作,導緻的結果就是:無法确定i輸出的值。

下面來示範synchronized的幾種用法,如何保證資料的準确性。

方式一: 同步代碼塊

public class SynchronizedMethod2 implements Runnable {

    static SynchronizedMethod2 instance = new SynchronizedMethod2();

    static int i = 0;

    @Override

    public void run() {

         synchronized (this){

            for (int j = 0; j < 100000; j++) {

                i++;

            }

        }

    }

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(instance);

        Thread t2 = new Thread(instance);

        t1.start();

        t2.start();

        t1.join();

        t2.join();

        System.out.println("最終的結果是:" + i);

    }

}

this指的是目前的執行個體對象。

方式二: 同步方法

public class SynchronizedMethod3 implements Runnable {

    static SynchronizedMethod3 instance = new SynchronizedMethod3();

    static int i = 0;

    @Override

    public void run() {

        add();

    }

    public synchronized void add() {

        for (int j = 0; j < 100000; j++) {

            i++;

        }

    }

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(instance);

        Thread t2 = new Thread(instance);

        t1.start();

        t2.start();

        t1.join();

        t2.join();

        System.out.println("最終的結果是:" + i);

    }

}

第二種方式也能夠保證資料的準确。

下面我再對上述代碼進行改動。執行個體化兩個對象,分别給線程使用。這時會發現,上述的兩種方式都盡管都使用了synchronized,但依然無法保證資料的準确。原因是因為我們使用的是synchronized的對象鎖,對于不同的執行個體對象,線程隻能夠對自己引用的對象進行加鎖。

public class SynchronizedMethod3 implements Runnable {

    static SynchronizedMethod3 instance1 = new SynchronizedMethod3();

    static SynchronizedMethod3 instance2 = new SynchronizedMethod3();

    static int i = 0;

    @Override

    public void run() {

        add();

    }

    public synchronized void add() {

        for (int j = 0; j < 100000; j++) {

            i++;

        }

    }

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(instance1);

        Thread t2 = new Thread(instance2);

        t1.start();

        t2.start();

        t1.join();

        t2.join();

        System.out.println("最終的結果是:" + i);

    }

}

示範下面用法的時候先講一個概念:Java類可能有很多個對象,但隻有1個Class對象。這個很重要,要考的!!!比如我new一個Student對象A,一個Student對象B,這是兩個對象,但它們隻有一個Class對象,這個Class對象由JVM實作。

方式三: 靜态方法鎖

與第二種方式相比就是在同步方法上多加了static

public class SynchronizedMethod4 implements Runnable{

    static SynchronizedMethod4 instance1 = new SynchronizedMethod4();

    static SynchronizedMethod4 instance2 = new SynchronizedMethod4();

    static int i = 0;

    @Override

    public void run() {

        try {

            add();

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

    }

    public static synchronized void add() throws InterruptedException {

        System.out.println(Thread.currentThread().getName());

        for (int j = 0; j < 100000; j++) {

            i++;

        }

        Thread.sleep(2000);

    }

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(instance1);

        Thread t2 = new Thread(instance2);

        t1.start();

        t2.start();

        t1.join();

        t2.join();

        System.out.println("最終的結果是:" + i);

    }

當作用在靜态方法時鎖住的便是對象對應的Class執行個體,因為Class資料存在于永久帶,是以靜态方法鎖相當于該類的一個全局鎖。是以就算new了多個對象,但一樣能夠鎖住。

方式四: synchronized鎖為Class對象

public class SynchronizedMethod5 implements Runnable {

    static SynchronizedMethod5 instance1 = new SynchronizedMethod5();

    static SynchronizedMethod5 instance2 = new SynchronizedMethod5();

    static int i = 0;

    @Override

    public void run() {

        synchronized (SynchronizedMethod5.class) {

            for (int j = 0; j < 100000; j++) {

                i++;

            }

        }

    }

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(instance1);

        Thread t2 = new Thread(instance2);

        t1.start();

        t2.start();

        t1.join();

        t2.join();

        System.out.println("最終的結果是:" + i);

    }

}

在方法四中,我們的鎖對象是*.class,無論執行個體化多少對象,這些對象也隻有一個class對象,是以,這種方式也是可以鎖住的。

3:synchronized可重入性

synchronized和Lock(ReentrantLock)都是可重入鎖,可重入鎖又叫做遞歸鎖。在這裡隻說明synchronized的可重入性,不對二者進行比較。

可重入性定義:同一線程在外層函數擷取到鎖後,内層函數可以直接再次擷取到鎖。

synchronized的可重入性有兩點好處:避免死鎖,提升封裝性。

這是什麼意思呢?我們用如下代碼來說明:

public class SynchronizedMethod6 {

    static SynchronizedMethod6 instance = new SynchronizedMethod6();

    public void method1() {

        synchronized (SynchronizedMethod6.class) {

            System.out.println("method1執行成功");

            method2();

        }

    }

    public void method2() {

        synchronized (SynchronizedMethod6.class) {

            System.out.println("method2執行成功");

        }

    }

    public static void main(String[] args) {

        instance.method1();

    }

}

我們假設synchronized不具有重入性,在調用method2時,由于目前持有的鎖沒有釋放,又無法擷取到method2中的鎖,這時就會導緻死鎖。

由于synchronized是java内置的鎖,自帶重入性,是以封裝性更強。

原理:加鎖次數計數器。

每個對象都有一把鎖,JVM負責跟蹤對象被加鎖的次數。線程第一次擷取對象鎖的時候,計數變為1.當這個線程在此對象上再次獲得鎖的時候,計數會遞增。當任務離開的時候,計數遞減,當計數遞減為0的時候,鎖完全被釋放。

4:synchronized可見性

既然說到線程之間的可見性,那麼必須要先了解java的記憶體模型JMM(這篇部落格中簡單簡單講解了一下java的記憶體模型:https://blog.csdn.net/love1793912554/article/details/88618453)。在這裡不再詳細講述JMM。對于synchronized來說,在釋放鎖的時候,線程會把操作的資料重新整理到主記憶體中去,由此就可以保證線程之間的資料可見性。

Java并發程式設計:深入了解synchronized關鍵字

5:synchronized實作原理

synchronized是java内置的關鍵字,由此可見它的重要性。但它無法直接通過源碼來分析。下面我用反編譯位元組碼資訊來分析一下synchronized的實作原理。

先用javac将方法一中的.java檔案編譯成.class檔案,執行javap -v SynchronizedMethod1.class,javap是jdk自帶的工具,想要仔細了解可以參考(https://blog.csdn.net/junsure2012/article/details/7099222)。

Java并發程式設計:深入了解synchronized關鍵字

執行同步代碼塊需要先擷取對象的螢幕monitor。monitorenter對應多個monitorexit,釋放鎖的情況可能有多種,正常釋放鎖,異常釋放鎖,是以monitorexit相比較于monitorenter會多。

6:synchronized的缺陷

鎖的釋放情況少。synchronized釋放鎖隻有兩種情況,一種是正常流程運作結束,另一種是發生了異常。如果說一個線程正在執行IO操作(當然,不建議在鎖中進行耗時的IO操作,隻是舉例),那麼另一個線程就隻能焦急的等待。

試圖獲得鎖時不能設定逾時時間。排隊擷取鎖的線程會一直存在,不會因為暫時的堵塞而撤退。與之對應的tryLock(long time, TimeUnit unit) 在指定的時間内不能獲得鎖就會主動放棄。

不能中斷一個正在試圖獲得鎖的線程。

不夠靈活:每個鎖僅有單一的條件,加鎖和釋放鎖的時機單一。

無法知道是否成功擷取到鎖。

7:synchronized的使用注意

1:鎖的對象不能為空,鎖是存在于對象頭中的,對象都沒有,如何加鎖。

2:鎖的作用域不宜過大,簡單點說就是加鎖的代碼塊不能過多,如果代碼塊都存在與鎖中,代 碼的執行就變成了串行執行。這就無法展現多線程的效率了。

3:避免死鎖。在一個線程中避免擷取不同的鎖。

記錄自己的學習和成長,如果寫的有什麼不對的地方,還請大家多多指正。