多線程-synchronized(你以為你真的懂Synchronized)


一、為什么要使用synchronized關鍵字?

1、使用synchronized關鍵字的原因:在並發編程問題中存在着共享數據,在多線程操作共享數據時,要保證同一時刻只有一個線程在執行某個方法或某個代碼塊;    synchronized既保證了原子性,又保證了可見性,所以可以使用synchronized來達到目的。

  • 原子性:此操作不可分割,不能分為多步操作,也就是在此操作過程中不能有其他線程介入,過來的線程只能等當前線程釋放互斥鎖,然后獲得此鎖才能執行該方法或代碼塊(synchronized   作用 的方法或代碼塊內容要么不執行,要執行就保證全部執行完畢);
  • 可見性:一個線程對共享數據修改后,要保證其他線程能夠知道;這會涉及到下一篇我們要講的Volatile也可以做到可見性;

2、這里解釋一下共享變量不可見原因:一般Java的變量都存儲在主內存中,每個線程都有自己獨立的工作內存,當他們工作時會將共享變量拷貝一份進行操作,並不是直接操作主內存,這里就不是一個原子操作,所以存在中間狀態,而多線程之間又會出現交叉執行,就會出現沒能及時的對主內存的共享數據進行更新,這就出現了可見性的問題;

  • 線程對共享變量的所有操作都必須在自己的工作內存中進行,不能直接在主內存中讀寫;
  • 不同線程之間無法直接訪問其他線程工作內存中的變量,線程間變量值的傳遞需要通過主內存來完成;

3、線程執行synchronized代碼塊的流程:

  • 線程獲得互斥鎖
  • 清空工作內存
  • 從主內存拷貝共享變量最新的值到工作內存成為副本
  • 執行代碼
  • 將修改后的副本的值刷新回主內存中
  • 線程釋放鎖

二、synchronized的原子性和可見性

舉一個例子,多線程對同一個共享變量進行操作,如果有兩個線程操作變量 int a = 0;  不加synchronized關鍵字時,線程1和線程2可以同時操作a變量,恰好它們同時讀取了a = 0,然后分別拷貝了一份到自己線程的工作區域;線程1進行a++; 把它修改為1,線程2也做同樣的操作也修改成了1,本來已經加了兩次,結果應該是2,但是現在的結果還是1。這時我們加上synchronized關鍵字,如果線程1獲取了鎖資源,線程2必須等線程1執行結束釋放了鎖,他才能夠獲取這個互斥鎖,進行操作;

1、第一個小程序:

package com.example.demo.threaddemo.juc_002;

/**
 * 創建100個線程對共享變量a進行操作,比較使用synchronized前后的區別
 * 不加synchronized很大程度上會出現臟讀,加上synchronized就可以避免這個問題
 */
public class SynchronizedTest00 {

    //定義一個共享變量
    private int a = 0;

    //定義一個Object對象
    Object o = new Object();

    //對變量進行add操作
    public void add(){
        synchronized (o) {  //這里鎖定的是o這個對象
            a++;
            System.out.println(Thread.currentThread().getName() + " a = " + a);
        }
    }

    public static void main(String[] args) {
        SynchronizedTest00 t = new SynchronizedTest00();
        for (int i = 0; i < 100; i++) {
            new Thread(()-> t.add(),"THREAD"+i).start();
        }
    }
}

 

2、當然每次加鎖都要new Object() ,實在太麻煩了,你還可以使用 synchronized (this):

package com.example.demo.threaddemo.juc_002;

/**
 * 創建100個線程對共享變量a進行操作,比較使用synchronized前后的區別
 * 不加synchronized很大程度上會出現臟讀,加上synchronized就可以避免這個問題
 */
public class SynchronizedTest00 {

    //定義一個共享變量
    private int a = 0;
    
    //對變量進行add操作
    public void add(){
        synchronized (this) {  //這里鎖定的是當前對象
            a++;
            System.out.println(Thread.currentThread().getName() + " a = " + a);
        }
    }

