JAVA之Synchronized和Lock

目錄

什麼是線程安全,如何保證線程安全

synchronized的三種應用方式

Java對象頭與Monitor

鎖升級的過程

monitorenter/monitorexit實現原理

1、公平鎖

2、非公平鎖

3、ReentrantLock

4、synchronized

5、可重入鎖(又名遞歸鎖)

6、獨佔鎖(寫鎖)、共享鎖(讀鎖)、互斥鎖

7、自旋鎖

問題:synchronized和lock有什麼區別?用新的Lock有什麼好處?你舉例說說

 

 

 


什麼是線程安全,如何保證線程安全

  • 線程安全:就是多線程訪問時,採用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,其他線程不能進行訪問直到該線程讀取完,其他線程纔可使用。不會出現數據不一致或者數據污染。 線程不安全就是不提供數據訪問保護,有可能出現多個線程先後更改數據造成所得到的數據是髒數據。
  • 如何保證
    • 使用線程安全的類;
    • 使用synchronized同步代碼塊,或者用Lock鎖;
    • 多線程併發情況下,線程共享的變量改爲方法局部級變量;

 

synchronized的三種應用方式

synchronized關鍵字最主要有以下3種應用方式,下面分別介紹

  • 修飾實例方法,作用於當前實例加鎖,進入同步代碼前要獲得當前實例的鎖
  • 修飾靜態方法,作用於當前類對象加鎖,進入同步代碼前要獲得當前類對象的鎖
  • 修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。
     

Java對象頭與Monitor

在JVM中,對象在內存中的佈局分爲三塊區域:對象頭、實例數據和對齊填充。

  1. 對象頭:它實現synchronized的鎖對象的基礎,synchronized使用的鎖對象是存儲在Java對象頭裏的,jvm中採用2個字來存儲對象頭(如果對象是數組則會分配3個字,多出來的1個字記錄的是數組長度)
  2. 實例變量:存放類的屬性數據信息,包括父類的屬性信息,如果是數組的實例部分還包括數組的長度,這部分內存按4字節對齊。
  3. 填充數據:由於虛擬機要求對象起始地址必須是8字節的整數倍。填充數據不是必須存在的,僅僅是爲了字節對齊,這點了解即可。

鎖升級的過程

  • 偏向鎖:是指一段同步代碼一直被一個線程所訪問,那麼該線程會自動獲取鎖。降低獲取鎖的代價。其中識別是不是同一個線程一隻獲取鎖的標誌是在上面提到的對象頭Mark Word(標記字段)中存儲的。
  • 輕量級鎖:是指當鎖是偏向鎖的時候,被另一個線程所訪問,偏向鎖就會升級爲輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,提高性能。
  • 重量級鎖:是指當鎖爲輕量級鎖的時候,另一個線程雖然是自旋,但自旋不會一直持續下去,當自旋一定次數的時候,還沒有獲取到鎖,就會進入阻塞,該鎖膨脹爲重量級鎖。重量級鎖會讓其他申請的線程進入阻塞,性能降低。這時候也就成爲了原始的Synchronized的實現。

 

monitorenter/monitorexit實現原理

我們先看一下JVM規範是怎麼定義monitorenter和monitorexit的
(1) monitorenter:
每一個對象都會和一個監視器monitor關聯。監視器被佔用時會被鎖住,其他線程無法來獲取該monitor。
當JVM執行某個線程的某個方法內部的monitorenter時,它會嘗試去獲取當前對象對應的monitor的所有權。其過程如下:

  1. 若monior的進入數爲0,線程可以進入monitor,並將monitor的進入數置爲1。當前線程成爲monitor的owner(所有者)
  2. 若線程已擁有monitor的所有權,允許它重入monitor,並遞增monitor的進入數
  3. 若其他線程已經佔有monitor的所有權,那麼當前嘗試獲取monitor的所有權的線程會被阻塞,直到monitor的進入數變爲0,才能重新嘗試獲取monitor的所有權。

(2) monitorexit:

  1. 能執行monitorexit指令的線程一定是擁有當前對象的monitor的所有權的線程。
  2. 執行monitorexit時會將monitor的進入數減1。當monitor的進入數減爲0時,當前線程退出monitor,不再擁有monitor的所有權,此時其他被這個monitor阻塞的線程可以嘗試去獲取這個monitor的所有權。

 

1、公平鎖

是指多個線程按照申請鎖的順序來獲取鎖,類似排隊打飯,先來後到。

2、非公平鎖

是指多個線程獲取鎖的順序並不是按照申請鎖的順序,有可能後申請的線程比先申請的線程優先獲取鎖,在高併發的情況下,有可能造成優先級反轉或者飢餓現象

兩者區別:

公平鎖就是很公平,在併發環境中,每個線程在獲取鎖時會險查看此鎖維護的等待隊列,如果爲空,或者當前線程是等待隊列的第一個,就佔有鎖。否則就會加入到等待隊列中,以後會按照FIFO的規則從隊列中取到自己。

非公平鎖比較粗魯,上來就直接嘗試佔有鎖。如果嘗試失敗,就才喲個類似公平鎖的那種方式。

3、ReentrantLock

併發包中ReentrantLock的創建可以指定構造函數的boolean類型來得到公平鎖或非公平鎖,默認是非公平鎖。非公平鎖有點在於吞吐量比公平鎖大。

1、問題:是否能多次 lock 和 unlock?

回答:允許,只要成對出現,程序可以正常運行。

2、問題:1次lock,2次unlock會報錯嗎?

