简介
ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种保证线程并发安全的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全.
与synchronized相比
相同点
- 都是独占锁的实现
- 都是可重入锁;同一线程可以多次获得同一个锁
- 都保证了可见性和互斥性
- 阻塞等待中的线程来实现同步:也就是说当一个线程获得了锁,进入了同步代码块,其他访问的线程都必须阻塞在同步代码块外面进行等待,而线程阻塞和唤醒的代价是很高的(线程阻塞以后进入内核态,我们的JVM运行在用户态,当我们唤醒线程操作系统需要通过CPU要在用户态与内核态进行一轮切换,是一个非常重型的操作);
不同点
比较的点 | synchronized | ReentrantLock |
---|---|---|
获得锁的方式 | 隐式锁,jvm控制加锁解锁 | 显式锁,手动加锁解锁 |
是否公平 | 非公平锁 | 既可以公平锁,也可以非公平锁,通过构造方法传入的boolean判断 |
锁的对象 | 锁的是对象,锁是保存在Mark Word里面,根据Mark Word数据标识锁当前的状态 | 锁的是线程,根据进入的线程和AQS内部维护的state同步器标识锁当前的状态 |
构造方法
/**
* 使用内部类Sync来保证同步
* Sync继承AQS框架,有两个子类
* NonfairSync:非公平锁
* FairSync:公平锁
*/
private final Sync sync;
/**
* 无参构造无参构造默认使用非公平锁
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* 带参构造
* @param fair true:公平锁
* false:非公平锁
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock通过内部类Sync来保证线程安全,Sync继承AbstractQueuedSynchronized,还对该抽象类的部分方法做了实现;并且还定义了两个子类:
- FairSync 公平锁
- NonfairSync 非公平锁
这两个类都继承Sync,相当于间接的继承了AbstractQueuedSynchronized,所以ReentrantLock同时具备公平与非公平两个特性
AbstractQueuedSynchronized这个类里面维护了一个state属性,这个属性是int类型的,被volatile关键字修饰,记录的当前锁的状态,这个类里面还有一个内部类Node,Node是一个双向链表,AbstractQueuedSynchronized这个类里面记录着等待队列中的头节点与为节点.主要用于来实现等待队列.
在上面的继承关系图上面还有一个类是非常重要的就是AbstractOwnableSynchronizer,这个类里面维护了一个属性:exclusiveOwnerThread,这个属性是Thread类型的,记录的就是当前拿到锁的线程;
加锁
/**
* @Classname ReentrantLockDemo
* @Description
* @Date 2021/8/31 22:01
* @Author fanqiechaodan
*/
@Slf4j
public class ReentrantLockDemo {
public static void main(String[] args) {
// 公平锁
ReentrantLock lock = new ReentrantLock(true);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
lock.lock();
log.info("加锁成功");
} finally {
lock.unlock();
}
}).start();
}
}
}
- 会先调用ReentrantLock.lock()
- 由于在创建ReentrantLock对象时,参数传的是true,所以下一步就会调用公平锁FairSync.lock()
- 方法里面有接着调用AbstractQueuedSynchronizer.acquire(int arg);并且传了一个固定的参数1
3.1 acquire方法里面第一步就是调用tryAcquire(arg),并把参数1传了进去,看到本类中的tryAcquire方法并没有任何的实现,他的实现都是在子类中完成的,这个类中只负责定义具体的行为;到最后还是会调用FairSync.tryAcquire(),这个方法是进行尝试加锁,整个加锁的逻辑都在这里
// FairSync.tryAcquire()公平锁尝试加锁方法
protected final boolean tryAcquire(int acquires) {
// 拿到当前线程
final Thread current = Thread.currentThread();
// 获取state当前锁的状态,
int c = getState();
if (c == 0) {
// 如果c为0,就代表当前可以获取锁
// 因为现在是公平锁,hasQueuedPredecessors():要先去查询有没有线程在排队,如果有当前线程是不是第一个
//compareAndSetState(0, acquires):使用cas的方式将state设置为acquires(这个参数就是1)
// 然后将AbstractOwnableSynchronizer这个类中的exclusiveOwnerThread属性设置为当前线程
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
// 所有操作都做完以后加锁成功
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 如果c不等于0但是此时占用锁的线程就是当前线程
// 对state+1然后设置state,代表ReentrantLock是具备可重入性的
// 这段代码直接使用加号,本身就是线程安全的,
// 因为有前面的判断会保证只有一个线程能进来
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
// 加锁成功
return true;
}
// 当前锁已经被占用并且不是当前线程占用的,加锁失败
return false;
}
3.2 经过上一步加锁的方法,加锁成功的会去执行我们的业务代码,没有加锁成功的就会执行AbstractQueuedSynchronizer.acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
//把当前线程创建一个节点,传了一个Node参数Node.EXCLUSIVE,上面有讲过是独占模式
private Node addWaiter(Node mode) {
// 把当前线程当作构造参数新建一个Node
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 把尾节点的指针给pred
Node pred = tail;
// tail指针从来都没有被赋值过,
// pred是一定为null的!=null里面的逻辑永远都不会走
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 将节点插入等待队列
enq(node);
return node;
}
// 将节点插入等待队列的方法
private Node enq(final Node node) {
// 死循环(自旋)保证一定可以将当前节点插入到等待队列中
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
// 如果队列没有进行初始化,就对队列进行初始化
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 自旋逻辑,队列初始化后一直会走else
node.prev = t;
// cas方式插入队列
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
// 将创建好的节点传进来
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取当前节点
final Node p = node.predecessor();
// 判断一下当前节点是不是头节点
// 如果是头节点的话,再去尝试的获取锁
// 这样做是为了不想马上阻塞线程,
// 因为阻塞/唤醒需要用户态到内核态切换,是一个重型操作尽量避免
if (p == head && tryAcquire(arg)) {
// 加锁成功
// 把当前节点设置为头部节点,方法里面会把当前节点置为null
// 相当于从队列中删除掉当前节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// shouldParkAfterFailedAcquire(p, node):判断一下当前节点是不是可以被唤醒的
// 判断逻辑会执行两次
// 首先第1轮循环、修改head的状态,修改成sinal=-1标记处可以被唤醒.
//第2轮循环,阻塞线程,并且需要判断线程是否是有中断信号唤醒的!
// parkAndCheckInterrupt():将当前节点阻塞住,底层调用的LockSupport.park(this)实现的
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
解锁
public final boolean release(int arg) {
if (tryRelease(arg)) {
// 解锁成功
Node h = head;
// 判断一下还有没有在等待的线程
// 如果有看一下当前线程是不是可以唤醒的
if (h != null && h.waitStatus != 0)
// 如果有等待的线程并且可以被唤醒就唤醒线程
// 底层调用的LockSupport.unpark(s.thread);
unparkSuccessor(h);
return true;
}
return false;
}
- 解锁不管公平还是非公平使用的都是Sync.tryRelease(int releases),调用的时候参数传的都是1
protected final boolean tryRelease(int releases) {
// 把state减掉1
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
// 把当前占有锁的线程置为null
setExclusiveOwnerThread(null);
}
//将c设置为state
setState(c);
return free;
}
公平锁和非公平锁的区别
通过对比公平锁与非公平锁的加锁代码,可以看得出来两个内部类的加锁方法基本上来说是一摸一样的,唯一不同的是,当发现锁没有被占用时:
- 非公平锁:直接采用cas的方式设置锁的状态,并且将占用锁的线程设置为当前线程,然后返回加锁成功
- 公平锁: 先执行!hasQueuedPredecessors()代码,这段代码主要是为了看一下等待队列中有没有正在等待的线程,如果有,那么判断一下当前线程是不是排在最前面的线程,如果是最前面的线程,就使用cas的方式设置锁的状态,并且将占用锁的线程设置为当前线程然后返回加锁成功
可以获取锁的时候,非公平锁会马上获取锁,不会考虑等待队列中有没有比当前线程排队更靠前的线程,直接进行加锁;公平锁则会考虑等待队列中有没有比当前队列更靠前的线程,如果有当前线程就不能加锁.