開發與維運

萬字長文帶你徹底理解synchronized關鍵字(上)

一、簡介


Synchronized一句話來解釋其作用就是:能夠保證同一時刻最多隻有一個線程執行該段代碼,以達到併發安全的效果。也就是說Synchronized就好比是一把鎖,某個線程把資源鎖住了之後,別人就不能使用了,只有當這個線程用完了別人才能用。

對於Synchronized關鍵字來說,它是併發編程中一個元老級角色,也就是說你只要學習併發編程,就必須要學習Synchronized關鍵字。由此可見其地位。

說了這麼多,好像我們還沒體驗過它的威力。我們就直接舉個例子,來分析一下。

public class SynTest01 implements Runnable{
    static int a=0;
    public static void main(String[] args) 
                throws InterruptedException {
        SynTest01 syn= new SynTest01();
        Thread thread1 = new Thread(syn);
        Thread thread2 = new Thread(syn);
        thread1.start();thread1.join();
        thread2.start();thread2.join();
        System.out.println(a);
    }
    @Override
    public void run() {
        for(int i=0;i<1000;i++) {
            a++;
        }
    }
}

上面代碼要完成的功能就是,thread1對a進行增加,一直到1000,thread2再對a進行增加,一直到2000。不過如果我們運行過之後我們就會發現,最後的輸出值總是小於2000,這是為什麼呢?

這是因為我們在執行a++的時候其實包含了以下三個操作:

(1)線程1讀取a

(2)線程1將a加1

(3)將a的值寫入內存

出錯原因的關鍵就在於第二操作和第三個操作之間,此時線程1還沒來得及把a的值寫入內存,線程2就把舊值讀走了,這也就造成了a加了兩次,但是內存中的a的值只增加了1。這也就是不同步現象。

但是如果說我們使用了Synchronized關鍵字之後呢?

public class SynTest01 implements Runnable{
    static int a=0;
    Object object = new Object();
    public static void main(String[] args) throws InterruptedException {
        SynTest01 syn= new SynTest01();
        Thread thread1 = new Thread(syn);
        Thread thread2 = new Thread(syn);
        thread1.start();thread1.join();
        thread2.start();thread2.join();
        System.out.println(a);
    }
    @Override
    public void run() {
        synchronized (object) {
            for(int i=0;i<1000;i++) {
                a++;
            }
        }//結束
    }
}

現在我們使用synchronized關鍵字把這一塊代碼鎖住,不管你怎麼輸出都是2000了,鎖住之後,同一時刻只有一個線程進入。也就不會發生上面a寫操作不同步的現象了。

現在相信你開始覺得synchronized關鍵字的確很實用,可以解決多線程中的很多問題。上面這個小例子只是帶我們去簡單的認識一下,下面我們就來看看其詳細的使用。

二、使用


對於synchronized關鍵字來說,一共可以分為兩類:對象鎖和類鎖。

v2-831a62474f5a066f2df94e4b6e3cbab2_1440w.jpg

我們一個一個來看如何使用。

1、對象鎖


對於對象鎖來說,又可以分為兩個,一個是方法鎖,一個是同步代碼塊鎖。

(1)同步代碼塊鎖


同步代碼塊鎖主要是對代碼塊進行加鎖,其實已經演示過了,就是上面的那個案例。不過為了保持一致我們再舉一個例子。

