Synchronized關鍵字

1、Synchronized的做用

做用:可以保證在 同一時刻最多隻有 一個線程執行該代碼,以達到保證併發安全的效果
public class DisappearRequest implements Runnable{
    
    static DisappearRequest dr = new DisappearRequest();
    
    static int count = 0;
    
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(dr);
        Thread t2 = new Thread(dr);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count="+count);
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            count++;
        }
    }

}
// 結果count小於20000(線程不安全)

2、Synchronized的兩個用法

1. 對象鎖:包括同步代碼塊鎖(本身指定鎖對象)和方法鎖(默認鎖對象爲this當前實例對象)面試

1.1 代碼塊形式:手動指定鎖對象
public class SynchronizedObjectCodeBlock implements Runnable {
    
    static SynchronizedObjectCodeBlock instance = 
            new SynchronizedObjectCodeBlock();
    
    
    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        
        t1.start();
        t2.start();
        
        while(t1.isAlive() || t2.isAlive()){
            
        }
        System.out.println("finished");
    }

    @Override
    public void run() {
        // 兩個線程串行操做
        synchronized(this){
            System.out.println("我是對象鎖的代碼塊形式。我叫:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "運行結束");
        }
    }

}
public class SynchronizedObjectCodeBlock implements Runnable {
    
    static SynchronizedObjectCodeBlock instance = 
            new SynchronizedObjectCodeBlock();
    
    Object lock1 = new Object();
    Object lock2 = new Object();
    
    
    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        
        t1.start();
        t2.start();
        
