1 概述
Java程序提供两种方法来实现锁功能1)synchronized关键字和2)lock接口,两者最大区别是synchronized是Java内置的关键字,是JVM层面的锁;而lock是java.util.concurrent包下面提供的工具,由Java代码编写的。在JDK1.6之前,synchronized是重量级锁,每次都会调用OS使线程进入阻塞状态;ReentrantLock是轻量锁,如果只是当单纯线程交替等场景,是不会触发OS的,性能优于synchronized关键字。现在两者性能已经差不多,未来随着JVM不断优化,synchronized性能不断提高,很大程度会超过lock接口。
2 AQS简介
队列同步器AbstractQueuedSynchronizer(AQS)使用一个int类型成员变量state表示同步状态,通过内置的FIFO队列实现多线程排队,是ReentrantLock框架的基础。
//同步队列容器
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
//内部节点
static final class Node {
//独占式
static final Node EXCLUSIVE = null;
static final int SIGNAL = -1;
//等待状态,初始值为0;当waitStatus为-1时,说明后继节点得线程处于等待状态
volatile int waitStatus;
//前驱节点
volatile Node prev;
//后继节点
volatile Node next;
//当前节点存储的线程
volatile Thread thread
//线程状态
volatile int waitStatus;
...
}
//AQS头指针
private transient volatile Node head;
//AQS尾指针
private transient volatile Node tail;
//同步状态,默认为0
private volatile int state;
//独占线程,用于锁重入场景,判读是否当前线程持有锁
private transient Thread exclusiveOwnerThread;
}
AQS通过一个FIFO队列,使没有抢占到锁的线程在队列中进行排队。head指向的node节点1是头节点,里面的线程永远为空,是因为代表的线程已经抢占到锁,不会被放在等待队列中。
节点2才是真正意义上第一个在老老实实排队的线程。尾节点指向节点3,代表最后一个排队的节点。
3 加锁用例
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockApp {
private static ReentrantLock reentrantLock = new ReentrantLock(true);
public static void main(String[] args) {
Thread t1 = new Thread(){
@Override
public void run() {
testLock1();
}
};
t1.setName("thread1");
Thread t2 = new Thread(){
@Override
public void run() {
testLock1();
}
};
t2.setName("thread2");
t1.start();
t2.start();
}
public static void testLock1(){
reentrantLock.lock();
System.out.println(Thread.currentThread().getName() + "获取锁了");
try {
Thread.sleep(2000);
//锁重入
testLock2();
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "释放锁了");
reentrantLock.unlock();
}
public static void testLock2(){
reentrantLock.lock();
System.out.println(Thread.currentThread().getName() + "再次获取锁了");
try {
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "再次释放锁了");
reentrantLock.unlock();
}
}
打印结果:
thread1获取锁了
thread1再次获取锁了
thread1再次释放锁了
thread1释放锁了
thread2获取锁了
thread2再次获取锁了
thread2再次释放锁了
thread2释放锁了
从控制台可以看出,只有获取锁的线程执行完,才会轮到下一个线程。这是因为ReentrantLock是个独占锁,每次只允许一个线程拿到锁。
thread1在持有锁的线程同时,还可以直接再次获取锁,是因为发生了锁重入。
4 构造函数
ReentrantLock由两种实现方式,无参构造函数默认是非公平锁,线程获锁的方式是随机的。而有参构造方法的入参为true时表示公平锁,是按照线程加锁的顺序获得锁,即FIFO方式。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
5 公平锁的加锁机制
调用lock方法进行加锁,这里的1是次数
final void lock() {
acquire(1);
}
public final void acquire(int arg) {
//tryAcquire返回值为ture,线程获获取到锁,并更改同步状态;
// tryAcquire返回值为false,添加到等待队列
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
同步状态有3种可能的值 1) 0 没有被占用 2)1 被占用 3)n(n >1)同一个线程锁重入了n -1 次。当前线程尝试获取同步状态,如果同步状态C为0,则当前线程获得锁,被设置为独占线程,或者当前线程已经是独占线程,那么就发生锁重入。两种情况都不是,则同步状态被其他线程改变,当前线程需要排队。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取同步状态
int c = getState();
//同步状态为0,说明锁没有被占用
if (c == 0) {
//判断等待队列中是否有其他线程在排队
if (!hasQueuedPredecessors() &&
//修改同步状态值为1
compareAndSetState(0, acquires)) {
//设置拥有独占模式的线程为当前线程
setExclusiveOwnerThread(current);
return true;
}
}
//同步状态值不为0,说明锁被占用,但是当前线程为独占线程,可以锁重入
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判断线程需不需要排队,返回结果true表示需要排队,false表示不需要
public final boolean hasQueuedPredecessors() {
// 尾节点
Node t = tail;
// 头节点
Node h = head;
//临时节点
Node s;
//h == t :说明此时没有线程在队列里面排队,h和t有可能没有初始化或者两者刚初始化后,共同指向一个包含的线程为null的node
// h != t 有可能是1)h刚初始化,t还没初始化 2)队列里面有其他线程在排队
//(s = h.next)== null :为true说明有其他线程已经执行完AQS的头节点和尾节点初始化操作,根据FIFO原则,当前线程需要排队
//s.thread != Thread.currentThread():为true说明头节点的后继节点中存储的线不是当前线程,当前线程需要排队
//h指向第一个节点,s此时是队列中的第二个节点
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
通过CAS操作,修改同步状态
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
将线程设置为独占线程,用于锁重入场景,可以判断线程是否持有锁
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
调用addWaiter将线程添加到等待队列中
private Node addWaiter(Node mode) {
//初始化一个独占模式的节点
Node node = new Node(Thread.currentThread(), mode);
//尾节点赋值给pred
Node pred = tail;
//末尾节点不为空,说明他线程已经初始化head节点和tail节点
if (pred != null) {
//将node的前驱节点指向tail节点
node.prev = pred;
//将node设置为尾节点
if (compareAndSetTail(pred, node)) {
//旧的tail节点的后继指针指向node节点
pred.next = node;
//返回当前节点
return node;
}
}
//1)tail节点为空 或者 2)taili节点不为空但compareAndSetTail操作失败时,调用enq
enq(node);
return node;
}
enq方法是将节点插入到等待队列中
private Node enq(final Node node) {
//无限循环
for (;;) {
Node t = tail;
//首次添加时,head和tail都为null,需要初始化
if (t == null) {
//实列化一个空节点,通过CAS操作添加成头节点
if (compareAndSetHead(new Node()))
//赋值给尾节点,此时两者共同指向空的node节点
tail = head;
} else {
//将node的前驱节点指向tail节点
node.prev = t;
//将node设置为尾节点
if (compareAndSetTail(t, node)) {
//旧的tail节点的后继指针指向node节点
t.next = node;
//返回旧的tail节点
return t;
}
}
}
}
通过自选尝试获取锁,自选2次失败则线程会进入阻塞。从这里可以看出ReentrantLock并没有让获取不到锁的线程直接阻塞,而是通过FIFI等待队列,自旋等操作,尽量避免线程进入阻塞状态,降低线程状态切换的代价。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//自旋
for (;;) {
//p为node节点的前继节点
final Node p = node.predecessor();
// 队头head指向的node里面得thread永远为空,因此第二个节点才是实际排队第一位的节点
//如果前继节点为头节点,且成功更新同步状态,说名当前线程获得锁
if (p == head && tryAcquire(arg)) {
//将第二个节点设为头节点
setHead(node);
//释放旧的头节点
p.next = null;
failed = false;
return interrupted;
}
//同个线程自选2次(调用两次shouldParkAfterFailedAcquire方法)才会返回true
if (shouldParkAfterFailedAcquire(p, node) &&
//将线程阻塞
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
调用shouldParkAfterFailedAcquire修改前继节点的等待状态,连续调用两次,就会进入parkAndCheckInterrupt()使线程进入阻塞状态
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//线程首次进来,其前继节点的为等待状态为0
int ws = pred.waitStatus;
//线程第二次调用shouldParkAfterFailedAcquire时,ws为-1,,返回true
//线程第二次调用shouldParkAfterFailedAcquire才返回ture,是为了让线程多自选一次,尽量避免进入阻塞状态
if (ws == Node.SIGNAL)
return true;
if (ws > 0)
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//线程第一次调用shouldParkAfterFailedAcquire时,前继节点的ws=0,通过cas将ws修改为-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//线程进入阻塞状态
private final boolean parkAndCheckInterrupt() {
//通过unsafe类的park方法阻塞线程
LockSupport.park(this);
return Thread.interrupted();
}
6 非公平锁的加锁机制
调用默认无参构造函数,使用非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
public void lock() {
sync.lock();
}
非公平锁比公平锁的加锁过程对比,就是简单粗暴
final void lock() {
//全部线程都能抢占,由于CAS是原子操作,因此只有一个线程能成功执行状态更新操作
if (compareAndSetState(0, 1))
//将抢占到锁的线程
setExclusiveOwnerThread(Thread.currentThread());
else
//获取锁失败的线程就走公平锁那套,老老实实去排队
acquire(1);
}
7 锁的解锁机制
加锁时候入参也是1,表示加锁1次,同理解锁时候也是1,表示解锁1次
public void unlock() {
sync.release(1);
}
更新同步状态,如果没发送
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
//h != null 说明头节点指向包含空线程的node节点1
//waitStatus !=0 说明node节点2处于阻塞状态,需要被唤醒
if (h != null && h.waitStatus != 0)
//唤醒node节点2
unparkSuccessor(h);
return true;
}
return false;
}
执行同步状态减1操作后,同步状态值为0返回ture;大于0返回false
protected final boolean tryRelease(int releases) {
//同步状态减1
int c = getState() - releases;
//判断是否独占线程
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//为0说明没有被占用
if (c == 0) {
free = true;
//将独占线程指向空
setExclusiveOwnerThread(null);
}
//修改同步状态值
setState(c);
//同步状态为0,free为true;同步状态大于0,说明发生了锁重入,当前线程还是持有锁,free返回false
return free;
}
更新同步状态值
protected final void setState(int newState) {
state = newState;
}
唤醒处于阻塞状态的第一个真正在排队的节点
private void unparkSuccessor(Node node) {
//获取等待状态
int ws = node.waitStatus;
//等待状态小于0,说明后继节点处于阻塞状态
if (ws < 0)
//将node节点的等待状态修改为初始值0
compareAndSetWaitStatus(node, ws, 0);
//获取当前节点的后继节点
Node s = node.next;
/后继节点为空或被取消
if (s == null || s.waitStatus > 0) {
s = null;
//从后往前找,找到最靠前的等待状态不大于0的节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
//将找到的目标节点赋值s
s = t;
}
//后继节点不为空,唤醒后继节点
if (s != null)
LockSupport.unpark(s.thread);
}
通过UNSAFE非安全类执行唤醒操作
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
8 参考文献
- JDK7在线文档
https://tool.oschina.net/apidocs/apidoc?api=jdk_7u4 - JDK8在线文档
https://docs.oracle.com/javase/8/docs/api/ - Bruce Eckel,Java编程思想 第4版. 2007, 机械工业出版社