JUC之多執行緒鎖問題

多執行緒鎖

8種問題鎖狀態:

該部分全部圍繞的是以下內容並結合相應的例子:synchronized實現同步的基礎:Java中每個對象都可以作為鎖。

具體表現為以下三種形式:(之前只是簡單的了解)

  1. 對於普通同步方法,鎖是當前實例對象。
  2. 對於靜態同步方法,鎖是當前類的Class對象。
  3. 對於同步方法塊,鎖是Synchonized括弧里配置的對象

當一個執行緒試圖訪問同步程式碼塊時,它首先必須得到鎖,退出或拋出異常時必須釋放鎖

也就是說如果一個實例對象的普通同步方法獲取鎖後,該實例對象的其他普通方法必須等待獲取鎖的方法釋放鎖後才能獲取鎖

,可是別的實例對象的非靜態同步方法因為跟該實例對象的普通同步方法用的是不同的鎖,所以必須等待該實例對象已獲取鎖的普通同步方法釋放鎖就可以獲取他們自己的鎖。

所以的靜態同步方法用的也是同一把鎖—類對象本身,這兩把鎖(this/class)是不同的對象,所以靜態同步方法與非靜態同步方法之間是不會有競爭條件的。但是一旦一個靜態同步方法獲取鎖後,其他的靜態同步方法都必須等待該方法釋放鎖後才能獲得鎖,而不管是同一實例對象的靜態同步方法之間,還是不同的實例對象的靜態同步方法之間,只要它們是同一個類的實例對象。

具體的例子如下:

package com.JUC;

import java.util.concurrent.TimeUnit;

class phone{
    public static synchronized  void sendEmail() throws Exception {
//        Thread.sleep(4000);
        //暫定4s
        TimeUnit.SECONDS.sleep(1);
        System.out.println("sendEmail-------");
    }
    public synchronized void sendMessage() throws Exception {
        System.out.println("sendSMS----------");
    }
    public void hello(){
        System.out.println("hello wold");
    }
}

