JAVA基礎-synchronized關鍵字

synchronized關鍵字也叫作互斥鎖或者同步。
 
這個關鍵字的存在是爲了解決編程中的線程安全問題的,而線程安全問題出現的主要原因一般爲:多個線程操作同一個對象的數據,也就是同時操作共享變量的值。
 
synchronized的出現解決了這個問題,互斥鎖的含義爲,當一個線程操作一個對象的時候,對該對象增加一個鎖,任何其他線程都處在等待狀態,不可以對該對象進行操作。當持有鎖的線程執行完畢後,會釋放持有鎖,其他等待線程共同競爭鎖資源。
同時,synchronized關鍵字還可以保證線程的變化對其他線程可見,保證共享變量的可見性,也就是volatile功能。
 
synchronized關鍵字的應用
 
1、修飾實例方法:對當前實例對象加鎖,執行同步代碼前獲取當前實例對象的控制權。
2、修飾靜態方法:對當前類對象加鎖,執行同步代碼前獲取當前類對象的控制權。
3、修飾代碼塊:對任意指定對象加鎖,執行同步代碼前獲取指定對象的控制權。
 
 
修飾實例方法:
 
/**
* synchronized
*
* Created by j on 2018/4/26.
*/
public class SynchronizedTest {
    static int i = 0;
    public synchronized void add(){
        i++;
    }
    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++ ){
                    synchronizedTest.add();
                }
            }
        });
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++ ){
                    synchronizedTest.add();
                }
            }
        });
        thread.start();
        thread1.start();
        // 確保線程執行完畢
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(i);
    }
}
 
 
上述代碼可以看到,新建了兩個線程分別調用同步方法add() 由於兩個線程鎖的都是synchronizedTest對象,所以可以保證線程運行完畢,i的值爲200000。得出結論修飾實例方法是對實例對象進行加鎖。
 
接下來看一段代碼:
/**
* synchronized
*
* Created by j on 2018/4/26.
*/
public class SynchronizedTest {
    static int i = 0;
    public synchronized void add(){
        i++;
    }
    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        SynchronizedTest synchronizedTest1 = new SynchronizedTest();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++ ){
                    synchronizedTest.add();
                }
            }
        });
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++ ){
                    synchronizedTest1.add();
                }
            }
        });
        thread.start();
        thread1.start();
        // 確保線程執行完畢
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(i);
    }
}
 
對之前的代碼做了輕微的改動,兩個線程分別對兩個實例對象加鎖,這時當線程運行結束,結果不一定會是200000,得到了198478結果,說明兩個線程用的是不同的鎖,無法保證線程安全。
 
修飾靜態方法:
 
/**
* synchronized
*
* Created by j on 2018/4/26.
*/
public class SynchronizedTest {
    static int i = 0;
    public static synchronized void add(){
        i++;
    }
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++ ){
                    add();
                }
            }
        });
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++ ){
                    add();
                }
            }
        });
        thread.start();
        thread1.start();
        // 確保線程執行完畢
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(i);
    }
}
 
 
同步方法添加了static關鍵字,說明對當前類對象加鎖。靜態方法不屬於任何一個實例。
但是如果一個線程A調用靜態同步方法,另一個線程B調用實例同步方法,並且對同一個變量進行操作的時候,會發生線程安全問題,因爲:靜態方法鎖的是類對象,實例方法鎖的是實例對象,是兩個不同的鎖。
代碼舉例:
/**
* synchronized
*
* Created by j on 2018/4/26.
*/
public class SynchronizedTest {
    static int i = 0;
    public static synchronized void add(){
        i++;
    }
    public synchronized void add1(){
        i++;
    }
    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest test = new SynchronizedTest();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++ ){
                    test.add1();
                }
            }
        });
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++ ){
                    add();
                }
            }
        });
        thread.start();
        thread1.start();
        // 確保線程執行完畢
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(i);
    }
}
 
 
修飾代碼塊:
 
    public void add(){
        synchronized (SynchronizedTest.class){
            i++;
        }
    }
    public void add1(){
        synchronized (this){
            i++;
        }
    }
 
