synchronized 同步

同步問題的引出

需求:多個線程同時賣票

class MyThread implements Runnable {
    private int ticket = 10;
    @Override
    public void run() {
        while (this.ticket > 0)
        {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"還剩下"+this.ticket--+"票");
        }
    }
}
public class TestThread {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        new Thread(myThread,"黃牛1").start();
        new Thread(myThread,"黃牛2").start();
        new Thread(myThread,"黃牛3").start();
    }
}

Files\Java\jdk1.8.0_181\jre\lib\rt.jar;E:\Java\code\out\production\code" www.bit.java.TestThread
黃牛3還剩下10票
黃牛1還剩下9票
黃牛2還剩下10票
黃牛3還剩下8票
黃牛1還剩下6票
黃牛2還剩下7票
黃牛3還剩下5票
黃牛2還剩下3票
黃牛1還剩下4票
黃牛2還剩下2票
黃牛3還剩下1票
黃牛1還剩下0票
黃牛2還剩下-1票

Process finished with exit code 0

這個時候我們發現,票數竟然出現負數,這種問題我們稱之爲不同步操作。
不同步的唯一好處是處理速度快(多個線程併發執行)

同步處理

所謂的同步指的是所有的線程不是一起進入到方法中執行,而是按照順序一個一個進來。

synchronized處理同步問題

如果要想實現這把"鎖"的功能,可以採用關鍵字synchronized來處理。
使用synchronized關鍵字處理有兩種模式:同步代碼塊、同步方法

  • 同步代碼塊:在方法中使用synchronized(對象),一般可以鎖定當前對象this。表示同一時刻只有一個線程能夠進入同步代碼塊,但是多個線程可以同時進入方法。
class MyThread implements Runnable {
    private int ticket = 1000;
    @Override
    public void run() {
        for(int i = 0;i< 1000;i++){
            synchronized (this){
                if(this.ticket > 0)
                {
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"還剩下"+this.ticket--+"票");
                }
            }
        }
    }
}
public class TestThread {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        new Thread(myThread,"黃牛1").start();
        new Thread(myThread,"黃牛2").start();
        new Thread(myThread,"黃牛3").start();
    }
}
  • 同步方法:在方法上加synchronized,表示此時只有一個線程能夠進入同步方法。
class MyThread implements Runnable {
    private int ticket = 1000;
    @Override
    public void run() {
        for(int i = 0;i< 1000;i++){
            SellTicket(this.ticket);
        }
    }
    private synchronized void SellTicket(int ticket){
        if(this.ticket > 0)
        {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"還剩下"+this.ticket--+"票");
        }
    }
}
public class TestThread {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        new Thread(myThread,"黃牛1").start();
        new Thread(myThread,"黃牛2").start();
        new Thread(myThread,"黃牛3").start();
    }
}

關於synchronized的額外說明

先來看一段代碼:觀察synchronized鎖多對象

class Sync {
    public synchronized void test() {
        System.out.println("test方法開始,當前線程爲 "+Thread.currentThread().getName());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("test方法結束,當前線程爲 "+Thread.currentThread().getName());
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        Sync sync = new Sync();
        sync.test();
    }
}

public class TestThread {
    public static void main(String[] args) {
        for(int i = 0;i < 3;i++){
            new Thread(new MyThread()).start();
        }
    }
}

通過上述代碼以及運行結果我們可以發現,沒有看到synchronized起到作用,三個線程同時運行test()方法。

實際上,synchronized(this)以及非static的synchronized方法,只能防止多個線程同時執行同一個對象的同步代碼段。即synchronized鎖住的是括號裏的對象,而不是代碼。對於非static的synchronized方法,鎖的就是對象本身也就是this。
當synchronized鎖住一個對象後,別的線程如果也想拿到這個對象的鎖,就必須等待這個線程執行完成釋放鎖,才能再次給對象加鎖,這樣才達到線程同步的目的。即使兩個不同的代碼段,都要鎖同一個對象,那麼這兩個代碼段也不能在多線程環境下同時運行。
那麼,如果真要鎖住這段代碼,要怎麼做?

  • 鎖同一個對象
class Sync {
    public synchronized void test() {
        System.out.println("test方法開始,當前線程爲 "+Thread.currentThread().getName());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("test方法結束,當前線程爲 "+Thread.currentThread().getName());
    }
}

class MyThread extends Thread{
    private Sync sync;
    public MyThread(Sync sync){
        this.sync = sync;
    }
    @Override
    public void run() {
        this.sync.test();
    }
}

public class TestThread {
    public static void main(String[] args) {
        Sync sync = new Sync();
        for(int i = 0;i < 3;i++){
            new Thread(new MyThread(sync)).start();
        }
    }
}
  • 讓synchronized鎖這個類對應的Class對象—全局鎖
