多線程安全-sychronized


layout: post title: "多線程安全-sychronized" categories: [編程] tags: [Java,多線程] published: True

形成線程數據錯亂的三要素 ( 同時也是保持線程安全的要素 )

名詞解釋

  • 原子性(Synchronized, Lock)即一個操做或者多個操做,要麼執行 要麼就都不執行,在執行過程當中不可打斷
  • 有序性 (Volatile,Synchronized, Lock) 程序默認的執行順序
  • 可見性 (Volatile,Synchronized,Lock) 當變量被修改時,所修改的值會被當即同步到主存中,其餘程序或者進程再讀取時獲取的值是最新的

synchronized的三種應用方式

  • 修飾實例方法,做用於當前實例加鎖,進入同步代碼前要得到當前實例的鎖

    public class AccountingSync implements Runnable{
        //共享資源(臨界資源)
        static int i=0;
    
        /** * synchronized 修飾實例方法 * 暫時先不添加 */
        public void increase(){
            i++;
        }
        @Override
        public void run() {
            for(int j=0;j<1000000;j++){
                increase();
            }
        }
        public static void main(String[] args) throws InterruptedException {
            AccountingSync instance=new AccountingSync();
            Thread t1=new Thread(instance);
            Thread t2=new Thread(instance);
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(i);
        }
        /** * 輸出結果: * 1802452 */
    }
    複製代碼

    i++ 賦值操做並無 原子性 在讀取原來的參數和返回新的參數的這段時間中,若是咱們第二個線程也作了一樣的操做,兩個線程看到的參數是同樣的,一樣執行了 +1 的操做,這裏就形成了線程安全破壞,咱們最終輸出的結果是 1802452java

    若是咱們添加上 synchronized 此時 increase() 方法在一個時間內 只能被一個線程讀寫,也就避免髒數據的產生。編程

    可是這樣也不是安全的,若是咱們 new 出兩個 AccountingSync 對象去執行操做 結果是怎麼樣?安全

    public class AccountingSyncBad implements Runnable{
        static int i=0;
        public synchronized void increase(){
            i++;
        }
        @Override
        public void run() {
            for(int j=0;j<1000000;j++){
                increase();
            }
        }
        public static void main(String[] args) throws InterruptedException {
            //new新實例
            Thread t1=new Thread(new AccountingSyncBad());
            //new新實例
            Thread t2=new Thread(new AccountingSyncBad());
            t1.start();
            t2.start();
            //join含義:當前線程A等待thread線程終止以後才能從thread.join()返回
            t1.join();
            t2.join();
            System.out.println(i);
        }
    }
    複製代碼

    結果依舊是產生了髒數據,緣由是兩個實例對象鎖並不一樣相同,此時若是兩個線程操做數據並不是共享的,線程安全是有保障的,遺憾的是若是兩個線程操做的是共享數據,安全將沒有保障,他們自己的鎖只能保持自己數據結構

  • 修飾靜態方法,做用於當前類對象加鎖,進入同步代碼前要得到當前類對象的鎖

    public class AccountingSyncClass implements Runnable{
        static int i=0;
    
        /** * 做用於靜態方法,鎖是當前class對象,也就是 * AccountingSyncClass類對應的class對象 */
        public static synchronized void increase(){
            i++;
        }
    
        /** * 非靜態,訪問時鎖不同不會發生互斥 */
        public synchronized void increase4Obj(){
            i++;
        }
    
        @Override
        public void run() {
            for(int j=0;j<1000000;j++){
                increase();
            }
        }
        public static void main(String[] args) throws InterruptedException {
            //new新實例
            Thread t1=new Thread(new AccountingSyncClass());
            //new心事了
            Thread t2=new Thread(new AccountingSyncClass());
            //啓動線程
            t1.start();t2.start();
    
            t1.join();t2.join();
            System.out.println(i);
        }
    }
    複製代碼

    這個實例當中咱們將 synchronized 做用於靜態方法了,由於靜態方法不屬於任何實例對象,它是類成員,因此這把鎖也能夠理解爲加在了 Class 上 ,可是若是咱們線程A 調用了 class 內部 static synchronized 方法 線程B 調用了 class 內部 非 static 方法 是能夠的,不會發生互斥現象,由於訪問靜態 synchronized 方法佔用的鎖是當前類的class對象,而訪問非靜態 synchronized 方法佔用的鎖是當前實例對象鎖多線程

  • 修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要得到給定對象的鎖。

    public class AccountingSync implements Runnable{
        static AccountingSync instance=new AccountingSync();
        static int i=0;
        @Override
        public void run() {
            //省略其餘耗時操做....
            //使用同步代碼塊對變量i進行同步操做,鎖對象爲instance
            synchronized(instance){
                for(int j=0;j<1000000;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 是如何實現的同步

同步代碼塊

內部使用 monitorenter 和 monitorexit 指令實現,monitorenter 指令插入到同步代碼塊的開始位置,monitorexit 指令插入到同步代碼塊的結束位置,jvm須要保證每個monitorenter都有一個 monitorexit 與之對應。任何一個對象都有一個 monitor 與之相關聯,當它的monitor被持有以後,它將處於鎖定狀態。線程執行到 monitorenter 指令前,將會嘗試獲取對象所對應的 monitor 全部權,即嘗試獲取對象的鎖;將線程執行到 monitorexit 時就會釋放鎖。jvm

(人話:)每一個對象都會與一個monitor相關聯,當某個monitor被擁有以後就會被鎖住,當線程執行到monitorenter指令時,就會去嘗試得到對應的monitor。步驟以下:ide

  1. 每一個monitor維護着一個記錄着擁有次數的計數器。未被擁有的monitor的計數器爲0,當一個線程得到monitor(執行monitorenter)後,該計數器自增變爲 1 。當同一個線程再次得到該monitor的時候,計數器再次自增;當不一樣線程想要得到該monitor的時候,就會被阻塞。post

  2. 當同一個線程釋放monitor(執行monitorexit指令)的時候,計數器再自減。當計數器爲0的時候。monitor將被釋放,其餘線程即可以得到monitor。 spa

    image

同步方法

不過相對於普通方法,其常量池中多了ACC_SYNCHRONIZED標示符。JVM就是根據該標示符來實現方法的同步的:當方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,若是設置了,執行線程將先獲取monitor,獲取成功以後才能執行方法體,方法執行完後再釋放monitor。線程

在jvm字節碼層面並無任何特別的指令來實現synchronized修飾的方法,而是在class文件中將該方法的access_flags字段中的acc_synchronized標誌位設置爲1,表示該方法爲synchronized方法。

在java設計中,每個對象自打孃胎裏出來就帶了一把看不見的鎖,即monitor鎖。monitor是線程私有的數據結構,每個線程都有一個monitor record列表,同時還有一個全局可用列表。每個被鎖住對象都會和一個monitor關聯。monitor中有一個owner字段存放擁有該對象的線程的惟一標識,表示該鎖被這個線程佔有。owner:初始時爲null,表示當前沒有任何線程擁有該monitor,當線程成功擁有該鎖後,owner保存線程惟一標識,當鎖被釋放時,owner又變爲null。

image
總結: 同步方法和同步代碼塊底層都是經過monitor來實現同步的。二者的區別:同步方式是經過方法中的access_flags中設置ACC_SYNCHRONIZED標誌來實現;同步代碼塊是經過monitorenter和monitorexit來實現咱們知道了每一個對象都與一個monitor相關聯。而monitor能夠被線程擁有或釋放。
相關文章
相關標籤/搜索