1. 程式人生 > 其它 >Synchronized 和 Lock

Synchronized 和 Lock

1. synchronized

1.1. synchronized 缺陷

1.1.1. 缺陷一

如果一個程式碼塊被synchronized修飾了,當一個執行緒獲取了對應的鎖並執行該程式碼塊時,其他執行緒便只能一直等待,等待獲取鎖的執行緒釋放鎖,而這裡獲取鎖的執行緒釋放鎖只會有兩種情況:

  1. 獲取鎖的執行緒執行完了,然後釋放鎖。
  2. 執行緒執行發生異常,自動釋放鎖。

根據上邊兩點,可以看出synchronized執行緒如果被阻塞了,會導致其他執行緒便只能乾巴巴地等待

1.1.2. 缺陷二

當多個執行緒同時進行讀寫操作時,為了防止讀寫出現安全問題,通常會對讀寫操作都加上 synchronized 以保證執行緒安全。但是會產生以下兩種情況:

  1. 寫執行緒加 synchronized 是沒有問題的,這保證了每次只能一個執行緒去寫。
  2. 但是讀資料時,往往是需要支援多個執行緒同時讀的,一旦加了 synchronized 會導致每次讀資料時,只能有一條執行緒去讀資料。

根據上邊的情況,會對讀鎖產生疑問:為什麼讀執行緒要加鎖?讀執行緒又不修改資料,只需要加個 volatile 關鍵字保證可見性不行嗎?

至於讀執行緒要不要加鎖,可以看下邊的程式碼。

public class Test {

    volatile static int num = 0;
    static final int max = 2;

    public synchronized static void write() {
        num++;
        if (num > max) {
            num = 0;
        }
    }

    public static void read() {
        if (num > max) {
            System.err.println(String.format("資料異常,ThreadName=%s, num = %s", Thread.currentThread().getName(), num));
            System.exit(0);
        }
        System.out.println(String.format("ThreadName=%s, num = %s", Thread.currentThread().getName(), num));
    }

    public static void main(String[] args) {

        // 迴圈 3 次,建立 3個寫執行緒,3個讀執行緒
        IntStream.range(0, 3).forEach(i -> {
            new Thread(() -> {
                while (true) {
                    write();
                }
            }, "write" + i).start();

            new Thread(() -> {
                while (true) {
                    read();
                }
            }, "read" + i).start();
        });
    }
}

上邊的結果看得出來,不加讀鎖,會導致讀到了正在修改中的資料,但是加了讀鎖 synchronized 又導致每次只能一個執行緒去讀,這明顯看得出 synchronized 滿足不了多執行緒的讀寫需求。

1.2. Synchronized 和 Lock 區別

  1. synchronized是java內建關鍵字,Lock是個java類;
  2. synchronized無法判斷是否獲取鎖的狀態,Lock可以判斷是否獲取到鎖;
  3. synchronized會自動釋放鎖,Lock需在finally中手工釋放鎖;

2. Lock 鎖

什麼是 Lock?

Lock 是一個介面主要方法有以下

public interface Lock {
    // 用來獲取鎖。如果鎖已被其他執行緒獲取,則進行等待。
    void lock();
    // 用來獲取鎖。但是可以被interrupt()方法中斷獲取鎖,直接丟擲異常。
    void lockInterruptibly() throws InterruptedException;
    // 用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他執行緒獲取),則返回false
    boolean tryLock();
    // 和tryLock()方法是類似的,只不過區別在於這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    // 釋放鎖
    void unlock();
    // Condition中的await()方法相當於Object的wait()方法,Condition中的signal()方法相當於Object的notify()方法
    Condition newCondition();
}

lock() 和 unlock() 方法

lock()方法是平常使用得最多的一個方法,就是用來獲取鎖。如果鎖已被其他執行緒獲取,則進行等待。

採用Lock,必須主動去釋放鎖。一般來說,使用Lock必須在try{}catch{}中執行,並且將unlock放在finally中,以保證鎖一定被被釋放。

通常使用Lock是以下面這種形式去使用的:

Lock lock = new ReentrantLock();// ReentrantLock是可重入鎖
lock.lock();// 獲得鎖
try{
    //處理任務
}finally{
    lock.unlock();   //釋放鎖
}

tryLock() 方法

tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他執行緒獲取),則返回false,也就說這個方法無論如何都會立即返回。在拿不到鎖時不會一直在那等待。

tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,只不過區別在於這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false。

一般情況下tryLock獲取鎖時是這樣使用的:

Lock lock = new ReentrantLock();// ReentrantLock是可重入鎖
if(lock.tryLock()) {
     try{
         //處理任務
     }finally{
         lock.unlock();   //釋放鎖
     } 
}else {
    //如果不能獲取鎖,則直接做其他事情
}

lockInterruptibly() 方法

lockInterruptibly()方法比較特殊,當通過這個方法去獲取鎖時,如果執行緒正在等待獲取鎖,則呼叫interrupt()方法能夠中斷執行緒的等待狀態(中斷等待獲取鎖的狀態)。

由於lockInterruptibly()方法中申明瞭丟擲異常,所以lock.lockInterruptibly()必須放在try塊中或者在呼叫方法的中宣告丟擲InterruptedException。

因此lockInterruptibly()一般的使用形式如下:

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}

或者

public void method() {
    try {
        lock.lockInterruptibly();
        try {  
            //.....
        }
        finally {
            lock.unlock();
        }
    } catch (InterruptedException e) {
        //...
    }
}

lockInterruptibly() 加鎖和 lock() 加鎖被 interrupt() 中斷對比

lock() 方法會忽略interrupt()方法的中斷請求,繼續等待獲取鎖直到成功,成功獲取鎖之後再丟擲異常

lockInterruptibly() 和 lock() 不同,lockInterruptibly() 直接丟擲中斷異常立即響應中斷

public class Test {
    
    static Lock lock = new ReentrantLock();
    
    public static void write() {
        try {
            lock.lock(); // 使用 lock 加鎖
            // lock.lockInterruptibly(); // 使用 lockInterruptibly 加鎖
            try {
                System.out.println(Thread.currentThread().getName() + ">>>得到鎖");
                Thread.sleep(3000);
            } catch (Exception e) {
                System.out.println(Thread.currentThread().getName() + "===先得到鎖,再丟擲異常");
            }finally {
                lock.unlock();
                System.out.println(Thread.currentThread().getName() + "<<<釋放鎖");
            }
        } catch (Exception e1) {
            System.out.println(Thread.currentThread().getName() + "+++不等待獲取鎖了,直接丟擲異常");
        }
    }
    
    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(() -> {
            write();
        }, "t1");
        
        Thread t2 = new Thread(() -> {
            write();
        }, "t2");
        
        t1.start();
        Thread.sleep(10);
        
        t2.start();
        t2.interrupt();// 中斷 t2 執行緒
    }
}

使用 lock() 加鎖列印結果如下:

使用 lockInterruptibly() 加鎖列印結果如下:

3. ReadWriteLock 讀寫鎖

什麼是 ReadWriteLock?

ReadWriteLock 是讀寫鎖,也是一個介面,在它裡面只定義了兩個方法

public interface ReadWriteLock {
    // 讀鎖
    Lock readLock();
    // 寫鎖
    Lock writeLock();
}

readLock() 和 writeLock() 的使用方式

  • 寫鎖:寫鎖只能有一個執行緒寫,當 write1 執行緒佔用寫鎖之後,其他寫執行緒 write* 以及讀執行緒 read* 都需要等待,只有當 write1 執行緒釋放鎖之後,其他的 write* 以及 read* 執行緒才能嘗試獲取鎖。

  • 讀鎖:讀鎖可以多個執行緒同時讀,當 read1 執行緒佔用讀鎖之後,其他讀執行緒 read* 也能同時進入鎖中讀取資料。但是在 read1 執行緒釋放鎖之前,所有寫執行緒 write* 都需要等待。

上邊已經說過 synchronized 滿足不了多執行緒的讀寫需求。這次可以使用 ReadWriteLock 來解決多執行緒的讀寫問題。