class Sync {
    public synchronized void test() {
        synchronized (Sync.class){
            System.out.println("test方法開始,當前線程爲 "+Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("test方法結束,當前線程爲 "+Thread.currentThread().getName());
        }
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        Sync sync = new Sync();
        sync.test();
    }
}

public class TestThread {
    public static void main(String[] args) {
        for(int i = 0;i < 3;i++){
            new Thread(new MyThread()).start();
        }
    }
}

上面代碼用synchronized(Sync.class)實現了全局鎖的效果。因此,如果要想鎖的是代碼段,鎖住多個對象的同一方法,使用這種全局鎖,鎖的是類而不是this。
static synchronized方法,static方法可以直接類名加方法名調用,方法中無法使用this,所以它鎖的不是this,而是類的Class對象,所以,static synchronized方法也相當於全局鎖,相當於鎖住了代碼段。

class Sync {
    public static synchronized void test() {
        System.out.println("test方法開始,當前線程爲 "+Thread.currentThread().getName());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("test方法結束,當前線程爲 "+Thread.currentThread().getName());

    }
}
class MyThread extends Thread{
    @Override
    public void run() {
        Sync sync = new Sync();
        sync.test();
    }
}

public class TestThread {
    public static void main(String[] args) {
        for(int i = 0;i < 3;i++){
            new Thread(new MyThread()).start();
        }
    }
}

synchronized實現原理

在這裏插入圖片描述

同步代碼塊底層實現

先來看一段簡單的代碼:

public class Test{    
	private static Object object = new Object();    
	public static void main(String[] args) {        
		synchronized (object) {            
			System.out.println("hello world");        
		}    
	} 
}

下面我們使用javap反編譯後看看生成的部分字節碼

 ...
 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: getstatic     #2                  // Field object:Ljava/lang/Object;
         3: dup
         4: astore_1
         5: monitorenter   // 瞪大眼睛看這裏!!! 
         6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         9: ldc           #4                  // String hello world
        11: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        14: aload_1
        15: monitorexit    // 瞪大眼睛看這裏!!! 
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit    // 瞪大眼睛看這裏!!! 
        22: aload_2
        23: athrow
        24: return
      Exception table:
         from    to  target type
             6    16    19   any
            19    22    19   any
...

執行同步代碼塊後首先要先執行monitorenter指令,退出的時候monitorexit指令。通過分析之後可以看出,使用 Synchronized進行同步,其關鍵就是必須要對對象的監視器monitor進行獲取,當線程獲取monitor後才能繼續往下執行,否則就只能等待。而這個獲取的過程是互斥的,即同一時刻只有一個線程可以獲取到該對象的monitor監視器。

上述字節碼中包含一個monitorenter指令以及多個monitorexit指令。這是因爲Java虛擬機需要確保所獲得的鎖在正常執行路徑,以及異常執行路徑上都能夠被解鎖。

同步方法底層實現

public synchronized void test();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #4                  // String hello world
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 471: 0
        line 472: 8

當使用synchronized標記方法時,字節碼會出現一個訪問標記ACC_SYNCHRONIZED。該標記表示在進入方法時,JVM需要進行monitorenter操作。在退出方法時,無論是正常返回,還是向調用者拋異常,JVM需要進行monitorexit操作。

這裏 monitorentermonitorexit 操作所對應的鎖對象是隱式的。對於實例方法來說,這兩個操作對應的鎖對象是 this;對於靜態方法來說,這兩個操作對應的鎖對象則是所在類的 Class 實例。

當JVM執行monitorenter時,如果目標對象monitor的計數器爲0,表示此時該對象沒有被其他線程所持有。此時JVM會將該鎖對象的持有線程設置爲當前線程,並且將monitor計數器+1。

在目標鎖對象的計數器不爲0的情況下,如果鎖對象的持有線程是當前線程,JVM可以將計數器再次+1(可重入鎖);否則需要等待,直到持有線程是釋放線程。

當執行monitorexit時,JVM需將鎖對象計數器-1。當計數器減爲0時,代表該鎖以及被釋放掉,喚醒所有正在等待的線程去競爭該鎖。

之所以採用這種計數器的方式,是爲了允許同一個線程重複獲取同一把鎖。舉個例子,如果一個 Java 類中擁有多個 synchronized 方法,那麼這些方法之間的相互調用,不管是直接的還是間接的,都會涉及對同一把鎖的重複加鎖操作。因此,我們需要設計這麼一個可重入的特性,來避免編程裏的隱式約束。

證明同一個對象的同步方法再次獲得鎖時不能獲取成功

class MyThread extends Thread{
    public synchronized void A(){
        while(true){}
    }
    public synchronized void B(){
        System.out.println(Thread.currentThread().getName()+",線程B...");
    }

    @Override
    public void run() {
        A();
        B();
    }
}

public class TestThread {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        new Thread(myThread,"A").start();
        new Thread(myThread,"B").start();
    }
}

證明同一個線程再次獲得鎖時可以獲取成功,而其他線程獲取鎖會阻塞(即鎖的可重入性)

class MyThread extends Thread{
    public synchronized void A(){
    	 while(true){
    	 	System.out.println(Thread.currentThread().getName()+",線程A...");
        	B();
    	 }
    }
    public synchronized void B(){
        System.out.println(Thread.currentThread().getName()+",線程B...");
    }

    @Override
    public void run() {
        A();
        B();
    }
}

public class TestThread {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        new Thread(myThread,"A").start();
        new Thread(myThread,"B").start();
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章