Java - 可重入鎖ReentrantLock簡(jiǎn)單用法

Java - 可重入鎖ReentrantLock簡(jiǎn)單用法

Java 中顯示鎖的借口和類主要位于java.util.concurrent.locks下,其主要的接口和類有:

  • 鎖接口Lock,其主要實(shí)現(xiàn)為ReentrantLock
  • 讀寫(xiě)鎖接口ReadWriteLock,其主要實(shí)現(xiàn)為ReentrantReadWriteLock

一、接口Lock

其中顯示鎖Lock的定義為:

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

其中:

  1. lock()/unlock() : 為獲取鎖和釋放鎖的方法,其中l(wèi)ock()會(huì)阻塞程序,直到成功的獲取鎖。
  2. lockInterruptibly():與lock()不同的地方是,它可以響應(yīng)程序中斷,如果被其他程序中斷了,則拋出InterruptedException。
  3. tryLock():嘗試獲取鎖,該方法會(huì)立即返回,并不會(huì)阻塞程序。如果獲取鎖成功則返回true,反之則返回false。
  4. tryLock(long time, TimeUnit unit):嘗試獲取鎖,如果能獲取鎖則直接返回true;否則阻塞等待,阻塞時(shí)長(zhǎng)由傳入的參數(shù)來(lái)決定,在等待的同時(shí)響應(yīng)程序中斷,如果發(fā)生了中斷則拋出InterruptedException;如果在等待的時(shí)間中獲取了鎖則返回true,反之返回false。
  5. newCondition():新建一個(gè)條件,一個(gè)Lock可以關(guān)聯(lián)多個(gè)條件。

相比synchronized,顯示鎖可以用非阻塞的方式獲取鎖,可以響應(yīng)程序中斷,可以設(shè)定程序的阻塞時(shí)間,擁有更加靈活的操作。

二、可重入鎖ReentrantLock

2.1 基本用法

ReentrantLock是Lock接口的主要實(shí)現(xiàn)類,其基本用法lock()/unlock()實(shí)現(xiàn)了與synchronized一樣的語(yǔ)義,其中包括:

  • 可重入,一個(gè)線程在持有一個(gè)鎖的前提下,可以繼續(xù)獲得該鎖;
  • 可以解決競(jìng)態(tài)條件問(wèn)題(臨界區(qū)資源);
  • 可以保證內(nèi)存可見(jiàn)性問(wèn)題。

ReentrantLock有兩個(gè)構(gòu)造方法。

public ReentrantLock()
public ReentrantLock(boolean fair)

參數(shù)fair表示是否保證公平,在不指定的情況下默認(rèn)值為false,表示不保證公平。

公平的意思是指:等待時(shí)間最長(zhǎng)的線程優(yōu)先獲取鎖。

但是保證公平可能會(huì)影響程序的性能,在一般情況下也不需要保證公平,所以默認(rèn)值為 false 。而synchronized也是不保證公平的。

在使用顯示鎖的情況下,一定要記得調(diào)用 unlock 。一般而言,應(yīng)該將 lock 之后的代碼塊包裝在 try 語(yǔ)句中,在 finally 語(yǔ)句中釋放鎖,例如以下實(shí)現(xiàn)計(jì)數(shù)器的代碼:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Created by Joe on 2018/4/10.
 */
public class Counter {
    private final Lock lock = new ReentrantLock();
    private volatile int count;
    public void incr() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
    public int getCount() {
        return count;
    }
}

2.2 使用tryLock避免死鎖

使用tryLock()方法可以避免死鎖的發(fā)生。在持有一個(gè)鎖而嘗試獲取另外一個(gè)鎖,但是獲取不到的時(shí)候,可以釋放已持有的鎖,給其他線程獲取鎖的機(jī)會(huì),然后重試獲取所有的鎖。

接下來(lái)使用銀行之間轉(zhuǎn)賬的例子。

表示賬戶的Account類:

public class Account {
    private Lock lock = new ReentrantLock();
    private volatile double money;
    public Account(double initialMoney) {
        this.money = initialMoney;
    }
    public void add(double money) {
        lock.lock();
        try {
            this.money += money;
        } finally {
            lock.unlock();
        }
    }
    public void reduce(double money) {
        lock.lock();
        try {
            this.money -= money;
        } finally {
            lock.unlock();
        }
    }
    public double getMoney() {
        return money;
    }
    void lock() {
        lock.lock();
    }
    void unlock() {
        lock.unlock();
    }
    boolean tryLock() {
        return lock.tryLock();
    }
}

