Synchronized 詳解


為了方便記憶,將鎖做如下的分類

一、對象鎖

包括方法鎖(默認鎖對象為this,當前實例對象)和同步代碼塊鎖(自己指定鎖對象)

1.代碼塊形式:手動指定鎖定對象,也可是是this,也可以是自定義的鎖

public class SynchronizedObjectLock implements Runnable {
    static SynchronizedObjectLock instence = new SynchronizedObjectLock();

    @Override
    public void run() {
        // 同步代碼塊形式——鎖為this,兩個線程使用的鎖是一樣的,線程1必須要等到線程0釋放了該鎖后,才能執行
        synchronized (this) {
            System.out.println("我是線程" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "結束");
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instence);
        Thread t2 = new Thread(instence);
        t1.start();
        t2.start();
    }
}

輸出結果:

  我是線程Thread-0
  Thread-0結束
  我是線程Thread-1
  Thread-1結束

public class SynchronizedObjectLock implements Runnable {
    static SynchronizedObjectLock instence = new SynchronizedObjectLock();
    // 創建2把鎖
    Object block1 = new Object();
    Object block2 = new Object();

    @Override
    public void run() {
        // 這個代碼塊使用的是第一把鎖,當他釋放后,后面的代碼塊由於使用的是第二把鎖,因此可以馬上執行
        synchronized (block1) {
            System.out.println("blocl1鎖,我是線程" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("blocl1鎖,"+Thread.currentThread().getName() + "結束");
        }

        synchronized (block2) {
            System.out.println("blocl2鎖,我是線程" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("blocl2鎖,"+Thread.currentThread().getName() + "結束");
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instence);
        Thread t2 = new Thread(instence);
        t1.start();
        t2.start();
    }

輸出結果:

  blocl1鎖,我是線程Thread-0
  blocl1鎖,Thread-0結束
  blocl2鎖,我是線程Thread-0  // 可以看到當第一個線程在執行完第一段同步代碼塊之后,第二個同步代碼塊可以馬上得到執行,因為他們使用的鎖不是同一把
  blocl1鎖,我是線程Thread-1
  blocl2鎖,Thread-0結束
  blocl1鎖,Thread-1結束
  blocl2鎖,我是線程Thread-1
  blocl2鎖,Thread-1結束

2.方法鎖形式:synchronized修飾普通方法,鎖對象默認為this

public class SynchronizedObjectLock implements Runnable {
    static SynchronizedObjectLock instence = new SynchronizedObjectLock();

    @Override
    public void run() {
        method();
    }

    public synchronized void method() {
        System.out.println("我是線程" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "結束");
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instence);
        Thread t2 = new Thread(instence);
        t1.start();
        t2.start();
    }
}

輸出結果:

  我是線程Thread-0
  Thread-0結束
  我是線程Thread-1
  Thread-1結束

 

二、類鎖

指synchronize修飾靜態的方法或指定鎖對象為Class對象

1.synchronize修飾靜態方法

public class SynchronizedObjectLock implements Runnable {
    static SynchronizedObjectLock instence1 = new SynchronizedObjectLock();
    static SynchronizedObjectLock instence2 = new SynchronizedObjectLock();

    @Override
    public void run() {
        method();
    }

    // synchronized用在普通方法上,默認的鎖就是this,當前實例
    public synchronized void method() {
        System.out.println("我是線程" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "結束");
    }

    public static void main(String[] args) {
        // t1和t2對應的this是兩個不同的實例,所以代碼不會串行
        Thread t1 = new Thread(instence1);
        Thread t2 = new Thread(instence2);
        t1.start();
        t2.start();
    }
}

輸出結果:

  我是線程Thread-0
  我是線程Thread-1
  Thread-1結束
  Thread-0結束

public class SynchronizedObjectLock implements Runnable {
    static SynchronizedObjectLock instence1 = new SynchronizedObjectLock();
    static SynchronizedObjectLock instence2 = new SynchronizedObjectLock();

    @Override
    public void run() {
        method();
    }

    // synchronized用在靜態方法上,默認的鎖就是當前所在的Class類,所以無論是哪個線程訪問它,需要的鎖都只有一把
    public static synchronized void method() {
        System.out.println("我是線程" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "結束");
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instence1);
        Thread t2 = new Thread(instence2);
        t1.start();
        t2.start();
    }
}

輸出結果:

  我是線程Thread-0
  Thread-0結束
  我是線程Thread-1
  Thread-1結束

2.synchronized指定鎖對象為Class對象

public class SynchronizedObjectLock implements Runnable {
    static SynchronizedObjectLock instence1 = new SynchronizedObjectLock();
    static SynchronizedObjectLock instence2 = new SynchronizedObjectLock();

    @Override
    public void run() {
        // 所有線程需要的鎖都是同一把
        synchronized(SynchronizedObjectLock.class){
            System.out.println("我是線程" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "結束");
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instence1);
        Thread t2 = new Thread(instence2);
        t1.start();
        t2.start();
    }
}

輸出結果:
我是線程Thread-0
Thread-0結束
我是線程Thread-1
Thread-1結束

 

三、思考

1.兩個線程同時訪問1個對象的同步方法

2.兩個線程同時訪問2個對象的同步方法

3.兩個線程訪問的是synchronized靜態方法

4.兩個線程同時訪問同步(被synchronized修飾)和非同步(未被snychronized修飾)方法

5.兩個線程同時訪問1個對象的不同的普通同步方法

6.兩個線程同時訪問一個靜態的synchronized方法和非靜態的synchronized方法

7.方法拋出異常后,會釋放鎖嗎?

核心思想:

1.一把鎖只能同時被一個線程獲取,沒有難道鎖的線程只能等待(對應上面的1,5)

2.每個實例都對應有自己的一把鎖(this),不同實例之間互不影響;例外:鎖對象是*.class以及synchronized修飾的是static方法的時候,所有對象公用同一把鎖(對應上面的2,3,4,6)

3.synchronized修飾的方法,無論方法正常執行完畢還是拋出異常,都會釋放鎖(對應上面的7)

 

四、synchronized的性質

1.可重入性

概念:指同一個線程外層函數獲取到鎖之后,內層函數可以直接使用該鎖

好處:避免死鎖,提升封裝性(如果不可重入,假設method1拿到鎖之后,在method1中又調用了method2,如果method2沒辦法使用method1拿到的鎖,那method2將一直等待,但是method1由於未執行完畢,又無法釋放鎖,就導致了死鎖,可重入正好避免這這種情況)

粒度:線程而非調用(用3中情況來說明與pthread的區別)

1)情況1:證明同一個方法是可重入的(遞歸)

public class SynchronizedDemo2 {
    int a = 0;

    public static void main(String[] args) {
        new SynchronizedDemo2().method1();
    }

    public synchronized void method1() {
        System.out.println("a=" + a);
        if (a == 0) {
            a++;
            method1();
        }
    }
}

輸出結果:

  a=0
  a=1

 

2)情況2:證明可重入不要求是同一個方法

public class SynchronizedDemo2 {
    public static void main(String[] args) {
        new SynchronizedDemo2().method1();
    }

    public synchronized void method1() {
        System.out.println("method1");
        method2();
    }

    public synchronized void method2() {
        System.out.println("method2");
    }
}

輸出結果:

  method1
  method2

 

3)情況3:證明可重入不要求是同一個類中

public class SynchronizedDemo2 {
    public synchronized void method1() {
        System.out.println("父類method1");
    }
}

class SubClass extends SynchronizedDemo2 {
    public synchronized void method1() {
        System.out.println("子類method1");
        super.method1();
    }

    public static void main(String[] args) {
        new SubClass().method1();
    }
}

輸出結果:

  子類method1
  父類method1

 

2.不可中斷性

概念:如果這個鎖被B線程獲取,如果A線程想要獲取這把鎖,只能選擇等待或者阻塞,直到B線程釋放這把鎖,如果B線程一直不釋放這把鎖,那么A線程將一直等待。

相比之下,未來的Lock類,可以擁有中斷的能力(如果一個線程等待鎖的時間太長了,有權利中斷當前已經獲取鎖的線程的執行,也可以退出等待)

 

五、深入原理

1.加鎖和釋放鎖的原理:現象、時機(內置鎖this)、深入JVM看字節碼(反編譯看monitor指令)

Lock lock = new ReentrantLock();