public class SynTest01 implements Runnable {
    Object object = new Object();
    public static void main(String[] args) throws InterruptedException {
        SynTest01 syn = new SynTest01();
        Thread thread1 = new Thread(syn);
        Thread thread2 = new Thread(syn);
        thread1.start();
        thread2.start();
        //線程1和線程2只要有一個還存活就一直執行
        while (thread1.isAlive() || thread2.isAlive()) {}
        System.out.println("main程序運行結束");
    }
    @Override
    public void run() {
        synchronized (object) {
            try {
                System.out.println(Thread.currentThread().getName() 
                        + "線程執行了run方法");
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() 
                        + "執行2秒鐘之後完畢");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

在這個例子中,我們使用了synchronized鎖住了run方法中的代碼塊。表示同一時刻只有一個線程能夠進入代碼塊。就好比是去醫院掛號,前面一個人辦完了業務,下一個人才開始。

在這裡面我們看到,線程1和線程2使用的是同一個鎖,也就是我們new的Object。如果我們讓線程1和線程2每一個人擁有一個鎖對象呢?

public class SynTest01 implements Runnable {
    Object object1 = new Object();
    Object object2 = new Object();
    public static void main(String[] args) throws InterruptedException {
        SynTest01 syn = new SynTest01();
        Thread thread1 = new Thread(syn);
        Thread thread2 = new Thread(syn);
        thread1.start();
        thread2.start();
        //線程1和線程2只要有一個還存活就一直執行
        while (thread1.isAlive() || thread2.isAlive()) {}
        System.out.println("main程序運行結束");
    }
    @Override
    public void run() {
        synchronized (object1) {
            try {
                System.out.println(Thread.currentThread().getName() 
                        + "線程執行了object1");
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() 
                        + "執行object1完畢");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        synchronized (object2) {
            try {
                System.out.println(Thread.currentThread().getName() 
                        + "線程執行object2");
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() 
                        + "執行object2完畢");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

現在線程1和線程2每個人擁有一把鎖,去訪問不同的方法資源。這時候會出現什麼情況呢?

v2-9f4814d763d8eeea6c3ee9bf6e123afc_1440w.jpg

我們同樣用一張圖看一下其原理。

v2-1ab8f44a5492a95d73f22b261b0666a8_1440w.jpg

也就是說,相當於兩個業務有倆窗口都可以辦理,但是兩個任務都需要排隊辦理。

同步代碼塊鎖總結:

同步代碼塊鎖主要是對代碼塊進行加鎖,此時同一時刻只能有一個線程獲取到該資源,要注意每一把鎖只負責當前的代碼塊,其他的代碼塊不管。

以上就是同步代碼快的使用方法。下面我們看對象鎖的另外一種形式,那就是方法鎖。這裡的方法鎖指代的是普通方法。

(2)方法鎖

方法鎖相比較同步代碼塊鎖就簡單很多了,就是在普通方法上添加synchronized關鍵字修飾即可。

public class SynTest2 implements Runnable {
    public static void main(String[] args) throws InterruptedException {
        SynTest2 syn = new SynTest2();
        Thread thread1 = new Thread(syn);
        Thread thread2 = new Thread(syn);
        thread1.start();
        thread2.start();
        // 線程1和線程2只要有一個還存活就一直執行
        while (thread1.isAlive() || thread2.isAlive()) {
        }
        System.out.println("main程序運行結束");
    }
    @Override
    public void run() {
        method();
    }
    public synchronized void method() {
        try {
            System.out.println(Thread.currentThread().getName() + "進入到了方法");
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "執行完畢");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在這個例子中我們使用兩個線程對同一個普通方法進行訪問,結果可想而知,也就是同一時刻只能有一個線程進入到此方法。我們運行一下,看一下結果。

v2-48f14f84654fd70b2ad209a093b42d75_1440w.jpg

跟我們預想的一樣,很簡單。不過我們想過一個問題沒有,此時我們synchronized關鍵字加了一把鎖,這個鎖指代是誰呢?像同步代碼塊鎖synchronized (object),這裡面都有object,但是方法鎖是誰呢?

答案就是this對象,也就是說我們在方法鎖裡面synchronized其實鎖的就是當前this對象。我們如何去驗證this鎖的存在呢?不如我們再舉一個例子:

public class SynTest3 implements Runnable {
    public static void main(String[] args) throws InterruptedException {
        SynTest3 syn = new SynTest3();
        Thread thread1 = new Thread(syn);
        Thread thread2 = new Thread(syn);
        thread1.start();
        thread2.start();
        // 線程1和線程2只要有一個還存活就一直執行
        while (thread1.isAlive() || thread2.isAlive()) {}
        System.out.println("main程序運行結束");
    }
    @Override
    public void run() {
        method1();
        method2();
    }
    public synchronized void method1() {
        try {
            System.out.println(Thread.currentThread().getName() + "進入到了方法1");   
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "離開方法1,並釋放鎖");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public synchronized void method2() {
        try {
            System.out.println(Thread.currentThread().getName() + "進入到了方法2");
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "離開方法2,並釋放鎖");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

上面這個例子中,我們定義了兩個synchronized關鍵字修飾的方法method1和method2,然後讓兩個線程同時運行,我們測試一下看看會出現什麼結果:v2-dd723ea9d9448f843952f7587b3f8739_1440w (1).jpg

從結果來看,會發現不管是method1還是method2,同一個時刻兩個方法只能有一個線程在運行。這也就是this鎖導致的。我們再給一張圖描述一下其原理。

v2-12c08429702bd089c0836cd55869e038_1440w.jpg

現在應該明白了吧,這也就驗證了方法鎖的存在。也驗證了方法鎖的原理。下面我們繼續。討論一下類鎖。

2、類鎖


上面的鎖都是對象鎖,下面我們看看類鎖。類鎖其實也有兩種形式,一種是static方法鎖,一種是class鎖。

(1)static方法鎖


在java中,java的類對象可能有無數個,但是類卻只有一個。首先我們看第一種形式。

public class SynTest4 implements Runnable {
    public static void main(String[] args) throws InterruptedException {
        SynTest4 instance1 = new SynTest4();
        SynTest4 instance2 = new SynTest4();
        Thread thread1 = new Thread(instance1);
        Thread thread2 = new Thread(instance2);
        thread1.start();
        thread2.start();
        System.out.println("main程序運行結束");
    }
    @Override
    public void run() {
        method1();
    }
    public static synchronized void method1() {
        try {
            System.out.println(Thread.currentThread().getName() + "進入到了靜態方法");
            
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "離開靜態方法,並釋放鎖");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在這個例子中我們定義了兩個不同的對象instance1和instance2。分別去執行了method1。會出現什麼結果呢?

v2-ef68a9cf2a55f501503c254f2d405405_1440w.png

如果我們把static關鍵字去掉,很明顯現在就是普通方法了,如果我們再去運行,由於instance1和instance2是兩個不同的對象,那麼也就是兩個不同的this鎖,這時候就能隨便進入了。我們去掉static關鍵字之後運行一下:

v2-4bf23217d78970ceb2a4ee36ac6fc878_1440w.jpg

現在看到了,由於是兩個不同的this鎖,所以都能進入,就好比是一個門有兩把鑰匙,每一把都能打開門。

(2)class鎖


這種用法我們直接看例子再來分析一下:

public class SynTest5 implements Runnable {
    public static void main(String[] args) throws InterruptedException {
        SynTest5 instance1 = new SynTest5();
        SynTest5 instance2 = new SynTest5();
        Thread thread1 = new Thread(instance1);
        Thread thread2 = new Thread(instance2);
        thread1.start();
        thread2.start();
        // 線程1和線程2只要有一個還存活就一直執行
        while (thread1.isAlive() || thread2.isAlive()) {}
        System.out.println("main程序運行結束");
    }
    @Override
    public void run() {
        method1();
    }
    public void method1() {
        synchronized (SynTest5.class) {
            try {
                System.out.println(Thread.currentThread().getName() + "進入到了方法");
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() + "離開方法");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

在這個例子中我們使用了同步代碼塊,不過synchronized關鍵字包裝的可不是object了,而是SynTest5.class。我們還定義了兩個不同的對象實例instance1和instance2。運行一下我們會發現,線程1和線程2依然會依次執行。

以上就是synchronized關鍵字的幾種常見的用法,到這裡我們來一個總結:

對於同步不同步,關鍵點在於鎖,兩個線程執行的是同一把鎖,那麼就依次排隊等候,兩個線程執行的不是同一把鎖,那就各幹各的事。

基本的使用我們也講完了,下面我們進入下一個專題,那就是我們需要注意的事項。這是面試常考的一個問題,不管是機試還是面試。

Leave a Reply

Your email address will not be published. Required fields are marked *