當只需要對一部分代碼進行同步操作時,可以用synchronized修飾代碼片段,鎖定的對象可以是實例鍍錫或者this當前實例對象,也可以是XX.class類對象。
 
synchronized重入性
 
synchronized是給對象添加一個互斥鎖,當一個線程持有對象鎖時,其他操作該對象的線程將處於阻塞狀態。但是當一個線程持有對象鎖,然後再次請求自己持有對象鎖的臨界資源時,就是重入鎖,可以請求成功。也就是說,在一個synchronized方法執行時,可以調用該對象的另一個synchronized方法。
 
/**
* synchronized
*
* Created by j on 2018/4/26.
*/
public class SynchronizedTest {
    static int i = 0;
    public void add(){
        synchronized (SynchronizedTest.class){
            i++;
        }
    }
    public void add1(){
        synchronized (this){
            add();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest test = new SynchronizedTest();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++ ){
                    test.add();
                }
            }
        });
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++ ){
                    test.add();
                }
            }
        });
        thread.start();
        thread1.start();
        // 確保線程執行完畢
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(i);
    }
}
 
 
synchronized底層原理
 
JVM中的同步是通過持有和退出Monitor(監視器/管程)對象來實現的。
無論是顯式同步(同步代碼塊)或者是隱式同步(同步方法)都是這樣。
顯式同步與隱式同步的區別:
顯式同步:通過明確的代碼指令monitorenter(同步開始)和monitorexit(同步結束)來實現。
隱式同步:通過讀取常量池中方法的ACC_SYNCHRONIZED標識來實現。
 
synchronized同步代碼塊原理:
 
    public void synchronizedAdd(){
        synchronized (this){
            i++;
        }
    }
 
首先,對上面這個同步塊代碼進行反編譯,javap -c SynchronizedTest.class
可以得到這個方法的指令代碼。
其中第3行和第13行可以看到是通過monitorenter和monitorexit指令來實現同步。
當執行monitorenter指令時,當前線程獲取鎖對象的monitor持有權,當monitor持有計數器爲0時,線程獲得鎖成功,計數器+1。如果在運行時,調用該對象其他鎖方法,此時爲重入狀態,計數器再次+1。當同步代碼執行完畢,計數器歸0。其他線程將會試圖獲取monitor。
我們可以看到19行多了一個monitorexit,是爲了當程序發生異常時,來釋放monitor的。否則其他線程將無法獲得monitor。
 
public void synchronizedAdd();
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter
       4: getstatic     #3                  // Field i:I
       7: iconst_1
       8: iadd
       9: putstatic     #3                  // Field i:I
      12: aload_1
      13: monitorexit
      14: goto          22
      17: astore_2
      18: aload_1
      19: monitorexit
      20: aload_2
      21: athrow
      22: return
    Exception table:
       from    to  target type
           4    14    17   any
          17    20    17   any
 
 
synchronized同步方法原理:
 
同步方法,使用的是隱式同步方法,不通過指令來開始或結束同步(持有或釋放monitor)。
同步方法通過方法常量池方法表結構中的ACC_SYNCHRONIZED標識區分是否爲同步方法,如果在方法調用時發現ACC_SYNCHRONIZED被設置了,那麼線程首先會獲得monitor,其他線程無法獲得這個monitor並處於阻塞狀態。當方法執行完成時,釋放monitor。如果在同步方法期間拋出異常,並且沒有捕捉處理,那麼該線程所持有的monitor在拋到同步方法之外時釋放。
同步方法指令:
 
  public synchronized void synchronizedAdd();
    Code:
       0: getstatic     #2                  // Field i:I
       3: iconst_1
       4: iadd
       5: putstatic     #2                  // Field i:I
       8: return
 
 
現在在實際的編程中已經幾乎很少使用synchronized關鍵字了,可以用更多的方式或封裝類來替代。
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章