引言
重入锁,顾名思义在于这个重
字。开发过程中,我们在用到锁时,可能会用于递归的方法上加锁,此时,那同一个方法对象去重复加锁,是怎么加的呢?本文一起学习一下重入锁这个概念。
重入锁介绍
重入锁概念
重入锁ReentrantLock,是指支持重进入的锁,表示锁可以支持一个线程对资源的重复加锁,也就是说任意线程在获取到这个锁之后,如果说再次获取该锁时,不会被锁所阻塞(递归无阻塞)。另外,重入锁还支持锁时的公平和非公平性(默认)选择。
重入锁实现
实现重入机制,必须解决两个问题:1)线程需要再次获取锁;2)锁需要得到最终的释放。
- 线程再次获取锁:
锁一定要能够识别获取锁的线程是否为当前占据锁的线程,如果是,则获取成功。 - 锁的最终释放:
锁获取对应就会有锁释放,线程重复n次获取了该锁后,需要在第n次释放锁后,其他线程才能够获取该锁。实现机制是计数器:锁每获取一次,计数器自增1,该计数表示当前锁被重复获取的次数;锁每释放一次,计数器自减1,一直减到0,表示当前线程已成功释放该锁,其他线程可以来获取该锁。
公平锁和非公平锁
对于锁的获取,会存在公平性的问题。
所谓公平锁
,其实就是先对锁进行获取的请求肯定优先进行的,锁获取的顺序符合请求的绝对时间顺序,类似于FIFO。反之,即为非公平性锁。
ReentrantLock源码分析
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
ReentrantLock的重入性和公平性
ReentrantLock不支持隐式的重入锁,但是可以在调用lock()
方法时,已经获取到锁的线程,能够再次调用lock()
方法获取锁且不被阻塞。
从ReentrantLock
源码中,我们可以看到上述的一个构造方法,ReentrantLock
的公平与否,正是通过构造方法来抉择的,内部类Sync
继承了AQS
,分为公平锁FairSync
和非公平锁NonfairSync
。如果在绝对的时间上,对锁先发出获取请求的线程一定是先被满足的,这个锁即为公平的,其实也就是等待时间最长的线程最优先获取该锁,所以公平锁的获取是顺序的。
公平的锁机制通常没有非公平的效率高,但是公平锁可以减少“饥饿”发生的概率,等待越久的请求越是可以得到最先满足,不会导致一个线程苦苦等了长时间后得不到满足。
ReentrantLock非公平性锁和公平性锁
...
...
/**
* The synchronization state.
*/
private volatile int state;
/**
* Returns the current value of synchronization state.
* This operation has memory semantics of a {@code volatile} read.
* @return current state value
*/
protected final int getState() {
return state;
}
/**
* The current owner of exclusive mode synchronization.
*/
private transient Thread exclusiveOwnerThread;
/**
* Sets the thread that currently owns exclusive access.
* A {@code null} argument indicates that no thread owns access.
* This method does not otherwise impose any synchronization or
* {@code volatile} field accesses.
* @param thread the owner thread
*/
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
...
...
非公平nonfairTryAcquire()源码
...
...
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取同步计数器标识
int c = getState();
//第一次获取锁
if (c == 0) {
if (compareAndSetState(0, acquires)) {
//将当前线程设置为独占模式同步线程
setExclusiveOwnerThread(current);
return true;
}
}
//若当前线程还是获取锁的独占线程
else if (current == getExclusiveOwnerThread()) {
//计数
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//设置锁的计数值,并返回true
setState(nextc);
return true;
}
//其他线程,返回false
return false;
}
...
...
非公平锁是ReentrantLock默认的锁方式,如何获取同步状态?如上述的源码中显示该方法增加了再次获取同步状态的处理逻辑是通过判断当前线程是否为获取锁的线程,从而决定获取锁的操作是true还是false,如果说是获取锁的线程发出的请求,则同步状态值会自增1并返回true,表示获取锁成功;若不是获取锁的线程发出的请求 ,则返回false。只要CAS
设置同步状态值成功,就表示当前线程获取了锁。
公平tryAcquire()源码
...
...
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
...
...
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取同步状态值
int c = getState();
//第一次获取锁
if (c == 0) {
//判断同步队列中当前节点是否有前驱节点
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//当前线程为独占锁的线程(重入锁的过程)
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
...
...
公平锁同步计数与非公平锁的区别在于:公平锁的方法多了一个hasQueuedPredecessors()
方法判断,判断同步队列中当前节点是否有前驱节点,如果有,则表示有线程比当前线程更早地发出了获取锁的请求,所以需要等待更早的线程获取并释放锁后才能去获取该锁。
tryRelease()源码
...
...
protected final boolean tryRelease(int releases) {
//同步状态值递减
int c = getState() - releases;
//当前线程若不是独占锁的线程,抛异常,计数值不变
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
//当前线程完全释放锁成功
free = true;
//将该锁的独占线程对象置为null,其他线程可以来获取锁。
setExclusiveOwnerThread(null);
}
//设置同步状态值
setState(c);
return free;
}
...
...
同步状态值在再次获取锁时,自增1,对应的,当释放锁是会递减1。上述源码中,可以看到如果该锁被获取了n
次,那么前(n-1)
次的tryRelease()
方法必定是返回了false,只有当第n次完全释放锁,同步状态值c==0
,此时独占锁的线程对象也被设置为了null
,才会返回true,表示完全释放锁成功。
测试
package com.example.andya.demo.service;
import javafx.concurrent.Task;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author Andya
* @date 2020/12/6
*/
public class FairAndUnfairLockTest {
//公平锁
private static Lock fairLock = new ReentrantLockDemo(true);
//非公平锁
private static Lock unfairLock = new ReentrantLockDemo(false);
//公平锁线程
public void lockFairTask() {
TaskThread taskThread = new TaskThread(fairLock, "FAIR");
taskThread.start();
}
//非公平锁线程
public void lockUnfairTask() {
TaskThread taskThread = new TaskThread(unfairLock, "UNFAIR");
taskThread.start();
}
//线程启动后打印线程信息
private static class TaskThread extends Thread{
private Lock lock;
private String type;
public TaskThread(Lock lock, String type) {
this.lock = lock;
this.type = type;
}
@Override
public void run() {
for (int i = 0; i < 2; i++) {
lock.lock();
try {
System.out.println(type + " lock by [" + getId() + "], waiting by "
+ ((ReentrantLockDemo)lock).getQueuedThreads());
} finally {
lock.unlock();
}
}
}
//重写toString方法,使得线程打印出线程id来标识线程
@Override
public String toString() {
return getId() + "";
}
}
private static class ReentrantLockDemo extends ReentrantLock {
public ReentrantLockDemo(boolean fair) {
super(fair);
}
//获取正在等待获取锁的线程列表
@Override
public Collection<Thread> getQueuedThreads() {
List<Thread> threadList = new ArrayList<>(super.getQueuedThreads());
Collections.reverse(threadList);
return threadList;
}
}
public static void main(String[] args) {
FairAndUnfairLockTest fairAndUnfairLockTest = new FairAndUnfairLockTest();
for (int i = 0; i < 5; i++) {
//公平锁测试
fairAndUnfairLockTest.lockFairTask();
//非公平锁测试
// fairAndUnfairLockTest.lockUnfairTask();
}
}
}
结果
// 公平锁的结果
FAIR lock by [9], waiting by []
FAIR lock by [10], waiting by [11, 12, 9]
FAIR lock by [11], waiting by [12, 9, 13, 10]
FAIR lock by [12], waiting by [9, 13, 10, 11]
FAIR lock by [9], waiting by [13, 10, 11, 12]
FAIR lock by [13], waiting by [10, 11, 12]
FAIR lock by [10], waiting by [11, 12, 13]
FAIR lock by [11], waiting by [12, 13]
FAIR lock by [12], waiting by [13]
FAIR lock by [13], waiting by []
// 非公平锁的结果
UNFAIR lock by [10], waiting by [9, 11]
UNFAIR lock by [10], waiting by [9, 11, 12, 13]
UNFAIR lock by [9], waiting by [11, 12, 13]
UNFAIR lock by [9], waiting by [11, 12, 13]
UNFAIR lock by [11], waiting by [12, 13]
UNFAIR lock by [11], waiting by [12, 13]
UNFAIR lock by [12], waiting by [13]
UNFAIR lock by [12], waiting by [13]
UNFAIR lock by [13], waiting by []
UNFAIR lock by [13], waiting by []
分析
从结果中我们可以看出来,公平锁每次都是从同步队列中的第一个节点获取锁,而非公平锁则是会连续两次获取锁。
刚释放锁的线程再次获取同步状态的概率比较大,会出现连续获取锁的情况。
同时,我们可以看到另一个问题就是开销,对于公平锁的开销会比较大一些,因为它每次都是切换到另一个线程,而对于非公平锁,会出现连续获取锁的现象,切换次数少一些,所以非公平性锁的开销会更小
一些。这也反映了公平锁
虽然保证了锁的获取按照顺序进行,保证了公平性,解决了“饥饿”问题
,但是代价却是进行了大量的线程切换
。