    public static void main(String[] args) {
        SynchronizedTest00 t = new SynchronizedTest00();
        for (int i = 0; i < 100; i++) {
            new Thread(()-> t.add(),"THREAD"+i).start();
        }
    }
}

3、synchronized還可以直接修飾方法(這里用synchronized直接修飾方法顯然鎖的粒度變大了,某些情況會影響執行效率):

package com.example.demo.threaddemo.juc_002;

/**
 * 創建100個線程對共享變量a進行操作,比較使用synchronized前后的區別
 * 不加synchronized很大程度上會出現臟讀,加上synchronized就可以避免這個問題
 */
public class SynchronizedTest00 {

    //定義一個共享變量
    private int a = 0;

    //對變量進行add操作
    public synchronized void add(){
        a++;
        System.out.println(Thread.currentThread().getName() + " a = " + a);
    }

    public static void main(String[] args) {
        SynchronizedTest00 t = new SynchronizedTest00();
        for (int i = 0; i < 100; i++) {
            new Thread(()-> t.add(),"THREAD"+i).start();
        }
    }
}

三、同步方法和非同步方法可以同時調用嗎?

就是我有一個同步方法m1,在調用m1的過程中是否可以調用m2(非同步方法) ,用腳趾頭想想肯定是可以的因為你調用m2的時候又不需要獲得鎖;

package com.example.demo.threaddemo.juc_008;

import java.util.concurrent.TimeUnit;

/**
 * 測試同步方法和非同步方法是否可以同時調用
 */
public class Synchronized_01 {

    /**
     * 同步方法
     */
    public synchronized void m1(){
        System.out.println("m1 is begining -----------");
        System.out.println(Thread.currentThread().getName()+ "------"+"m1 Thread");
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m1 is end ----------------");
    }

    /**
     * 非同步方法
     */
    public void m2(){
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+ "------"+"m2 Thread");
    }

    public static void main(String[] args) {
        Synchronized_01 t =new Synchronized_01();
        new Thread(t::m1 , "Thread m1").start();
        new Thread(t::m2 ,"Thread m2").start();
    }
}

//運行結果:

m1 is begining -----------
Thread m1------m1 Thread
Thread m2------m2 Thread
m1 is end ----------------

 

四、synchronized的可重入性

可重入性的意思就是在一個同步方法中調用另一個同步方法;現在有兩個同步方法m1、m2  而且加的是同一把鎖; 你在方法m1中調用m2,首先獲得這把鎖開始執行m1方法,當你要執行m2時也要獲得這把鎖,如果這時鎖不可重入,那就進入了死鎖的狀態;  如果可重入,允許你申請,沒毛病 問題不大這就叫可重入鎖;

package com.example.demo.threaddemo.juc_008;

import java.util.concurrent.TimeUnit;

/**
 * 測試鎖的可重入性
 */
public class Synchronized_02 {
    /**
     * 同步方法m1
     */
    public synchronized void m1(){
        System.out.println("m1 is begining -----------");
        System.out.println(Thread.currentThread().getName()+ "------"+"m1 Thread");
        m2();
        System.out.println("m1 is end ----------------");
    }

    /**
     * 同步方法m2
     */
    public synchronized void m2(){
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+ "------"+"m2 Thread");
    }

    public static void main(String[] args) {
        Synchronized_02 t =new Synchronized_02();
        new Thread(t::m1 , "Thread m1").start();
    }
}

 

五、出現鎖異常(並發處理一定要注意)

異常鎖 程序中出現異常 默認鎖會被釋放假設 web端處理程序的過程中,多個servlet共同訪問一個資源,這時第一個線程出現了異常此時釋放了鎖,其他線程就會進入同步代碼塊,極大可能讀取到異常時產生的數據。所以要非常小心的處理同步業務的異常;

package com.example.demo.threaddemo.juc_008;

import java.util.concurrent.TimeUnit;

/**
 * 異常鎖 程序中出現異常 默認鎖會被釋放
 * 假設 web端處理程序的過程中,多個servlet共同訪問一個資源,這時第一個線程出現了異常
 * 此時釋放了鎖,其他線程就會進入同步代碼塊,極大可能讀取到異常時產生的數據。
 */
