Java - Simple usage of reentrant lock ReentrantLock
The interfaces and classes for displaying locks in Java are mainly located java.util.concurrent.locks
under the following main interfaces and classes:
- Lock interface Lock, which is mainly implemented as ReentrantLock
- The read-write lock interface ReadWriteLock, which is mainly implemented as ReentrantReadWriteLock
1. Interface Lock
The definition of the display lock Lock is:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
in:
- lock()/unlock() : A method for acquiring and releasing locks, in which lock() blocks the program until the lock is successfully acquired.
- lockInterruptibly(): Unlike lock(), it can respond to program interruptions and throw InterruptedException if interrupted by other programs.
- tryLock(): try to acquire the lock, this method will return immediately and will not block the program. Returns true if the lock was acquired successfully, otherwise returns false.
- tryLock(long time, TimeUnit unit): try to acquire the lock, if the lock can be acquired, return true directly; otherwise, block waiting, the blocking duration is determined by the incoming parameters, respond to program interruption while waiting, and throw if an interruption occurs InterruptedException; returns true if the lock was acquired during the waiting time, false otherwise.
- newCondition(): Create a new condition. A Lock can be associated with multiple conditions.
Compared with synchronized, display locks can acquire locks in a non-blocking way, respond to program interruptions, set the blocking time of programs, and have more flexible operations.
2. ReentrantLock ReentrantLock
2.1 Basic usage
ReentrantLock is the main implementation class of the Lock interface, and its basic usage lock()/unlock()
implements the synchronized
same semantics, including:
- Reentrant, a thread can continue to acquire a lock on the premise of holding a lock;
- Can solve race conditions (critical section resources);
- Memory visibility issues can be guaranteed.
ReentrantLock has two constructors.
public ReentrantLock()
public ReentrantLock(boolean fair)
The parameter fair indicates whether fairness is guaranteed. If not specified, the default value is false, indicating that fairness is not guaranteed.
Fairness means that the thread with the longest waiting time acquires the lock first.
But guaranteeing fairness may affect the performance of the program, and in general, it is not necessary to guarantee fairness, so the default value is false. And synchronized does not guarantee fairness.
In the case of using an explicit lock, be sure to call unlock. In general, you should wrap the block of code after the lock in a try statement and release the lock in a finally statement, such as the following code that implements a counter:
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 Use tryLock to avoid deadlock
Using the tryLock()
method can avoid the occurrence of deadlock. When you hold a lock and try to acquire another lock, but cannot acquire it, you can release the held lock, give other threads a chance to acquire the lock, and then retry to acquire all the locks.
Next, use the example of transferring money between banks.
Account class representing an 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();
}
}
The money in the Account class represents the current balance. add/reduce is used to modify the balance. To transfer money between accounts, both accounts need to be locked. If we just use lock() directly, our code listing is as follows:
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();
}
}
}
But this way of writing is prone to deadlock. For example, two accounts want to transfer money to each other at the same time, and both have obtained the first lock. In this case a deadlock occurs.
The following code is used to simulate the deadlock process of account transfer.
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 + "转账成功:" + money);
} catch (NoEnoughMoneyException e) {
}
}
}
}
};
threads[i].start();
}
}
public static void main(String[] args) {
simulateDeadLock();
}
The above code creates 10 accounts and 100 threads, each thread loops 100 times, and randomly selects two accounts for transfer in the loop. After running the program for many times, you will find the situation as shown in the following figure. The program is blocked due to deadlock and cannot execute the program completely:
![deadlock.png-29.3kB](http://static.zybuluo.com/ZzzJoe/l251yubvz3bjjuv6263og892/%E6%AD%BB%E9%94%81.png)
Next we use tryLock to write a new method, the code is as follows:
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;
}
Attempt to acquire the lock of the account, if both locks can be acquired successfully, return true, otherwise return false. All locks are released after the method body ends, regardless of the lock acquisition status. At the same time, we can modify the transfer method to call this method cyclically to avoid deadlock. The code can be:
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);
}