由下邊的程式和結果看得出來,加讀寫鎖之後,多執行緒讀寫資料不會存在任何安全問題。

public class Test {

    static volatile int num = 0;
    static final int max = 2;
    static ReadWriteLock rwl = new ReentrantReadWriteLock(); // 可重入讀寫鎖

    public static void write() {
        rwl.writeLock().lock();// 寫鎖,每次只能一個執行緒寫
        try {
            num++;
            if (num > max) {
                num = 0;
            }
        } finally {
            rwl.writeLock().unlock();
        }
    }

    public static void read() {
        rwl.readLock().lock();// 讀鎖,可以多個執行緒同時讀
        try {
            if (num > max) {
                System.err.println(String.format("資料異常,ThreadName=%s, num = %s", Thread.currentThread().getName(), num));
                System.exit(0);
            }
            System.out.println(String.format("ThreadName=%s, num = %s", Thread.currentThread().getName(), num));
        } finally {
            rwl.readLock().unlock();
        }
    }

    public static void main(String[] args) {

        // 迴圈 3 次,建立 3個寫執行緒,3個讀執行緒
        IntStream.range(0, 3).forEach(i -> {
            new Thread(() -> {
                while (true) {
                    write();
                }
            }, "write" + i).start();

            new Thread(() -> {
                while (true) {
                    read();
                }
            }, "read" + i).start();
        });
    }
}

4. 鎖相關概念

4.1. 可重入鎖

像synchronized、ReentrantLock、ReentrantReadWriteLock都是可重入鎖。

舉個簡單的例子,當一個執行緒執行到某個synchronized方法時,比如說method1,而在method1中會呼叫另外一個synchronized方法method2,此時執行緒不必重新去申請鎖,而是可以直接執行方法method2。

class MyClass {
    public synchronized void method1() {
        method2();
    }
     
    public synchronized void method2() {
         
    }
}

上述程式碼中的兩個方法method1和method2都用synchronized修飾了,假如某一時刻,執行緒A執行到了method1,此時執行緒A獲取了這個物件的鎖,而由於method2也是synchronized方法,假如synchronized不具備可重入性,此時執行緒A需要重新申請鎖。但是這就會造成一個問題,因為執行緒A已經持有了該物件的鎖,而又在申請獲取該物件的鎖,這樣就會執行緒A一直等待永遠不會獲取到的鎖。

4.2. 可中斷鎖

顧名思義,就是可以響應中斷的鎖。

在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。

如果執行緒A獲取了鎖,執行緒B正在等待獲取該鎖,可能由於等待時間過長,執行緒B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的執行緒中中斷它,這種就是可中斷鎖。在前面演示lockInterruptibly()的用法時已經體現了Lock的可中斷性。

4.3. 公平/非公平鎖

比如同時有多個執行緒在等待一個鎖,當這個鎖被釋放時,等待時間最久的執行緒(最先請求的執行緒)會獲得該所,這種就是公平鎖。

  • synchronized:非公平鎖
  • ReentrantLock:可以非公平鎖,也可以公平鎖
  • ReentrantReadWriteLock:可以非公平鎖,也可以公平鎖

ReentrantLock 和 ReentrantReadWriteLock 怎麼實現公平鎖?

ReentrantLock 和 ReentrantReadWriteLock 要實現公平鎖,可以看它們的原始碼,在建立物件時可以指定使用公平鎖還是非公平鎖

    public ReentrantLock() {
        sync = new NonfairSync();
    }

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

從上邊的構造方法可以看出,建立公平鎖可以使用以下方式
ReentrantLock lock = new ReentrantLock(true);

另外在ReentrantLock類中定義了很多方法,比如:

isFair()        //判斷鎖是否是公平鎖
isLocked()    //判斷鎖是否被任何執行緒獲取了
isHeldByCurrentThread()   //判斷鎖是否被當前執行緒獲取了
hasQueuedThreads()   //判斷是否有執行緒在等待該鎖

在ReentrantReadWriteLock中也有類似的方法,同樣也可以設定為公平鎖和非公平鎖。不過要注意,ReentrantReadWriteLock並未實現Lock介面,它實現的是ReadWriteLock介面。