1. 程式人生 > 其它 >Java多執行緒之執行緒同步方法:synchronized與Lock

Java多執行緒之執行緒同步方法:synchronized與Lock

一、什麼是執行緒同步?

多個執行緒操作同一個資源,即併發問題: 同一個物件多個執行緒同時操作

  • 多個執行緒訪問同一個物件,某些執行緒還想修改物件的值,這時候會出現執行緒不安全的問題

  • 執行緒同步是一種等待機制,即 多個需要同時訪問此物件的執行緒進入物件的等待池 形成佇列,等待前面執行緒使用完畢,下一個執行緒再使用

  • 形成條件: 佇列 + 鎖

  • 同一程序的多個執行緒共享同一塊儲存空間,方便之餘也帶來了訪問衝突問題

    因此,為了保證資料在方法中被訪問時的正確性,在訪問時加入鎖機制 synchronized,當一個執行緒獲得物件的排他鎖,獨佔資源,其他執行緒必須等待,使用後釋放鎖。

    • 存在問題:
      1. 一個執行緒持有鎖會導致其他所有需要此鎖的執行緒掛起
      2. 多執行緒競爭下,加鎖、釋放鎖會導致較多的上下文切換排程延時,引起效能問題
      3. 如果一個優先順序高的執行緒等待一個優先順序低的執行緒釋放鎖,會導致優先順序倒置,引起效能問題

二、同步方法

  1. synchronized 關鍵字
  • 由於我們可以通過private關鍵字保證資料物件只能被方法訪問,所以我們只需要針對方法提出一套機制,即 synchronized關鍵字,包括兩種用法:

      1. synchronized方法
      2. synchronized塊
    public synchronized void method(int args){
        // 同步方法
    }
    
  • synchronized 方法控制對 “物件”的訪問,每個物件對應一把鎖

    ,每個synchronized 方法都必須獲取呼叫該方法的鎖才能執行,否則執行緒會阻塞;方法一旦執行,就獨佔該鎖直到該方法返回才釋放鎖,後面被阻塞的執行緒才能獲得這個鎖,繼續執行

  • synchronized 鎖的是執行緒的共享資源 / 臨界資源

  • 缺陷:

    • 如果將一個大的方法宣告為synchronized 將會影響效率
    • 方法裡面需要修改的內容才需要鎖,鎖的太多反而浪費資源

同步塊

  • 同步塊:synchronized(Obj){ }
  • Obj稱之為 同步監視器
    • Obj可以是任何物件,但是推薦使用共享資源作為同步監視器
    • 同步方法中無需指定同步監視器,默認同步監視器為this,也就是物件本身,或者是class
  • 同步監視器的執行過程
    1. 第一個執行緒訪問:鎖定同步監視器,執行其中程式碼
    2. 第二個執行緒訪問,發現同步監視器被鎖定,無法訪問
    3. 第一個執行緒訪問完畢,解鎖同步監視器
    4. 第二個執行緒訪問,發現同步監視器沒有鎖,則鎖定並訪問
    5. .....

補充知識:

JUC: java.util.concurrent 即併發程式設計

CopyOnWriteArrayList: util.concurrent包下的安全型別集合 可保證執行緒安全,實現執行緒同步的功能!

 public static void main(String[] args) {

        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>(); // <> 裡面包含的是 泛型

        for (int i = 0; i < 10000; i++) {
            new Thread(()->{

                list.add(Thread.currentThread().getName());

            }).start();
        }


        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(list.size());
    }

死鎖

  • 多個執行緒各自佔有一些資源,並且互相等待其他執行緒佔有的資源才能執行,而導致兩個或多個執行緒都在等待對方釋放資源並停止執行;
  • 某一個同步塊同時“擁有兩個以上物件的鎖”時,就可能發生“死鎖”

死鎖避免方法

  • 產生死鎖的四個必要條件:

      1. 互斥條件:一個資源每次只能被一個程序使用
      2. 請求保持條件:一個程序因請求資源而阻塞時,對已經獲得的資源保持不放
      3. 不剝奪條件:程序已獲得的資源,在未使用完之前,不能強行剝奪
      4. 迴圈等待條件:若干程序之間形成一種頭尾相接的迴圈等待資源關係
  • 只要想辦法打破其中任意一個或多個條件,即可避免死鎖發生

  1. Lock(鎖)
  • 從JDK5.0開始,Java提供了更強大的執行緒同步機制——顯式定義同步鎖來實現同步

    同步鎖使用Lock物件來充當

  • java.util.concurrent.locks Lock介面是控制多個執行緒對共享資源進行訪問的工具,鎖提供了對共享資源的獨佔訪問,每次只有一個執行緒對Lock物件加鎖。執行緒開始訪問共享資源之前應先獲得Lock物件

  • ReentrantLock 類實現了Lock(可重入鎖),擁有與synchronized相同的併發性和記憶體語義,在實現執行緒安全的控制中,比較常用ReentrantLock,可以顯式加鎖、釋放鎖

synchronized 與 Lock 的對比

  • Lock是顯式鎖(需手動開啟和關閉鎖) synchronized 是隱式鎖,出了作用域自動釋放
  • Lock只有程式碼塊鎖 synchronized 有程式碼塊鎖 + 方法鎖
  • 使用Lock鎖,JVM將花費較少時間來排程執行緒 = > 效能更好 + 可拓展性強(提供更多子類)
  • 優先使用順序:
    • Lock > 同步程式碼塊(在方法體中,分配了相應資源)> 同步方法(線上程[run()]方法體之外)
class A{
    private final ReentrantLock lock = new ReentrantLock();
    
    @Override
    public void run() {
        while (true) {
            // 將延時放在加鎖之前
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }


            try {
                lock.lock();// 加鎖
                
                // 需要保證執行緒安全的程式碼塊
                if (ticketNum > 0) {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " 拿到了 "+ ticketNum--);

                }else{
                    break;
                }

            } finally {
                lock.unlock(); // 解鎖
            }
        }
    }
}

執行緒協作 重點!

  • 生產者(Producer)- 消費者(Consumer)問題
    • 執行緒同步問題 生產者消費者共享同一個資源
    • 生產者和消費者之間相互依賴,互為條件
    • 在生產者-消費者問題中,僅有synchronized是不夠的
      • synchronized可阻止併發更新同一個共享資源,實現同步
      • synchronized不能用來實現不同執行緒之間的訊息傳遞(通訊)
  • 讀(Read)寫(Write)問題
  • 理髮師理髮問題

三、執行緒通訊

  • Java提供了幾個方法用於執行緒通訊

    1. wait() — 執行緒一直等待,知道其他執行緒同志,與sleep不同,wait()會釋放鎖
    2. wait() — 指定等待的毫秒數
    3. notify() — 喚醒一個處於等待狀態的執行緒
    4. notifyAll() — 喚醒同一個物件上所有呼叫了wait() 方法的執行緒,優先級別高的執行緒優先排程

    注意:以上方法均為Object類的方法,都只能在同步方法中或者同步程式碼塊中使用,否則會丟擲異常IllegalMonitorStateException

本文來自部落格園,作者:{夕立君},轉載請註明原文連結:https://www.cnblogs.com/liuzhhhao/p/15016079.html