public class lockPhenomenon {
    public static void main(String[] args) throws InterruptedException {
        //phone 是模板--class,new phone --this
        phone phone = new phone();
        phone phone2 = new phone();
        new Thread(()->{
            try {
                phone.sendEmail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"AAA").start();
        Thread.sleep(100);
        new Thread(()->{
            try {
//                phone.sendMessage();
                //phone.hello();
                phone2.sendMessage();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"BBB").start();
    }
}

/**

  • 多執行緒8鎖
    1. 標準訪問,先列印郵件還是簡訊
  • sendEmail——-
  • sendSMS———-
  • 2、郵件方法暫停4s,請問先列印郵件還是簡訊
  • sendEmail——-
  • sendSMS———-
  • 解釋1-2問題:synchronized鎖的是當前類對象,一個對象裡面如果有多個synchronized方法,在某個時刻內,只要一個執行緒去調用其中的一個synch方法了,其他執行緒都得只能等待,換句話說,某一個時刻內,只能有唯一一個執行緒去訪問這些synchronized方法,鎖的是當前對象this,被鎖定後,其他的執行緒都不能進入到當前對象的其他synchronized方法。
  • 3、新增一個普通方法hello,先列印郵件還是hello.
  • hello wold
  • sendEmail——-
    解釋:加個普通方法後發現和同步鎖無關,
  • 4、兩部手機,先列印郵件還是先列印簡訊。
  • sendSMS———-
  • sendEmail——-
  • 解釋4:換成兩個對象後,不是同一把鎖了,情況立刻變化
  • 5、兩個靜態同步方法,同一部手機,請問先列印郵件還是簡訊
  • sendEmail——-
  • sendSMS———-
  • 6、兩個靜態同步方法,兩部手機,請問先列印郵件還是簡訊
  • sendEmail——-
  • sendSMS———-
  • 解釋5-6問題:都換成靜態同步方法後,情況又發生了變化,synchronized鎖的是new–this,具體的對象(如一部部手機)
    *static synchronized鎖的是 靜態 class—模板
  • 7、一個普通同步方法,一個靜態同步方法,1部手機,請問先列印郵件還是簡訊
  • sendSMS———-
  • sendEmail——-
  • 8、一個普通同步方法,一個靜態同步方法,2部手機,請問先列印郵件還是簡訊
  • sendSMS———-
  • sendEmail——-
    */

重入鎖:

重進入是指任意執行緒在獲取到鎖之後能夠再次獲取該鎖而不會被鎖所阻塞,並且支援獲取鎖時的公平和非公平性選擇

該特性的實現需要解決以下兩個問題。

1)執行緒再次獲取鎖。鎖需要去識別獲取鎖的執行緒是否為當前佔據鎖的執行緒,如果是,則再次成功獲取。

2)鎖的最終釋放。執行緒重複n次獲取了鎖,隨後在第n次釋放該鎖後,其他執行緒能夠獲取到該鎖。鎖的最終釋放要求鎖對於獲取進行計數自增,計數表示當前鎖被重複獲取的次數,而鎖被釋放時,計數自減,當計數等於0時表示鎖已經成功釋放

ReentrantLock:顯式的重進入,調用lock()方法時,已經獲取到鎖的執行緒,能夠再次調用lock()方法獲取鎖而不被阻塞。

synchronized:隱式的重進入,如一個synchronized修飾的遞歸方法,在方法執行時,執行執行緒在獲取了鎖之後仍能連續多次地獲得該鎖,而不像Mutex由於獲取了鎖,而在下一次獲取鎖時出現阻塞自己的情況。

公平鎖與非公平鎖:

非公平鎖:執行緒餓死,但是效率高

公平鎖:雨露均沾,效率相對較低

公平性與否是針對獲取鎖而言的,如果一個鎖是公平的,那麼鎖的獲取順序就應該符合請求的絕對時間順序,也就是FIFO(先進先出)。

死鎖:

之前的文章內容:Java多執行緒編程(同步、死鎖、生產消費)

執行緒 A 持有資源 2,執行緒 B 持有資源 1,他們同時都想申請對方的資源,所以這兩個執行緒就會互相等待而進入死鎖狀態。

image-20220104195446720

產生死鎖必須具備以下四個條件(作業系統部分):

  1. 互斥條件:該資源任意一個時刻只由一個執行緒佔用。
  2. 請求與保持條件:一個執行緒因請求資源而阻塞時,對已獲得的資源保持不放。
  3. 不剝奪條件:執行緒已獲得的資源在末使用完之前不能被其他執行緒強行剝奪,只有自己使用完畢後才釋放資源。
  4. 循環等待條件:若干執行緒之間形成一種頭尾相接的循環等待資源關係

解決方式:(破壞其中之一就好)

  1. 破壞互斥條件(無法破壞)

  2. 破壞請求與保持條件

  3. 破壞不剝奪條件

  4. 破壞循環等待條件

·避免一個執行緒同時獲取多個鎖。

·避免一個執行緒在鎖內同時佔用多個資源,盡量保證每個鎖只佔用一個資源。

·嘗試使用定時鎖,使用lock.tryLock(timeout)來替代使用內部鎖機制。

·對於資料庫鎖,加鎖和解鎖必須在一個資料庫連接里,否則會出現解鎖失敗的情況。

實現程式碼:

package com.JUC;

import java.util.concurrent.TimeUnit;

public class deadlock05 {
    static Object a = new Object();
    static Object b = new Object();

    public static void main(String[] args) {
        new Thread(()->{
            synchronized(a){   //獲取a資源,並加鎖
                System.out.println(Thread.currentThread().getName()+"持有a資源,試圖獲取b資源");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (b){  //獲取b資源,並加鎖
                    System.out.println(Thread.currentThread().getName()+"獲取到b資源");
                }
            }
        },"A執行緒").start();

        new Thread(()->{
            synchronized(b){  //獲取b資源,並加鎖
                System.out.println(Thread.currentThread().getName()+"持有b資源,試圖獲取a資源");
                synchronized (a){  //獲取a資源,並加鎖
                    System.out.println(Thread.currentThread().getName()+"獲取到a資源");
                }
            }
        },"B執行緒").start();
    }
}

B執行緒持有b資源,試圖獲取a資源
A執行緒持有a資源,試圖獲取b資源

驗證是否是死鎖:

  1. jps 類似Linux ps -ef
  2. jstack JVM自帶的堆棧跟蹤工具

程式碼在運行的時候,進入命令行:

image-20220104201757734

找到我們正在運行的程式pid.

image-20220104201925113

觀察下圖:

image-20220104202055952

最後顯示發現一個死鎖,然後上面的內容解析:

B執行緒當前鎖的是….fb00,等待的是….faf0

A執行緒當前鎖的是….faf0,等待的是….fb00;