多線程(二)、內置鎖 synchronized

前言

在上一篇 多線程(一)、基礎概念及notify()和wait()的使用 文章中咱們講了多線程的一些基礎概念還有等待通知機制,在講線程之間共享資源的時候,提到會出現數據不一樣步問題,咱們先經過一個示例來演示這個問題。java

/**
 * @author : EvanZch
 *         description:
 **/

public class SynchronizedTest {

    // 賦count初始值爲0
    public static int count = 0;
    // 進行累加操做
    public void add() {
        count++;
    }

    public static class TestThread extends Thread {
        private SynchronizedTest synchronizedTest;

        public TestThread(SynchronizedTest synchronizedTest) {
            this.synchronizedTest = synchronizedTest;
        }
        @Override
        public void run() {
            super.run();
            // 執行10000次累加
            for (int x = 0; x < 10000; x++) {
                synchronizedTest.add();
            }
        }
    }
    public int getCount() {
        return count;
    }
    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        // 開啓兩個線程
        new TestThread(synchronizedTest).start();
        new TestThread(synchronizedTest).start();
        int count = synchronizedTest.getCount();
        System.out.println("count=" + count);
    }
}
複製代碼

能夠看到,我程序中咱們啓動了兩個線程,同時對 Count 變量進行累加操做,每一個線程循環累加10000次,咱們預想的結果,獲取的count值應該會是20000,執行程序能夠發現。web

0?爲何結果會是0?由於咱們在main裏面開啓線程執行,方法是順序執行,當執行到 輸出語句的時候,線程run方法尚未啓動,因此這裏打印的是count的初始值 0;安全

怎麼獲取到正確結果?多線程

一、等待一會在獲取結果app

    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        new TestThread(synchronizedTest).start();
        new TestThread(synchronizedTest).start();
        // 等待一秒再回去結果
        Thread.sleep(1000);
        int count = synchronizedTest.getCount();
        System.out.println("count=" + count);
    }
複製代碼

咱們在獲取結果以前,先等待一秒,結果以下:ide

結果再也不爲 0 ,可是結果也不是咱們預想的 20000啊,難道是等待時間不夠?咱們增長等待時間,在運行,發現結果也不是20000,這麼看,使用等待時間不嚴謹,由於沒辦法判斷線程執行結束時間(其實線程執行很快的,遠不須要幾秒),那咱們可使用 join方法。函數

二、thread.join()post

咱們先看一下 thread 的 join方法this

    /**
     * Waits for this thread to die.
     *
     * <p> An invocation of this method behaves in exactly the same
     * way as the invocation
     *
     * <blockquote>
     * {@linkplain #join(long) join}{@code (0)}
     * </blockquote>
     *
     * @throws  InterruptedException
     *          if any thread has interrupted the current thread. The
     *          <i>interrupted status</i> of the current thread is
     *          cleared when this exception is thrown.
     */

    public final void join() throws InterruptedException {
        join(0);
    }
複製代碼

註釋大概意思是:當調用join方法後,會進行阻塞,直到該線程任務執行結束。spa

可讓線程順序執行。

那咱們能夠簡單修改代碼,讓兩個線程執行結束後再打印結果

這裏須要注意,咱們是在 main 這個線程裏面調用 join 方法, 則兩個線程會在main 線程阻塞,可是兩個子線程仍是在並行處理,都執行結束後纔會喚醒 main 線程執行後續操做。

    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        TestThread testThread = new TestThread(synchronizedTest);
        TestThread testThread1 = new TestThread(synchronizedTest);
        testThread.start();
        testThread1.start();
        // 讓程序順序執行
        testThread.join();
        testThread1.join();
        // 當兩個線程任務結束後再獲取結果
        int count = synchronizedTest.getCount();
        System.out.println("count=" + count);
    }
複製代碼

結果:

發現結果也不是咱們預想的 20000,咱們使用了 join() 方法,它會在調用線程進行阻塞(main),當testThreadtestThread1 都執行結束後再喚醒調用線程 , 能確保兩個線程確定是執行結束了的,但是結果跟預期不一致,屢次打印,發現結果一直在 10000 ~ 20000 這個區間波動。

爲何會出現這種狀況?

上一篇文章講過,同一個進程的多個線程共享該進程的全部資源,當多個線程同時訪問一個對象或者一個對象的成員變量,可能會致使數據不一樣步問題,好比 線程A數據a進行操做,須要從內存中進行讀取而後進行相應的操做,操做完成後再寫入內存中,可是若是數據尚未寫入內存中的時候,線程B 也來對這個數據進行操做,取到的就是還未寫入內存的數據,致使先後數據同步問題,咱們也叫線程不安全操做

好比 線程 A 取到 count 的時候,其值爲 100,加 1 後再放入內存中,若是在放入內存以前 線程B 也來拿 count 並對其進行累加操做,這個時候 **線程B **取到的 count 值 仍是100,加 1 後放入內存,這個時候值爲101, 這樣 線程 A 進行累加的那步操做就沒有被算上,這就是爲啥,最後兩個線程算出來的結果確定是小於 20000。

怎麼避免這種狀況?

咱們知道出現這種狀況的緣由是操做的時候,由於多個線程同時訪問一個對象或者對象的成員變量,要處理這個問題,咱們就引入了關鍵字 synchronized

正文

1、內置鎖 synchronized

關鍵字 synchronized 能夠修飾方法或者以同步塊的形式來進行使用,它主要確保多個線程在同一個時刻,只能有一個線程處於方法或者同步塊中,它保證了線程對變量訪問的可見性排他性,又稱爲內置鎖機制