public class Synchronized_03 {
    private int count = 10;

    public synchronized void m(){
        System.out.println(Thread.currentThread().getName()+"start -----------------");
        while(true) {
            count--;
            System.out.println(Thread.currentThread().getName() + "-----count:" + count);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (count == 5) {
                //故意制造異常 此處出現異常,鎖將被釋放,想要不釋放鎖 ,需要進行try catch
                int m = 1 / 0;
            }
        }
    }

    public synchronized void mm(){
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+":-----Thread m1 拋異常 釋放鎖了 我就來了----- count:"+ this.count);
    }

    public static void main(String[] args) {
        Synchronized_03 t =new Synchronized_03();
        new Thread(t::m , "Thread m1").start();

        new Thread(t::mm , "Thread m2").start();
    }
}

//運行結果:

Thread m1start -----------------
Thread m1-----count:9
Thread m1-----count:8
Thread m1-----count:7
Thread m1-----count:6
Thread m1-----count:5
Exception in thread "Thread m1" java.lang.ArithmeticException: / by zero
at com.example.demo.threaddemo.juc_008.Synchronized_03.m(Synchronized_03.java:25)
at java.lang.Thread.run(Thread.java:745)
Thread m2:-----Thread m1 拋異常 釋放鎖了 我就來了----- count:5

 

六、synchronized的底層實現

在很多人的認知里,都會認為synchronized是重量級的鎖;

  • 早期的synchronized的底層是重量級的,重量級到這個synchronized都需要向操作系統申請鎖資源,這就會造成運行的效率非常的低,后來Java越來越開始處理高並發的程序,很多的程序員開始不滿,覺得這個synchronized太重了,沒辦法需要開發新的框架;
  • 后期在jdk1.6以后對synchronized進行的優化,當我們使用synchronized時Hotspot是這樣實現的:

1、偏向鎖:偏向鎖,顧名思義,它會偏向於第一個訪問鎖的線程,如果在運行過程中,同步鎖只有一個線程訪問,不存在多線程爭用的情況,第一個來訪問某把鎖的線程,只是在這個對象頭(markword)中記錄這個線程(此時偏向鎖位為”1“ 鎖的標志位為”01“   從鎖的標志位不難看出第一次訪問的時候實際上沒有給這個對象上鎖,內部實現只是記錄線程的id在對象頭中 ),這時為偏向鎖

2、輕量級鎖(也稱自旋鎖):偏向鎖如果出現了競爭的話,就會升級為輕量級鎖,這時新來的競爭的線程不會跑到等待隊列中去,而是在這里自旋(虛擬機首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝,官方稱之為 Displaced Mark Word ,拷貝成功后,虛擬機將使用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針,並將Lock record里的owner指針指向object mark word , 如果這個更新動作成功了,那么這個線程就擁有了該對象的鎖,並且對象Mark Word的鎖標志位設置為“00”),當自旋的次數達到一定程度或者是自旋的線程數達到一定程度就會進行下一次升級,升級為重量級鎖;

3、重量級鎖:輕量級鎖膨脹為重量級鎖,鎖的標志位變為“10”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,后面等待鎖的線程也要進入阻塞狀態,重量級鎖就需要到操作系統中去申請資源。

 

 

 總結一下:通過以上對synchronized內部鎖升級的過程不難看出只有一個線程時,不存在競爭時,這時為偏向鎖,鎖的標志位為01;

                   當出現線程競爭時,鎖升級為輕量級鎖,這里有一個CAS(Compare And Swap)操作也就是自旋,如果markdown中的LockRecoed指針修改成功,說明此對象的鎖,鎖                     的標志位改為00;

                   當輕量級鎖的自旋次數或線程數達到一定程度,就會出現鎖的膨脹,升級為重量級鎖;

這里注意一點並不是CAS的效率就一定比系統鎖的效率要高,這要分不同的情況,執行時間短,線程的數量少,肯定是用自旋,線程數量多,執行時間長,肯定是使用系統鎖

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM