JDK1.8
前言
最开始,Java自带Synchronized关键字保证并发安全,但是它最大的缺点就是它是个重量级锁,线程唤醒和挂起会触发用户态和内核态的转换,主要是这个过程很耗费资源和性能。
基于这个问题,ReentrantLock出现了,在JDK层面上解决线程安全问题,利用FIFO队列、CAS操作、park\unpark系统调用。
并发模式有两种:交替执行和并发执行。在JDK1.6之前,即使是交替执行模式,Synchronized的做法也是当一个线程来了,就要加锁,执行完了,就要释放锁。而ReentrantLock的做法在这种无锁竞争的情况下连park\unpark系统调用都不需要使用,完全在JDK层面就保证了并发安全。
但是后来,JDK1.6开始,Synchronized进行了锁升级的优化,它的性能也和ReentrantLock不相上下,至于到底谁的性能更好,我没测试过,不过,这两种锁的差别还是挺大的,比如ReentrantLock是可中断锁,能设置超时放弃等锁等优点,因此只能说,选择谁还是要看具体的业务。
用法示例
public class Test1 {
// 非公平锁
// private static ReentrantLock reentrantLock = new ReentrantLock();
// 公平锁
private static ReentrantLock reentrantLock = new ReentrantLock(true);
public static void main(String[] args) {
Thread t1 = new Thread() {
@Override
public void run() {
Test1.getName();
}
};
Thread t2 = new Thread() {
@Override
public void run() {
Test1.getName();
}
};
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
public static void getName() {
reentrantLock.lock();
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
reentrantLock.unlock();
}
}
}
公平锁和非公平锁加锁流程
公平锁
先用这个例子来分析源码:
public class Test1 {
// 公平锁
private static ReentrantLock reentrantLock = new ReentrantLock(true);
public static void main(String[] args) {
Thread t1 = new Thread() {
@Override
public void run() {
Test1.getName();
}
};
t1.setName("t1");
t1.start();
}
public static void getName() {
// 只要调用这个lock方法能正常返回,则加锁成功
reentrantLock.lock();
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
reentrantLock.unlock();
}
}
}
执行new ReentrantLock(true);
、调用reentrantLock.lock();
源码是这样:
package java.util.concurrent.locks;
public class ReentrantLock implements Lock, java.io.Serializable {
// ....忽略很多源码
// 1.
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
// 2.
public void lock() {
sync.lock();
}
// 上面的sync是这个静态内部类的继承类的实例,调用lock方法,实际调用的是继承了Sync类的子类中重写的lock()方法
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
/**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
// 3. 实际是这个静态内部类中的方法
abstract void lock();
// ....忽略很多源码
}
// ....忽略很多源码
}
查看lock()
的两个实现方法:
方法的实现还是在同一个类中,FairSync()表示公平锁,NonfairSync()是非公平锁,前边示例中我们构造的是公平锁,所以这里先来看下FairSync():
package java.util.concurrent.locks;
public class ReentrantLock implements Lock, java.io.Serializable {
// ....忽略很多源码
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
/**
* 4.
* acquire()方法会调用java.util.concurrent.locks.AbstractQueuedSynchronizer
* 下的方法:
public final void acquire(int arg) {
// tryAcquire()返回true,表示加锁成功,取反后值为false,直接返回。
// tryAcquire()返回false,表示加锁失败,则把当前线程放入等锁队列中
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
*/
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
/**
* 5. 第4步注释中的tryAcquire()方法,在公平锁中的实现就是下面这个tryAcquire()方法,
* 所以我们的重点来到这个方法上,acquires参数此时的值是1,表示要把锁的状态state加1
*/
protected final boolean tryAcquire(int acquires) {
// 5.1 获取当前线程
final Thread current = Thread.currentThread();
/**
* 5.2 这个getState获取的是java.util.concurrent.locks.AbstractQueuedSynchronizer
* 类下的state变量的值,当有线程获取到锁时,这个值加1,
* 等于0表示当前还没有线程获取到锁
*/
int c = getState();
if (c == 0) {
/**
* 5.3 当前这个线程判断到state等于0,但是并不代表它就能获取到锁,因为
* 有可能队列中还有等锁的线程,只是这个线程来的时候,持有锁
* 的线程刚好释放锁而已。所以,hasQueuedPredecessors()方法就是
* 去判断是否要把当前线程加入队列,这个方法在《JUC——ReentrantLock源码简读(中)》博文中单独来说
*
* 如果hasQueuedPredecessors()返回fale,则进行CAS操作,
* compareAndSetState(0, acquires)中,0表示期望的state值,acquires表示要把state修改的值
*/
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 把当前线程设置为持有锁的线程
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
/**
* 5.3^ 如果当前线程等于持有锁的线程,说明是线程重入锁,则把state值加1
* (ReentrantLock是可重入锁)
*/
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
}
非公平锁
上面是公平锁尝试获取锁的流程,非公平锁有什么不同呢,调用sync.lock的时候,会进入到NonfairSync中:
package java.util.concurrent.locks;
public class ReentrantLock implements Lock, java.io.Serializable {
// ....忽略很多源码
/**
* 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() {
/**
* 非公平锁中,调用lock()方法,先执行CAS操作尝试加锁,不管队列中有没有等锁线程,
* 即使队列中有等锁线程,当前线程来的到时候,持有锁的线程刚好释放锁,那它就可能
* 获取到锁
*/
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
/**
* 如果CAS操作失败,说明当前锁被其他线程持有,则调用acquire()方法
* 和公平锁中提到的是同一个acquire()方法:
* java.util.concurrent.locks.AbstractQueuedSynchronizer下的方法:
public final void acquire(int arg) {
// tryAcquire()返回true,表示加锁成功,取反后值为false,直接返回。
// tryAcquire()返回false,表示加锁失败,则把当前线程放入等锁队列中
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
*/
acquire(1);
}
// 上面注释中提到的tryAcquire()方法就是非公平锁中的这个方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
// ....忽略很多源码
// nonfairTryAcquire()方法的实现在Sync类中
abstract static class Sync extends AbstractQueuedSynchronizer {
// ....忽略很多源码
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
// 可以看到这个方法的执行逻辑和公平锁的tryAcquire()的执行逻辑的一个区别就是
// 非公平锁不需要进行是否入队的判断
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 如果此时锁的状态是0,则执行CAS操作
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;
}
// 如果CAS操作失败或者锁重入失败都返回false
return false;
}
}
}
AQS队列
AbstractQueuedSynchronizer中,这里会用到的几个主要属性:
private transient volatile Node head; // 队头
private transient volatile Node tail; // 队尾
private volatile int state; // 锁的状态值(0:没有线程获取锁,等于1:有线程获取到锁,大于1:获取锁的线程重入)
volatile Thread thread; // 获取到锁的线程
上一节公平锁或者是非公平锁中,调用Sync.lock()方法的时候,如果是公平锁,进入FairSync类中的lock()方法,先执行acquire()方法;如果是非公平锁,进入NonfairSync类中的lock()方法,先执行一次CAS操作尝试获取锁,如果失败,则调用acquire()方法,acquire()方法的源码:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
//....忽略很多源码
public final void acquire(int arg) {
// tryAcquire()返回true,表示加锁成功,取反后值为false,直接返回。
// tryAcquire()返回false,表示加锁失败,则把当前线程放入等锁队列中
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
}
其中,tryAcquire()方法的实现在FairSync类或者NonfairSync类中,都是尝试去获取锁,
- 如果获取锁成功,tryAcquire()方法返回true,则acquire()方法就算是调用完成了,然后一路返回到我们自己在getName()方法中写的
reentrantLock.lock();
,拿到锁继续往下执行业务代码。 - 如果获取锁失败,tryAcquire()方法返回false,则继续执行
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
现在就来讨论acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
addWaiter()方法
队列中存储的是Node类型的节点,由于线程获取锁失败,那它就要调用addWaiter()加入到等锁队列中。Node和addWaiter()方法源码:
package java.util.concurrent.locks;
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
//...忽略部分源码
static final class Node {
//...
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
// 指向前一个节点
volatile Node prev;
// 指向下一个节点
volatile Node next;
// 记录当前节点的线程
volatile Thread thread;
// 当前线程的状态
volatile int waitStatus
}
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
// 1. 实例化一个Node节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
// 2. 队列不为空,直接把当前节点插入到队列尾部
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 2^. 如果队列为空,则初始化队列并加入node节点
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
// 如果队列的尾部为null,说明此时队列为空,
// 则实例化一个空节点(Node的Thread属性为null),
// 让队列的头指针和尾指针指向这个节点
// 继续for循环
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 如果队列不为空,把当前节点放到队列的尾部
node.prev = t;
// 设置尾部,退出循环
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
}
现在用图来描述上面入队的过程,假设t1线程持有锁正在执行,t2线程来了,第一次for循环,实例化一个节点作为头结点:
第二次for循环:
acquireQueued()方法
t2节点入队之后,调用这个方法:
package java.util.concurrent.locks;
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
//...忽略部分源码
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 注意这个for循环的退出条件
for (;;) {
// 3. 拿到当前节点的上一个节点
final Node p = node.predecessor();
// 4.
//判断上一个节点是不是头节点
// 如果是,证明队列中就只有当前线程在排队等锁
// 调用tryAcquire()进行自旋【第一次自旋】尝试获取锁,这个方法就是
// FairSync或者NonfairSync类中的方法
//
// 之所以有自旋操作,因为在当前线程执行入队等任务时,
// 之前持有锁的线程可能已经释放锁了,那这个线程就没必要在队列中等待了
if (p == head && tryAcquire(arg)) {
// 当前线程拿到锁之后,把当前线程节点设为头结点
setHead(node);
// 让GC回收之前的头结点
p.next = null; // help GC
failed = false;
return interrupted;
}
// 4^.
//上一个节点不是头节点或者是头节点但是当前节点自旋失败,
//调用shouldParkAfterFailedAcquire()方法,
//意思是在自旋一次加锁失败之后要不要执行park,
// 第一次调用这个方法会返回false,继续执行for循环自旋【第二次自旋】,假设还是没获取到锁,
// 第二次调用这个方法返回true,调用parkAndCheckInterrupt()方法执行park,阻塞当前线程(park是操作系统提供的API)
// 直到线程被唤醒唤醒之后,去判断当前线程有没有被中断,如果没有,则继续执行第三次for循环尝试锁
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;
}
// 参数pred是当前节点的上一个节点,node是当前节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 拿到上一个节点的状态,默认是0
int ws = pred.waitStatus;
// Node.SIGNAL是-1
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
// 把上一个节点的状态改成-1,表示处于睡眠状态
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
}
当节点插入到队列中后,就会执行acquireQueued()方法,上面从源码角度分析了这个过程,下面再用图来描述这个过程。
图解
假设有t0、t1、t2三个线程,t0最先拿到锁去执行,然后t1获取锁失败,调用addWaiter()入队列之后,再调用acquireQueued()方法,假设自旋一次后获取锁失败,则调用shouldParkAfterFailedAcquire()方法,判断到前一个节点的waitStatus等于0,则把它的waitStatus改为-1后返回false,并在第二次调用shouldParkAfterFailedAcquire()方法后返回true,继续调用parkAndCheckInterrupt()后执行park,t1线程阻塞住:
此时t2线程来了,判断队列不是空,则直接加到队列尾部:
t2线程执行到acquireQueued()方法的时候,也会把t1的waitStatus状态改成-1,然后自己阻塞住:
waitStatus=-1表示该线程处于睡眠状态,为什么不是一进入队列的时候就进入睡眠状态也就是说为什么不自己把自己的waitStatus值改成-1?
因为线程调用addWaiter()入队之后,立即调用acquireQueued()方法,按照上面那3副图,t2入队之后:
- 假设此时t1释放锁了(t0在执行任务,不在队列中,不是上面图的第一个节点哦!第一个节点是默认初始化队列时加上的一个节点,thread属性是null),那在acquireQueued()方法的第一次for循环中,通过一次自旋,t2就获取到了锁,那就不用阻塞到队列中了,所以不能在一进入队列中就把waitStatus改成-1去表示自己睡眠了。
- 那如果t2没有获取到锁(说明t0或者t1还在执行),在acquireQueued()方法的第一次for循环中把前一个节点的waitStatus改成-1,如果前一个节点是头结点,表示持有锁的线程还在执行,如果前一个节点是t1,那说明t1都还没获取到锁,t2执行acquireQueued()方法的第二次for循环的时候,判断前一个节点还没获取到锁,就把人家的waitStatus改成-1,表示它真的在睡眠,因为t1睡眠了,肯定没办法自己改自己的状态,只能由别人(下一个等锁的线程,就是这里的t2)去改。
.关于自旋:
- 如果是t1节点,acquireQueued()方法中调用第一次自旋失败后,把前一个节点waitStatus改成-1,第二次自旋失败后,自己就阻塞住了
- 如果是t2节点(假设此时t1还在队列中),就没有自旋操作,前一个节点都还在队列中,它自旋有什么意义呢!它只是在acquireQueued()方法的第一次for循环中,把前一个节点状态改成-1,第二次for循环中,自己就阻塞了
以上就是获取到锁的线程在执行,没获取到锁的线程在队里中等待,下面来讨论释放锁的情况。
释放锁
调用我们自己的getName()方法中的reentrantLock.unlock();
的时候触发释放锁:
public class ReentrantLock implements Lock, java.io.Serializable {
//...省略很多源码
// 1.
public void unlock() {
sync.release(1);
}
abstract static class Sync extends AbstractQueuedSynchronizer {
//3.
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) {
// 如果减到0了,就把队列的thread属性置为null
free = true;
setExclusiveOwnerThread(null);
}
// 修改队列的state属性值
setState(c);
// 返回true,表示要释放锁
// 返回false,表示当前只是释放重入锁一次,真正的锁还不能释放
return free;
}
}
}
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
//...省略很多源码
//2.
public final boolean release(int arg) {
//4. 如果tryRelease()返回true,表示要释放锁
if (tryRelease(arg)) {
Node h = head;
// 如果头结点不是null并且头结点的状态不是0,则唤醒下一个节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
}
图解
还是接着上面加锁的那副图来描述释放锁的过程,下图是之前等锁队列的状态:
此时t0释放锁,执行tryRelease()之后:
然后进入release()方法中的if语句块,此时头节点h不等于null,并且h.waitStatus不等于0(不等于0表示有下一个节点在等锁),就把下一个节点t1唤醒。
t1是在acquireQueued()方法中调用parkAndCheckInterrupt()方法之后阻塞住的,因此它接着这个方法往后执行,这里为了方便看,再贴下acquireQueued()方法:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
//... 省略很多源码
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)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) // t1在这里跌倒,就从这里爬起
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
}
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());
}
t1第三次执行acquireQueued()中的for循环,进入tryAcquire()方法,锁的状态state=0,然后调用hasQueuedPredecessors()方法,此时h != t,s = t1,s!=null,s.thread == Thread.currentThread(),所以返回false,t1可以继续执行CAS,成功后把state设置为1,thread设置为t1,拿到锁返回true,继续执行acquireQueued()中的if语句块之后:
当t1释放锁,执行tryRelease()之后:
然后进入release()方法中的if语句块,将t2唤醒,t2再从acquireQueued()阻塞的地方开始执行第3次for循环,进入tryAcquire()方法,调用hasQueuedPredecessors()方法,此时h != t,s = t2,s!=null,s.thread == Thread.currentThread(),返回false,t1可以继续执行CAS,成功后把state设置为1,thread设置为t2,拿到锁返回true,继续执行acquireQueued()中的if语句块之后:
当t2释放锁,执行tryRelease()之后:
然后进入release()方法中的if语句块,此时h不等于null,但是h.waitStatus等于0,表示没有线程在等t2释放锁,因此不会执行unparkSuccessor(h);