public synchronized void method1() {
  System.out.println("synchronized method1");
}

public void method2() {
  lock.lock();
  try {
    System.out.println("lock method2");
  } finally {
    lock.unlock();
  }
}
method1與method2等價,
synchronized相當於先獲取鎖,執行結束/拋出異常后,釋放鎖。

深入JVM看字節碼,創建如下的代碼:

public class SynchronizedDemo2 {
    Object object = new Object();

    public void method1() {
        synchronized (object) {

        }
    }
}
使用javac命令進行編譯生成.class文件

>javac SynchronizedDemo2.java

使用javap命令反編譯查看.class文件的信息

>javap -verbose SynchronizedDemo2.class
得到如下的信息:

關注紅色方框里的monitorenter和monitorexit即可。

Monitorenter和Monitorexit指令,會讓對象在執行,使其鎖計數器加1或者減1。每一個對象在同一時間只與一個monitor(鎖)相關聯,而一個monitor在同一時間只能被一個線程獲得,一個對象在嘗試獲得與這個對象相關聯的Monitor鎖的所有權的時候,monitorenter指令會發生如下3中情況之一:

1)monitor計數器為0,意味着目前還沒有被獲得,那這個線程就會立刻獲得然后把鎖計數器+1,一旦+1,別的線程再想獲取,就需要等待

2)如果這個monitor已經拿到了這個鎖的所有權,又重入了這把鎖,那鎖計數器就會累加,變成2,並且隨着重入的次數,會一直累加

3)這把鎖已經被別的線程獲取了,等待鎖釋放

monitorexit指令:釋放對於monitor的所有權,釋放過程很簡單,就是講monitor的計數器減1,如果減完以后,計數器不是0,則代表剛才是重入進來的,當前線程還繼續持有這把鎖的所有權,如果計數器變成0,則代表當前線程不再擁有該monitor的所有權,即釋放鎖。

 

2.可重入原理:加鎖次數計數器

jvm會負責跟蹤對象被加鎖的次數

線程第一次獲得所,計數器+1,當鎖重入的時候,計數器會遞增

當任務離開的時候(一個同步代碼塊的代碼執行結束),計數器會減1,當減為0的時候,鎖被完全釋放。

 

3.保證可見性的原理:內存模型

 訪問鏈接 https://www.cnblogs.com/xyabk/p/10894384.html

 

六、synchronized的缺陷

效率低:鎖的釋放情況少,只有代碼執行完畢或者異常結束才會釋放鎖;試圖獲取鎖的時候不能設定超時,不能中斷一個正在使用鎖的線程,相對而言,Lock可以中斷和設置超時

不夠靈活:加鎖和釋放的時機單一,每個鎖僅有一個單一的條件(某個對象),相對而言,讀寫鎖更加靈活

無法知道是否成功獲得鎖,相對而言,Lock可以拿到狀態,如果成功獲取鎖,....,如果獲取失敗,.....

 

七、Lock對synchronized的彌補

Lock類這里不做過多解釋,主要看上面紅色方框里面的4個方法

lock():加鎖

unlock():解鎖

tryLock():嘗試獲取鎖,返回一個boolean值

tryLock(long,TimeUtil):嘗試獲取鎖,可以設置超時

 

八、注意

1.鎖對象不能為空,因為鎖的信息都保存在對象頭里

2.作用域不宜過大,影響程序執行的速度,控制范圍過大,編寫代碼也容易出錯

3.避免死鎖

4.在能選擇的情況下,既不要用Lock也不要用synchronized關鍵字,用java.util.concurrent包中的各種各樣的類,如果不用該包下的類,在滿足業務的情況下,可以使用synchronized關鍵,因為代碼量少,避免出錯

 

九、思考

1.多個線程等待同一個snchronized鎖的時候,JVM如何選擇下一個獲取鎖的線程?

2.Synchronized使得同時只有一個線程可以執行,性能比較差,有什么提升的方法?

3.我想更加靈活地控制鎖的釋放和獲取(現在釋放鎖和獲取鎖的時機都被規定死了),怎么辦?

4.什么是鎖的升級和降級?什么事JVM里的偏斜鎖、輕量級鎖、重量級鎖?

 


免責聲明!

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



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