        while(t1.isAlive() || t2.isAlive()){
            
        }
        System.out.println("finished");
    }

    @Override
    public void run() {
        synchronized(lock1){
            System.out.println("我是對象鎖的代碼塊形式。我叫:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "運行結束");
        }
        
        synchronized(lock2){
            System.out.println("我是對象鎖的代碼塊形式。我叫:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "運行結束");
        }
    }

}
// CountDownLatch、信號量解決線程同步問題。
1.2 方法鎖形式:synchronized修飾普通方法,鎖對象默認爲this
普通方法鎖
public class SynchronizedMethodLock implements Runnable{

    static SynchronizedMethodLock instance = 
            new SynchronizedMethodLock();
    
    @Override
    public void run() {
        sync();
    }
    
    // 普通方法鎖(不能是靜態方法。鎖對象默認是this)
    public synchronized void sync(){
        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(instance);
        Thread t2 = new Thread(instance);
        
        t1.start();
        t2.start();
        
        while(t1.isAlive() || t2.isAlive()){
            
        }
        System.out.println("finished");
    }
    
    

}

2. 類鎖:指synchronized修飾靜態方法或指定鎖爲Class對象。Java類可能有不少個對象,但只有一個Class對象。
所謂的類鎖,不過是Class對象的鎖而已。
用法和效果:類鎖只能在同一時刻被一個對象擁有,其餘對象會被阻塞
對象鎖若是不一樣的實例建立出來的,互相鎖是不受影響的,你能夠運行我也能夠運行,並行運行,可是類鎖只有一個能夠運行。安全

2.1. synchronized加在static方法上。 場景:若是須要在全局狀況下同步該方法,而不是一個小範圍層面,則應該用這種形式去作同步保護。
public class SynchronizedMethodLock implements Runnable{

    // 建立兩個實例對象
    static SynchronizedMethodLock instance1 = 
            new SynchronizedMethodLock();
    static SynchronizedMethodLock instance2 = 
            new SynchronizedMethodLock();
    
    @Override
    public void run() {
        sync();
    }
    
    // 建立兩個實例對象,若是不是static方法的話,則並行操做,不然串行執行。
    public static synchronized void sync(){
        System.out.println("我是類鎖的第一種形式:static形式。我叫:" + 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(instance1);
        Thread t2 = new Thread(instance2);
        
        t1.start();
        t2.start();
        
        while(t1.isAlive() || t2.isAlive()){
            
        }
        System.out.println("finished");
    }
    
    

}
2.2. synchronized(*.class)代碼塊
public class SynchronizedMethodLock implements Runnable{

    // 建立兩個實例對象
    static SynchronizedMethodLock instance1 = 
            new SynchronizedMethodLock();
    static SynchronizedMethodLock instance2 = 
            new SynchronizedMethodLock();
    
    @Override
    public void run() {
        sync();
    }
    
    // 建立兩個實例對象,若是不是static方法的話,則並行操做,不然串行執行。
    public void sync(){
        // 若是是this的話則並行執行,Class對象則串行執行
        synchronized(SynchronizedMethodLock.class){
            System.out.println("我是類鎖的第二種形式:synchronized(*.class)形式。我叫:" + 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(instance1);
        Thread t2 = new Thread(instance2);
        
        t1.start();
        t2.start();
        
        while(t1.isAlive() || t2.isAlive()){
            
        }
        System.out.println("finished");
    }
    
    

}

3、線程不安全致使請求丟失問題解決

場景:前面【 1、Synchronized的做用 】中的demo計數場景。
以下四種方式解決(結果均爲20000):
3.1
@Override
    public synchronized void run() {
        for (int i = 0; i < 10000; i++) {
            count++;
        }
    }
3.2
@Override
    public void run() {
        synchronized (this){
            for (int i = 0; i < 10000; i++) {
                count++;
            }
        }
    }
3.3
@Override
    public void run() {
        synchronized(DisappearRequest.class){
            for (int i = 0; i < 10000; i++) {
                count++;
            }
        }
    }
3.4
@Override
    public void run() {
        add();
    }
    
    public static synchronized void add(){
        for (int i = 0; i < 10000; i++) {
            count++;
        }
    }

4、多線程訪問同步方法的7中狀況

4.一、 兩個線程同時訪問一個對象的同步方法多線程

兩個線程爭搶的是同一把鎖,線程間相互等待,只能有一個線程持有該鎖
public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while(t1.isAlive() || t2.isAlive()){
        }
        System.out.println("finished");
    }

4.二、 兩個線程訪問的是兩個對象的同步方法併發

建立兩個實例對象,若是不是static方法的話,則並行操做,不然串行執行,這兩個實例真正採用的鎖對象不是同一個,因此不會被幹擾。
// 建立兩個實例對象,若是不是static方法的話,則並行操做,不然串行執行。
    public void sync(){
        // 若是是this的話則並行執行,指向的是不一樣的實例對象,若爲Class對象則串行執行
        synchronized(this){
        // TO DO...
        }
    }
public static void main(String[] args) {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance2);
        t1.start();
        t2.start();
        while(t1.isAlive() || t2.isAlive()){    
        }
        System.out.println("finished");
    }

4.三、 兩個線程訪問的synchronized的靜態方法app

若是兩個線程訪問的是同一個對象的同步方法則串行執行,若是訪問的是不一樣對象的同步方法,若該方法是非靜態static方法則並行執行,不然兩個線程訪問的鎖對象爲同一把鎖,串行執行。
public static synchronized void sync(){
    // TO DO...
    }
    
    public static void main(String[] args) {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance2);
        
        t1.start();
        t2.start();
        
        while(t1.isAlive() || t2.isAlive()){
            
        }
        System.out.println("finished");
    }

4.4 同時訪問同步方法合肥同步方法jvm

非同步方法不受到影響

4.5 訪問同一個對象的不一樣的普通同步方法ide

同一個對象,兩個同步方法拿到的this是同樣的,同一把鎖,因此串行執行

4.6 同時訪問靜態synchronized和非靜態synchronized方法函數

兩個線程指定的鎖對象不是同一把鎖,因此鎖之間不衝突,並行執行
// 建立兩個實例對象,若是不是static方法的話,則並行操做,不然串行執行。
    public static synchronized void sync(){
        // 若是是this的話則並行執行,Class對象則串行執行
        System.out.println("我是靜態方法:synchronized(*.class)形式。我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "運行結束");
    }
    
    // 建立兩個實例對象,若是不是static方法的話,則並行操做,不然串行執行。
    public synchronized void sync1(){
        // 若是是this的話則並行執行,Class對象則串行執行
        System.out.println("我是非靜態方法:synchronized(*.class)形式。我叫:" + 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(instance1);
        Thread t2 = new Thread(instance1);
        
        t1.start();
        t2.start();
        
        while(t1.isAlive() || t2.isAlive()){
            
        }
        System.out.println("finished");
    }

4.7 方法拋異常後,是否會釋放鎖性能

拋出異常以後jvm會釋放鎖,後面的線程會進入同步方法。
// 建立兩個實例對象
    static SynchronizedMethodLock instance1 = 
            new SynchronizedMethodLock();
    static SynchronizedMethodLock instance2 = 
            new SynchronizedMethodLock();
    
    @Override
    public void run() {
        if(Thread.currentThread().getName().equals("Thread-0")){
            sync();
        }else{
            sync1();
        }
    }
    
    // 建立兩個實例對象,若是不是static方法的話,則並行操做,不然串行執行。
    public synchronized void sync(){
        // 若是是this的話則並行執行,Class對象則串行執行
        System.out.println("我是方法1:我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        throw new RuntimeException();
//        System.out.println(Thread.currentThread().getName() + "運行結束");
    }
    
    // 建立兩個實例對象,若是不是static方法的話,則並行操做,不然串行執行。
    public synchronized void sync1(){
        // 若是是this的話則並行執行,Class對象則串行執行
        System.out.println("我是方法2:我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "運行結束");
    }
    
    public static void main(String[] args) {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance1);
        
        t1.start();
        t2.start();
        
        while(t1.isAlive() || t2.isAlive()){
            
        }
        System.out.println("finished");
    }

clipboard.png

5、synchronized的性質

【5.1 可重入】:指的是同一線程的外層函數得到鎖以後,內層函數能夠直接再次獲取該鎖
好處:避免死鎖,提高封裝性
好比:如今有兩個均被synchronized修飾的方法f1和f2,此時線程A執行到了f1,而且得到了這把鎖,因爲方法二f2也是synchronized修飾(也就是說要執行f2必須拿到f2的鎖),若是synchronized不具有可重入性,此時線程A只是拿到了f1的鎖,沒有拿到f2的鎖,因此線程A即想拿到f2的鎖又不釋放f1鎖,那麼就會形成永遠等待,即死鎖,因此synchronized是具備可重入性。this

【可重入粒度以下】

5.1.1 證實同一個方法是可重入的(遞歸)

5.1.2 證實可重入不要求是同一個方法

public synchronized void method1(){
        System.out.println("我是method1");
        method2();
    }
    public synchronized void method2(){
        System.out.println("我是method2");
    }
5.1.3 證實同可重入不要求是同一個類中的
public class SyncSuperClass{
        public synchronized void doSomething(){
            System.out.println("我是父類方法");
        }
    }
    
    class TestClass extends SyncSuperClass{
        public synchronized void doSomething(){
            System.out.println("我是子類方法");
            super.doSomething();
        }
    }

【5.2 不可中斷性】

一旦這個鎖已經被別人得到了,若是我還想得到,我只能選擇等待或者阻塞,直到別的線程釋放這個鎖。若是別人永遠不釋放鎖,那麼我只能永遠等下去。
Lock類:相比之下,Lock類,能夠擁有中斷的能力,第一點,若是我以爲我等的時間太長了,有權中斷如今已經獲取到鎖的線程的執行;第二點,若是我以爲等待時間太長了不想等了,能夠退出。
Lock lock = new ReentrantLock(); 
    // 下面這兩種形式的鎖是等價的
    public synchronized void method1(){
        System.out.println("我是Synchronized形式的鎖");
    }
    
    public void method2(){
        lock.lock();
        try{
            System.out.println("我是Lock形式的鎖");
        }finally{
            lock.unlock();
        }
    }

6、synchronized的缺陷

【6.1 效率低】

鎖的釋放狀況少,試圖得到鎖時不能設定超時、不能中斷一個正在試圖得到鎖的線程。
6.1.一、 當一個線程得到了對應的sync鎖的時候,其餘線程只能等待我釋放以後才能獲取該鎖。
6.1.二、 只有兩種狀況才釋放鎖:1.執行完了這段代碼,2.發生異常自動釋放鎖
6.1.三、 不能中斷,可是Lock是有中斷能力的

【6.2 不夠靈活(如:讀寫鎖比較靈活:讀的時候不加鎖,寫才加鎖)】

加鎖和釋放鎖的時機單一,每一個鎖僅有單一的條件(某個對象),多是不夠的

【6.3 沒法知道是否成功獲取到鎖】

7、Lock鎖經常使用方法

Lock lock = new ReentrantLock(); // 非公平鎖
    // Lock lock = new ReentrantLock(true); // 公平鎖
    // Lock lock = new ReentrantReadWriteLock();
        
        lock.lock();
        lock.unlock();
        lock.tryLock(); // 獲取鎖
        lock.tryLock(10, TimeUnit.SECONDS);

8、常見面試題

8.1 使用synchroinzed注意點:鎖對象不能爲空、做用域不宜過大、避免死鎖
注:一個對象做爲鎖對象,這個對象必須是被new過的,或者是被其餘方法建立好的,而不是一個空對象,由於鎖的信息保存在對象頭中的。

8.2 如何選擇Lock和synchronized關鍵字
儘可能使用concurrent包下的CountDownLatch或者atomic包,或者信號量

9、思考

9.1 多個線程等待同一個synchronized鎖的時候,JVM如何選擇下一個獲取鎖的是哪一個縣城? 9.2 synchronized使得同時只能有一個線程能夠執行,性能較差,有什麼辦法能夠提高性能? 9.3 我想更靈活的控制鎖的獲取和釋放(如今釋放鎖的時機都被規定死了),怎麼辦? 9.4 什麼是鎖的升級、降級?什麼事JVM裏的偏斜所、輕量級鎖。重量級鎖?
相關文章
相關標籤/搜索