Java鎖的理解

  • 2020 年 3 月 11 日
  • 筆記

目錄:

  1.為什麼要使用鎖?

  2.鎖的類型?

 

1.為什麼要使用鎖?

  通俗的說就是多個執行緒,也可以說多個方法同時對一個資源進行訪問時,如果不加鎖會造成執行緒安全問題。舉例:比如有兩張票,但是有5個人進來買,買了一張票數就減1,在他們進門的時候會判斷是否還有票,但是在他們進門的那一刻,票還一張都沒有買走。但是他們都已經進門了,過了是否有票的校驗了,所以最後票數為被減成負3,顯然是不對的,因為票不能小於0,所以需要加一個鎖,在同一時刻只能有一個人進門去買票,也就是同一個資源同一個時刻只能有一個執行緒進行操作,這樣在第三個人進門的時候就能判斷出票數已經賣完了,不會產生票數成負數的情況。

2.鎖的類型

  1.重入鎖

  重入鎖也叫遞歸鎖,外層的函數獲取鎖後,如果裡面的函數仍然有獲取鎖的程式碼,裡面的函數就不用重新獲取鎖了。 比如:ReentrantLock 和 synchronized

  比如:

  

public class Test implements Runnable {      public  synchronized void get() {          System.out.println("name:" + Thread.currentThread().getName() + " get();");          set();      }        public synchronized  void set() {          System.out.println("name:" + Thread.currentThread().getName() + " set();");      }        @Override      public void run() {          get();      }        public static void main(String[] args) {          Test ss = new Test();          new Thread(ss).start();          new Thread(ss).start();          new Thread(ss).start();      }  }

public class Test02 extends Thread {      ReentrantLock lock = new ReentrantLock();      public void get() {          lock.lock();          System.out.println(Thread.currentThread().getId());          set();          lock.unlock();      }      public void set() {          lock.lock();          System.out.println(Thread.currentThread().getId());          lock.unlock();      }      @Override      public void run() {          get();      }      public static void main(String[] args) {          Test ss = new Test();          new Thread(ss).start();          new Thread(ss).start();          new Thread(ss).start();      }    }

View Code

   2.讀寫鎖

  讀寫鎖:既是排他鎖,又是共享鎖。讀鎖,共享鎖,寫鎖:排他鎖

  

public class Cache {      static Map<String, Object> map = new HashMap<String, Object>();      static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();      static Lock r = rwl.readLock();      static Lock w = rwl.writeLock();        // 獲取一個key對應的value      public static final Object get(String key) {          r.lock();          try {              System.out.println("正在做讀的操作,key:" + key + " 開始");              Thread.sleep(100);              Object object = map.get(key);              System.out.println("正在做讀的操作,key:" + key + " 結束");              System.out.println();              return object;          } catch (InterruptedException e) {            } finally {              r.unlock();          }          return key;      }        // 設置key對應的value,並返回舊有的value      public static final Object put(String key, Object value) {          w.lock();          try {                System.out.println("正在做寫的操作,key:" + key + ",value:" + value + "開始.");              Thread.sleep(100);              Object object = map.put(key, value);              System.out.println("正在做寫的操作,key:" + key + ",value:" + value + "結束.");              System.out.println();              return object;          } catch (InterruptedException e) {            } finally {              w.unlock();          }          return value;      }        // 清空所有的內容      public static final void clear() {          w.lock();          try {              map.clear();          } finally {              w.unlock();          }      }        public static void main(String[] args) {          new Thread(new Runnable() {                @Override              public void run() {                  for (int i = 0; i < 10; i++) {                      Cache.put(i + "", i + "");                  }                }          }).start();          new Thread(new Runnable() {                @Override              public void run() {                  for (int i = 0; i < 10; i++) {                      Cache.get(i + "");                  }                }          }).start();      }  }

View Code

  3.悲觀鎖

  總是假設最壞的情況,每次取數據時都認為其他執行緒會修改,所以都會加鎖(讀鎖、寫鎖、行鎖等),當其他執行緒想要訪問數據時,都需要阻塞掛起。可以依靠資料庫實現,如行  鎖、讀鎖和寫鎖等,都是在操作之前加鎖,在Java中,synchronized的思想也是悲觀鎖。

  

  4.樂觀鎖

  在更新數據的時候會判斷其他執行緒是否修改過數據,一般通過version版本號控制。

  具體實現,就是在數據表加一個version欄位,每次更新的時候都會與上一次取出數據的版本號做比較。

  ① 執行緒A 和 執行緒B 同時取出同一條數據,這是數據的version為0.

  ② 執行緒A 取出數據的時候version = 0 , 這時候執行緒A走完業務,需要更新數據,這是會 update tabel set version = version + 1 where id = {id} and version = #{取數  據時的version}

  ③ 這時執行緒B 也走完業務去更新這條數據,這時執行update tabel set version = version + 1 where id = {id} and version = #{取出數據時的version} 這時候取出數據時  的version為0但是執行緒這條數據的version已經為1了。所以會更新失敗。在這個情況下可以寫一個循環,重新去出該條數據進行更新,知道這條數據更新成功為止。

    

  5.CAS無鎖機制

  這種模式在多核cpu的情況下,完全沒有鎖競爭帶來的系統開銷,也沒有執行緒間頻繁調度帶來的開銷,因此,它要比基於鎖的方式擁有更優越的性能。

  演算法過程:CAS演算法的過程是這樣:它包含三個參數CAS(V,E,N): V表示要更新的變數,E表示預期值,N表示新值。僅當V值等於E值時,才會將V的值設為N,如果V值和E值不  同,則說明已經有其他執行緒做了更新,則當前執行緒什麼都不做。最後,CAS返回當前V的真實值。

  

  6.分散式鎖

  在分散式的場景下,如果是單資料庫的情況下,某些場景下還可以用樂觀鎖,大部分場景想在不用的jvm中保證數據的同步,安全問題,還是需要使用快取,數據,zookepper等實現分散式鎖。