一、基础概念
- 基于AQS+CAS+volatile实现的。(什么是AQS往下看完ReentrantLock源码就知道了)
- ReentrantLock有一个内部类Sync继承了AQS(AbstractQueuedSynchronizer)基类,ReentrantLock并没有直接去继承AQS。而是通过sync来构建锁的。
- AQS相当于一个“锁框架”,不同的锁实现不一样。用state、线程链表来构建的
- state是个什么鬼?
- state是AQS中的一个由volatile修饰的int类型变量,多个线程会通过CAS的方式修改state,在并发情况下,只会有一个线程成功的修改state(从0~1)
- 如果线程修改state失败怎么办?
- 如果线程没有拿到锁资源,会到AQS的双向链表中排队等待(在期间,线程可能会挂起)
- AQS的双向链表(队列)是个啥?
- AQS中的双向链表是基于内部类Node在维护,Node中包含prev,next,thread属性,并且在AQS中还有两个属性,分别是head,tail。
- 与Synchronized的区别
- 实现不一样,ReentrantLock底层是AQS,Synchronized底层是ObjectMonitor
- 用法不一样,ReentrantLock更加多样化。
- Synchronized是非公平,可重入,互斥锁。ReentrantLock是非公平/公平,可重入,互斥锁
- 如果是竞争很激烈的话,建议用ReentrantLock,因为不牵扯用户态和内核态的交互。
二、使用
lock
public class Test {
private static int count = 0;
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
new ThreadDemo().start();
new ThreadDemo().start();
Thread.sleep(1000);
System.out.println(count);
}
static class ThreadDemo extends Thread {
@Override
public void run() {
reentrantLock.lock();
try {
for (int i = 0; i < 1000; i ++) {
count++;
}
} finally {
reentrantLock.unlock();
}
}
}
}
trylock
public class Test {
private static int count = 0;
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
new ThreadDemo().start();
new ThreadDemo().start();
Thread.sleep(4000);
System.out.println(count);
}
static class ThreadDemo extends Thread {
@Override
public void run() {
try {
if (reentrantLock.tryLock(1, TimeUnit.SECONDS)) {
try {
for (int i = 0; i < 1000; i++) {
count++;
}
Thread.sleep(2000);
} finally {
reentrantLock.unlock();
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
lockInterruptibly
public class Test {
private static int count = 0;
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
reentrantLock.lock();
try {
for (int i = 0; i < 1000; i++) {
count++;
}
Thread.sleep(4000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
reentrantLock.unlock();
}
});
t1.start();
Thread.sleep(500);
Thread t2 = new Thread(() -> {
try {
reentrantLock.lockInterruptibly();
for (int i = 0; i < 1000; i++) {
count++;
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " : 被打断了, 我可以干点别的");
}
});
t2.setName("t2");
t2.start();
Thread.sleep(1000);
t2.interrupt();
System.out.println(count);
}
}
condition
比肩于synchronized的wait、notifiy、notifyAll。可以设置多个条件的等待唤醒。synchronized只能是一种。
public class Test {
private static final ReentrantLock lock = new ReentrantLock();
private static final Condition c1 = lock.newCondition();
private static final Condition c2 = lock.newCondition();
private static final List<Integer> list = new ArrayList<>();
private static void consumer() {
lock.lock();
try {
while (list.size() == 0) {
c2.await();
}
list.remove(0);
System.out.println("消费了一个商品");
c1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
private static void producer(Integer i) {
lock.lock();
try {
while (list.size() >= 20) {
c1.await();
}
list.add(i);
System.out.println("生产者物品数量" + list.size());
c2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
new Thread(() -> {
for (int i = 0; i < 100; i++) {
Test.producer(i);
}
}).start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
Test.consumer();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
}
}
三、源码(jdk1.8)
lock()
公平锁
final void lock() {
acquire(1);
}
非公平锁
final void lock() {
// 非公平锁加锁的时候,第一件事就是先用CAS抢一次锁。
if (compareAndSetState(0, 1))
// 如果抢到了,就将当前互斥锁的线程设置成自己。
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
acquire()
AQS方法,公平锁、非公平锁通用
public final void acquire(int arg) {
//1. 调用tryAcquire方法:尝试获取锁资源(非公平、公平),拿到锁资源,返回true,直接结束方法。 没有拿到锁资源,
// 需要执行&&后面的方法
//2. 当没有获取锁资源后,会先调用addWaiter:会将没有获取到锁资源的线程封装为Node对象,
// 并且插入到AQS的队列的末尾,并且作为tail
//3. 继续调用acquireQueued方法,查看当前排队的Node是否在队列的前面,如果在前面(head的next),尝试获取锁资源
// 如果没在前面,尝试将线程挂起,阻塞起来!
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire()
- tryAcquire分为公平和非公平两种
- tryAcquire主要做了两件事:
- 如果state为0,尝试获取锁资源
- 如果state不为0,看一下是不是锁重入操作
公平锁
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取AQS的锁状态
int c = getState();
// 当state == 0就说明当前没有线程拿到锁,当前线程就可以去抢锁了
if (c == 0) {
// 判断是否有线程在排队,如果有线程排队,返回true,配上前面的!,那会直接不执行返回最外层的false
// 如果没有线程排队,直接CAS尝试获取锁资源
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
// 将当前占用这个互斥锁的线程属性设置为当前线程
setExclusiveOwnerThread(current);
// 返回true,拿锁成功
return true;
}
}
// 当前state != 0,说明有线程占用着锁资源
// 判断拿着锁的线程是不是当前线程(锁重入)
else if (current == getExclusiveOwnerThread()) {
// 将state再次+1
int nextc = c + acquires;
// 锁重入是否超过最大限制
// 01111111 11111111 11111111 11111111 + 1
// 10000000 00000000 00000000 00000000
// 抛出error
if (nextc < 0)
// 重入次数最大值就是int的最大值了
throw new Error("Maximum lock count exceeded");
// 将值设置给state
setState(nextc);
// 返回true,拿锁成功
return true;
}
return false;
}
非公平锁
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取AQS的锁状态
int c = getState();
// 当state == 0就说明当前没有线程拿到锁,当前线程就可以去抢锁了
if (c == 0) {
// 没人占用锁资源,我直接抢一波(不管有没有线程在排队)
if (compareAndSetState(0, acquires)) {
// 将当前占用这个互斥锁的线程属性设置为当前线程
setExclusiveOwnerThread(current);
// 返回true,拿锁成功
return true;
}
}
// 当前state != 0,说明有线程占用着锁资源
// 判断拿着锁的线程是不是当前线程(锁重入)
else if (current == getExclusiveOwnerThread()) {
// 将state再次+1
int nextc = c + acquires;
// 锁重入是否超过最大限制
// 01111111 11111111 11111111 11111111 + 1
// 10000000 00000000 00000000 00000000
// 抛出error
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 将值设置给state
setState(nextc);
// 返回true,拿锁成功
return true;
}
return false;
}
addWaiter()
在获取锁资源失败后,需要将当前线程封装为Node对象,并且插入到AQS队列的末尾
// 将当前线程封装为Node对象,并且插入到AQS队列的末尾
private Node addWaiter(Node mode) {
// 将当前线程封装为Node对象,mode为null,代表互斥锁
Node node = new Node(Thread.currentThread(), mode);
// pred是tail节点
Node pred = tail;
// 如果pred不为null,有线程正在排队
if (pred != null) {
// 将当前节点的prev,指定tail尾节点
node.prev = pred;
// 以CAS的方式,将当前节点变为tail节点
if (compareAndSetTail(pred, node)) {
// 之前的tail的next指向当前节点
pred.next = node;
return node;
}
}
// 添加的流程为, 自己prev指向、tail指向自己、前节点next指向我
// 如果上述方式,CAS操作失败,导致加入到AQS末尾失败,如果失败,就基于enq的方式添加到AQS队列
enq(node);
return node;
}
// enq,无论怎样都添加进入
private Node enq(final Node node) {
for (;;) {
// 拿到tail
Node t = tail;
// 如果tail为null,说明当前没有Node在队列中
if (t == null) {
// 创建一个新的Node作为head,并且将tail和head指向一个Node
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 有了没用的Node节点当头了,那么当前这个节点就可以插入链表的tial了。
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueued()
- acquireQueued方法会查看当前排队的Node是否是head的next,如果是,尝试获取锁资源,如果不是或者获取锁资源失败那么就尝试将当前Node的线程挂起(unsafe.park())
- 在挂起线程前,需要确认当前节点的上一个节点的状态必须是小于等于0,
- 如果为1,代表是取消的节点,不能挂起
- 如果为-1,代表挂起当前线程
- 如果为-2,-3,需要将状态改为-1之后,才能挂起当前线程
// acquireQueued方法
// 查看当前排队的Node是否是head的next,
// 如果是,尝试获取锁资源,
// 如果不是或者获取锁资源失败那么就尝试将当前Node的线程挂起(unsafe.park())
final boolean acquireQueued(final Node node, int arg) {
// 标识。
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 拿到上一个节点
final Node p = node.predecessor();
// 说明当前节点是head的next
// 竞争锁资源,成功:true,失败:false
if (p == head && tryAcquire(arg)) {
// 进来说明拿到锁资源成功
// 将当前节点置位head,thread和prev属性置位null
setHead(node);
// 帮助快速GC
p.next = null; // help GC
// 设置获取锁资源成功
failed = false;
return interrupted;
}
/// 如果不是或者获取锁资源失败,尝试将线程挂起
// 第一个事情,当前节点的上一个节点的状态正常!
// 第二个事情,挂起线程
// 通过LockSupport将当前线程挂起
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 拿到前一个节点的状态
int ws = pred.waitStatus;
// 如果上一个节点为 -1
if (ws == Node.SIGNAL)
// 返回true,挂起线程
return true;
// 如果上一个节点是取消状态
if (ws > 0) {
// 循环往前找,找到一个状态小于等于0的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 将小于等于0的节点状态该为-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
unlock()
- 释放锁资源:
- 将state-1。
- 如果state减为0了,唤醒在队列中排队的Node。(一定唤醒离head最近的)
- 释放锁不分公平和非公平,就一个方法。
public void unlock() {
sync.release(1);
}
// 真正释放锁资源的方法
public final boolean release(int arg) {
// 核心的释放锁资源方法
if (tryRelease(arg)) {
// 释放锁资源释放干净了。 (state == 0)
Node h = head;
// 如果头节点不为null,并且头节点的状态不为0,唤醒排队的线程
if (h != null && h.waitStatus != 0)
// 唤醒线程
unparkSuccessor(h);
return true;
}
// 释放锁成功,但是state != 0
return false;
}
tryRelease()
// 核心的释放锁资源方法
protected final boolean tryRelease(int releases) {
// 获取state - 1
int c = getState() - releases;
// 如果释放锁的线程不是占用锁的线程,抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 是否成功的将锁资源释放利索 (state == 0)
boolean free = false;
if (c == 0) {
// 锁资源释放干净。
free = true;
// 将占用锁资源的属性设置为null
setExclusiveOwnerThread(null);
}
// 将state赋值
setState(c);
// 返回true,代表释放干净了
return free;
}
unparkSuccessor()
// 唤醒节点
private void unparkSuccessor(Node node) {
// 获取头结点的状态
int ws = node.waitStatus;
// 如果头节点状态小于0,那么就设为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 获取头结点的下一个节点
Node s = node.next;
// 如果头结点的下一个节点为null,或者状态是1。那么就从尾节点倒着开始往前找,找到那个状态小等于0且距离head最近节点。
// 为什么要从尾节点倒着始找?addWaiter添加节点的时候,是先把当前节点的pre指向tail,然后将tail设置为当前节点,最后将尾节点的next设置为当前节点。这就有个问题了,如果只执行了第一步,然后要唤醒,是不是从头开始找,找不到这个要挂起的线程。
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 节点唤醒
LockSupport.unpark(s.thread);
}
四、总结流程(个人总结,可能有错误)
lock 加锁
备注:抢锁的逻辑都是执行一次cas
- 非公平锁上来先抢一把,公平锁不会抢。(ReentrantLock中的Sync类中的lock方法)
- 尝试抢锁(AQS中的tryAcquire方法)
- 公平锁:拿到当前线程以及AQS锁状态
- 当state==0时,判断队列是不是空,空的话抢锁。非空的话需要去看一下head的next是不是当前线程,如果是也去抢锁。
- 当state!=0时,判断当前线程,是不是拿锁的线程,如果是话,就重入拿锁啊。
- 非公平锁:拿到当前线程以及AQS锁状态
- 当state==0时,直接去抢一次锁。
- 当state!=0时,判断当前线程,是不是拿锁的线程,如果是话,就重入拿锁啊
- 公平锁:拿到当前线程以及AQS锁状态
- 抢不到锁了,就添加队列(AQS中的addWaiter方法)
- 使用当前线程构造一个Node节点。判断尾节点(tail)不是null。那么就将Node节点加入到tail的后面。加入逻辑如下:
- 将Node节点的pre设置为tail
- 将tail设置为当前节点
- 将原来的tail的next设置为当前节点
- 起一个死循环
- 如果tail是个null。那么就创建一个Node但是没有线程,这就是那个没有实际意义的Node,将这个Node变为head和tail
- 如果tail不是null。那么就用上面的逻辑将Node加入到tail后面,这个是CAS操作,直到加入成功,这个循环才break
- 使用当前线程构造一个Node节点。判断尾节点(tail)不是null。那么就将Node节点加入到tail的后面。加入逻辑如下:
- 起一个死循环
- 判断addWatier创建出来的Node,pre是不是head,如果是就tryAcquire抢锁,抢锁成功,将当前node设置为head,并将当前node的pre的next设置为null,最后循环break。
- 如果不是判断当前Node的前一个Node状态
- 是-1,如果是-1就直接挂起当前线程
- 是1,从当前Node往前找,直到找到一个Node状态是小于等于0的,将当前节点挂在它的后面。流程就是addWaiter中将Node挂在tail的流程
- 是-2,-3,0,那么cas将这个节点的状态设置为-1。
- 最后将当前线程park挂起,等待唤醒。
lock 释放锁
- 调用ReentrantLock中的tryRelease方法,释放锁
- 判断当前线程是不是持有锁的线程,如果不是,那么直接抛出异常
- 用当前state - 1 == 0 ? 如果是0,那么就将AQS中的锁线程设置为null。将计算结果赋值给state
- 如果state==0的话,获取head,不为null且状态不是0,那么就从后往前,找一个距离head最近的且状态是0或者-1的Node。unpark唤醒它。