鎖又分爲對象鎖和類鎖:

對象鎖: 對一個對象實例進行鎖操做,實例對象能夠有多個,不一樣對象實例的對象鎖互不干擾。

類鎖:用於類的靜態方法或者類的Class對象進行鎖操做。咱們知道每一個類只有一個Class對象,也就只有一個類鎖。

注意點:

類鎖只是一個概念上的東西,它鎖的也是對象,只不過這個對象是類的Class對象,其惟一存在。

類鎖和對象鎖之間互不干擾。

經過上面的案例,咱們簡單改改,咱們在執行累加方法上加上 synchronized 關鍵字,而後再運行。

/**
 * @author : EvanZch
 *         description:
 **/

public class SynchronizedTest {

    public static int count = 0;

    // 咱們對add方法添加關鍵字 synchronized
    public synchronized void add() {
        count++;
    }

    public static class TestThread extends Thread {
        private SynchronizedTest synchronizedTest;

        public TestThread(SynchronizedTest synchronizedTest) {
            this.synchronizedTest = synchronizedTest;
        }

        @Override
        public void run() {
            super.run();
            for (int x = 0; x < 10000; x++) {
                synchronizedTest.add();
            }
        }
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        TestThread testThread = new TestThread(synchronizedTest);
        TestThread testThread1 = new TestThread(synchronizedTest);
        testThread.start();
        testThread1.start();

        // 讓程序順序執行
        testThread.join();
        testThread1.join();

        int count = synchronizedTest.getCount();
        System.out.println("count=" + count);
    }
}
複製代碼

結果:

能夠看到咱們只加了一個關鍵字 synchronized ,結果就跟咱們預期的 20000 一致,咱們將 synchronized

添加到方法上,就確保了多個線程同一時刻只有一個線程對此方法進行操做,這樣就確保了線程安全問題。

前面說了內置鎖存在對象鎖類鎖 ,咱們來看一下具體怎麼實現和區別。

1.一、對象鎖

對一個對象實例進行鎖操做,實例對象能夠有多個,不一樣對象實例的對象鎖互不干擾。

咱們在前面的示例上進行更改。

方法鎖:

    // 非靜態方法
    public synchronized void add() {
        count++;
    }
複製代碼

同步代碼塊鎖:

    public void add(){
        synchronized (this){
            count ++;
        }
    }
複製代碼

或者:

    // 非靜態變量
    public Object object = new Object();
    public void add(){
        synchronized (object){
            count ++;
        }
    }
複製代碼

咱們能夠看到對象鎖都是對非靜態方法和非靜態變量進行加鎖,以上三種從本質上來講沒有區別,咱們這個時候再改一下咱們的示例代碼,來驗證一下 不一樣對象實例的對象鎖互不干擾

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

        SynchronizedTest synchronizedTest = new SynchronizedTest();
        // 咱們再建立一個 SynchronizedTest 對象
        SynchronizedTest synchronizedTest1 = new SynchronizedTest();
        // 傳入 synchronizedTest 
        TestThread testThread = new TestThread(synchronizedTest);
        // 傳入 synchronizedTest1
        TestThread testThread1 = new TestThread(synchronizedTest1);

        testThread.start();
        testThread1.start();
        // 讓程序順序執行
        testThread.join();
        testThread1.join();

        int count = synchronizedTest.getCount();
        System.out.println("count=" + count);
    }
複製代碼

咱們開啓兩個線程,分別傳入了不一樣的實例對象,這個時候再屢次運行,查看運行結果。

結果:

咱們屢次運行獲取結果,發現都獲取不到咱們指望的20000,能夠咱們明明也在add() 方法上添加了 synchronized 啊,惟一不一樣的就是,兩個線程傳入了不一樣的對象,因此經過結果,咱們能夠得出,不一樣對象的對象鎖之間,是互不影響,各類運行。

1.二、類鎖

用於類的靜態方法或者類的Class對象進行鎖操做。咱們知道每一個類只有一個Class對象,也就只有一個類鎖。

類鎖其實也是對象鎖,只不過鎖的對象比較特殊。

靜態方法鎖:

    // 靜態方法
    public static synchronized void add() {
        count++;
    }
複製代碼

同步代碼塊鎖:

    public void add(){
        // 傳入Class對象
        synchronized (SynchronizedTest.class){
            count ++;
        }
    }
複製代碼

或者:

    // 靜態成員變量
    public static Object object = new Object();
    public void add(){
        synchronized (object){
            count ++;
        }
    }
複製代碼

咱們知道靜態變量和類的Class對象在內存中只存在一個,因此咱們對add方法經過類鎖方式進行加鎖,無論外界這個時候傳的對象有多少個,它也是惟一的,咱們再執行上面的main方法,打印結果:

能夠看到結果和指望一致。

知識拓展 :static 關鍵字和 new 一個對象,作了什麼操做?

static 關鍵字:

  • 靜態變量是隨着類加載時被完成初始化的,它在內存中僅有一個,且 JVM 也只會爲它分配一次內存,同時類全部的實例都共享靜態變量,即一處變、到處變,能夠直接經過類名來訪問它。
  • 可是實例變量則不一樣,它是伴隨着new實例化的,每建立一個實例就會產生一個實例變量,它與該實例同生共死。

new 一個對象,底層作了啥?
一、Jvm加載未加載的字節碼,開闢空間二、靜態初始化(1靜態代碼塊和2靜態變量)三、成員變量初始化(1普通代碼塊和2普通成員變量)四、構造器初始化(構造函數)

相關文章
相關標籤/搜索