答案:報錯,IllegalMonitorStateException

Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
	at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
	at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
	at com.example.demo.Phone.get(ReenterLockDemo.java:35)
	at com.example.demo.Phone.run(ReenterLockDemo.java:25)
	at java.lang.Thread.run(Thread.java:748)

3、問題:2次lock,1次unlock會報錯嗎?

答案:不會,但是死鎖

4、synchronized

synchronized而言,也是一種非公平鎖。

5、可重入鎖(又名遞歸鎖)

指的是同一線程外層函數獲得鎖後,內層遞歸函數仍然能獲取該鎖的代碼,在同一線程在外層方法獲取鎖的時候,進入內層方法會自動獲取鎖。

也就是說,線程可以進入任何一個它已經擁有的鎖所同步着的代碼塊。

ReentrantLock和synchronized默認是非公平的可重入鎖。

可重入鎖最大的作用就是避免死鎖。

例子:證明synchronized是可重入鎖

public class ReenterLockDemo {

    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "T1").start();

        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "T2").start();

    }
}

class Phone implements Runnable{

    public synchronized void sendSMS()throws Exception{

        System.out.println(Thread.currentThread().getId() + "\tinvoked sendSMS()");
        this.sendEmain();
    }

    public synchronized void sendEmain()throws Exception{
        System.out.println(Thread.currentThread().getId() + "\tinvoked sendEmain()");
    }
}

例子:證明ReentrantLock是可重入鎖

 

6、獨佔鎖(寫鎖)、共享鎖(讀鎖)、互斥鎖

多個線程同時讀一個資源類沒有任何問題,所以爲了滿足併發性量,讀取共享資源應該可以同時進行,但是,如果有一個線程想去寫共享資源類,就不應該再有其他線程可以對該資源類進行讀或寫。

小總結:

讀-讀能共享

讀-寫不能共享

寫-寫不能共享

寫操作:原子+獨佔,整個過程必須是一個完整的統一體,中間不許被分割,被打斷。

 

例子:請手寫一個讀寫鎖

class MyCache{
    private volatile Map<String, Object> map = new HashMap<>();
    private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void put(String key, Object value){
        rwLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t正在寫入" + key);
            try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "\t寫入完成");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    public void get(String key){
        rwLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t正在讀取" + key);
            try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
            Object result = map.get(key);
            System.out.println(Thread.currentThread().getName() + "\t讀取完成" + result);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rwLock.readLock().unlock();
        }
    }

    public void clearMap(){
        map.clear();
    }
}

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        for(int i = 1; i <=5; i++){
            final int tempInt = i;
            new Thread(() -> {
                myCache.put(""+ tempInt, "" + tempInt);
            }, "" + i).start();
        }

        for(int i = 1; i <=5; i++){
            final int tempInt = i;
            new Thread(() -> {
                myCache.get(""+ tempInt);
            }, "" + i).start();
        }
    }
}

7、自旋鎖

是指嘗試獲取鎖的線程不會立即阻塞,而是採用循環的方式去嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗,缺點是循環會消耗CPU。

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        return var5;
    }

例子:請手寫一個自旋鎖

public class SpinLockDemo {
    // 原子引用線程
    AtomicReference<Thread> atomicReference = new AtomicReference<>();
    public void myLock(){
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "\tcome on");
        while (!atomicReference.compareAndSet(null, thread)){
//            System.out.println(Thread.currentThread().getName() + "\t獲取鎖失敗");
        }
        System.out.println(Thread.currentThread().getName() + "\t獲取鎖成功");
    }

    public void myUnLock(){
        Thread  thread = Thread.currentThread();
        atomicReference.compareAndSet(thread, null);
        System.out.println(Thread.currentThread().getName() + "\tinvoked myUnLock()");
    }

    public static void main(String[] args) {
        SpinLockDemo demo = new SpinLockDemo();
        new Thread(() -> {
            demo.myLock();
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            demo.myUnLock();
        }, "AA").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            demo.myLock();
            demo.myUnLock();
        }, "BB").start();
    }
}

 

問題:synchronized和lock有什麼區別?用新的Lock有什麼好處?你舉例說說

1、原始構成

synchronized是關鍵字屬於JVM層面。

  • monitorenter(底層是通過monitor對象來完成,其實wait/notify等方法也依賴於monitor對象,只有在同步塊或方法中才能調用wait/notify等方法)
  • monitorexit

Lock是具體類(java.util.concurrent.locks.Lock)是api層面的鎖

2、使用方法

  • synchronized不需要用戶去手動釋放鎖,當synchronized代碼塊執行完成後系統會自動讓線程釋放對鎖的佔用
  • ReentrantLock則需要用戶去手動釋放鎖,弱沒有主動釋放鎖,就有可能導致出現死鎖現象。需要lock()、unlock()方法配合try、finally語句塊來完成

3、等待是否可中斷

  • synchronized不可中斷,除非拋出異常或者正常運行完成
  • ReentrantLock可中斷,1、設置超時方法tryLock(long timeout,TimeUnit unit)
  •                                    2、lockInterruptibly()放代碼塊中,調用interrupt()方法可中斷

4、加鎖是否公平

  • synchronized 非公平鎖
  • ReentrantLock 默認非公平鎖,構造方法可以傳入boolean值,true爲公平鎖,false爲非公平鎖

5、鎖綁定多個條件Condition

  • synchronized沒有
  • ReentrantLock用來實現分組喚醒需要喚醒的線程們,可以精確喚醒,而不是像synchronized要麼隨機喚醒一個,要麼全部喚醒
     
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章