Account類中的money表示當(dāng)前的余額。add/reduce用于修改余額。在賬戶之間轉(zhuǎn)賬,需要這兩個(gè)賬戶都要進(jìn)行鎖定。如果我們直接只用 lock() ,我們的代碼清單如下:

public class AccountMgr {
    public static class NoEnoughMoneyException extends Exception {}
    public static void transfer(Account from, Account to, double money)
            throws NoEnoughMoneyException {
        from.lock();
        try {
            to.lock();
            try {
                if(from.getMoney() >= money) {
                    from.reduce(money);
                    to.add(money);
                } else {
                    throw new NoEnoughMoneyException();
                }
            } finally {
                to.unlock();
            }
        } finally {
            from.unlock();
        }
    }
}

但是這種寫(xiě)法容易發(fā)生死鎖。比如,兩個(gè)賬戶都想同時(shí)給對(duì)方進(jìn)行轉(zhuǎn)賬,并且均獲得了第一個(gè)鎖。在這種情況下就會(huì)發(fā)生死鎖。

接下來(lái)的代碼用于模擬賬戶轉(zhuǎn)賬的死鎖過(guò)程。

public static void simulateDeadLock() {
    final int accountNum = 10;
    final Account[] accounts = new Account[accountNum];
    final Random rnd = new Random();
    for(int i = 0; i < accountNum; i++) {
        accounts[i] = new Account(rnd.nextInt(10000));
    }
    int threadNum = 100;
    Thread[] threads = new Thread[threadNum];
    for(int i = 0; i < threadNum; i++) {
        threads[i] = new Thread() {
            public void run() {
                int loopNum = 100;
                for(int k = 0; k < loopNum; k++) {
                    int i = rnd.nextInt(accountNum);
                    int j = rnd.nextInt(accountNum);
                    int money = rnd.nextInt(10);
                    if(i != j) {
                        try {
                            transfer(accounts[i], accounts[j], money);
                            System.out.println(i + "--->" + j + "轉(zhuǎn)賬成功:" + money);
                        } catch (NoEnoughMoneyException e) {
                        }
                    }
                }
            }
        };
        threads[i].start();
    }
}

public static void main(String[] args) {
    simulateDeadLock();
}

以上代碼創(chuàng)建了10個(gè)賬戶,100個(gè)線程,每個(gè)線程均循環(huán)100次,在循環(huán)中隨機(jī)挑選兩個(gè)賬戶進(jìn)行轉(zhuǎn)賬。在程序運(yùn)行多次之后你會(huì)發(fā)現(xiàn)如下圖所示的情況,程序因?yàn)榘l(fā)生死鎖陷入阻塞態(tài),無(wú)法完整執(zhí)行程序:

<center>
死鎖.png-29.3kB
死鎖.png-29.3kB
</center>

接下來(lái)我們使用 tryLock 書(shū)寫(xiě)一個(gè)新的方法,代碼如下所示:

public static boolean tryTransfer(Account from, Account to, double money)
            throws NoEnoughMoneyException {
    if (from.tryLock()) {
        try {
            if (to.tryLock()) {
                try {
                    if (from.getMoney() >= money) {
                        from.reduce(money);
                        to.add(money);
                    } else {
                        throw new NoEnoughMoneyException();
                    }
                    return true;
                } finally {
                    to.unlock();
                }
            }
        } finally {
            from.unlock();
        }
    }
    return false;
}

嘗試獲取賬戶的鎖,如果兩個(gè)鎖都能獲取成功,則返回 true,反之則返回 false。無(wú)論鎖的獲取狀態(tài)如何,在方法體結(jié)束之后都會(huì)釋放所有的鎖。同時(shí)我們可以改造 transfer 方法來(lái)循環(huán)調(diào)用該方法以避免死鎖情況的發(fā)生,其代碼可以為:

public static void transfer(Account from, Account to, double money)
            throws NoEnoughMoneyException {
    boolean success = false;
    do {
        success = tryTransfer(from, to, money);
        if (!success) {
            Thread.yield();
        }
    } while (!success);
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容