锁的分类
按照Java虚拟机对锁的实现方式划分,Java平台中锁可分为内部锁和显示锁
- 内部锁:也被称为监视器锁,是一种排他锁,也就是独占锁,是通过synchronized关键字实现,在我们之前 深入理解synchronized关键字文章中有详细分析
- 显示锁:自jdk1.5开始引入的排他锁,是通过java.concurrent.locks.Lock接口的实现类,起作用与内部锁相同。它提供了一些内部锁不具备的特性。但并不是内部锁的替代品
Lock
Lock接口的实现类:
- ReentrantLock:重入锁,也是Lock接口的默认实现类
- ReentrantReadWriteLock:重入读写锁,排他锁的改进版
- StampedLock:Java 8引入了新的读写锁
今天我们只要来讲一下Lock接口的默认实现类ReentrantLock
ReentrantLock的使用
public static int count=0;
static Lock lock=new ReentrantLock();
public static void incr(){
lock.lock();//获得锁
try {
Thread.sleep(1);
count++;
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
lock.unlock();//释放锁
}
}
public static void main( String[] args ) throws InterruptedException {
for(int i=0;i<1000;i++){
new Thread(()->App.incr()).start();
}
Thread.sleep(3000); //保证线程执行结束
System.out.println("运行结果:"+count);
}
显示锁ReentrantLock的用法也很简单,通过Lock接口定义的lock方法和unlock方法分别用于申请和释放相应Lock实例所代表的锁。
ReentrantLock的分类
ReentrantLock 分为公平锁和非公平锁,可以通过构造方法来指定具体类型,默认为非公平锁:
public ReentrantLock() {
sync = new NonfairSync();//默认构造非公平锁
}
/**
* 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();
}
- 公平锁:指多个线程按照申请锁的顺序来获取锁,类似于排队打饭,先来后到,不能插队。
- 非公平锁:多个线程情况下获取锁的顺序并不是按照申请锁的顺序,谁抢到就是谁的。
ReentrantLock源码分析
ReentrantLock 默认为非公平锁,那么我们先来看一下非公平锁是如何实现的:
非公平锁获得锁
不同于公平锁按照顺序依次获得。非公平锁是抢占模式,一进来,就先去抢占锁。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
通过compareAndSetState(CAS)执行原子操作,通过地址拿到当前锁的state值,如果是0,就变成1,表示抢占到了锁,同时宣布主权,将当前占有该锁的线程修改为当前线程setExclusiveOwnerThread(Thread.currentThread())
private volatile int state;//0表示锁没有被占
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
否则,进入队列acquire(1)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
里面有三个方法:
- tryAcquire(arg):实现抢占和重入锁
- addWaiter(Node.EXCLUSIVE):添加到队列
- acquireQueued(node,arg):将没有获得锁的线程阻塞
我们来一个个分析
tryAcquire(arg)
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;
}
先拿到当前线程,然后再去看当前锁的状态,如果为0,就去抢占。
如果占有锁的线程与当前线程相同时,将state+1,并返回true,即lock方法执行完毕,这就是重入锁的实现
addWaiter(Node.EXCLUSIVE)
Node.EXCLUSIVE表示独占,与之相反的还有Node.SHARED共享
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
private Node addWaiter(Node mode) {
//初始化一个node
Node node = new Node(Thread.currentThread(), mode);
//最后一个node
Node pred = tail;
//链表不为空,直接将节点添加到链表后面
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
队列是通过双向链表实现的,所以每个node都会有一个next指向下一个节点,一个pred指向上一个节点,这里就是把新来的节点插入到链表
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
//创建一个假的head节点,后面会用到
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
这里需要注意的一个点:当tail为空的时候,创建一个虚拟节点同时指向head和tail,为什么会有一个虚拟节点呢?我们后面会讲到。
acquireQueued(node,arg)
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//采用自旋方式,一直循环
for (;;) {
//获得当前线程节点的前一个节点
final Node p = node.predecessor();
// 如果前一个节点是 head ,tryAcquire()就尝试获取锁
// 如果 获取成功,就将当前节点设置为 head,注意 head 节点是永远不会唤醒的。
//因为head没有前一个node
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//获取不到锁,就需要阻塞当前线程
//阻塞之前,需要改变前一个节点的状态。如果是 SIGNAL 就阻塞,否则就改成 SIGNAL
//这是为了提醒前一个节点释放锁完后能够叫醒自己
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
采用自旋的方式一致循环。拿到当前节点的前一个节点,如果前一个节点是head节点,再次执行tryAcquire(arg)
如果成功占有锁,则将当前节点变成head节点,同时,将与之前head节点的链接断开
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
当占有锁失败后就需要阻塞当前线程,具体逻辑在shouldParkAfterFailedAcquire方法里
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
//前一个节点状态为SIGNAL,可以阻塞
if (ws == Node.SIGNAL)
return true;
//前一个节点状态为取消,需要跳过前一级,再试
if (ws > 0) {
do {
//将前一级的前一级赋值给当前节点的前一级
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//通过cas操作将状态改为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
AbstractQueuedSynchronizer(AQS)通过waitStatus状态去管理每个节点,默认值为0
- SIGNAL = -1 :表示后续线程需要释放
- CANCELLED = 1:表示线程已取消
所以,每个节点在休眠前,需要将前一个节点的waitStatus设置为SIGNAL,否则,自己将无法被唤醒
所以,我们才要在没有队列的时候去创建一个虚拟的head节点。
公平锁获得锁
公平锁与非公平锁的差异主要在获取锁:
公平锁就相当于买票,后来的人需要排到队尾依次买票,不能插队。所以,在第一部尝试获得锁的时候需要去判断有没有队列,而不是直接去抢占
final void lock() {
acquire(1);
}
第一次尝试获得锁:
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;
}
需要去判断队列是否为空:
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());
}
其他的就和非公平锁的实现方式差不多,这里就不展示了。接下来,我们再看看释放锁的方法
释放锁
公平锁和非公平锁的释放流程都是一样的:
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
//将锁的状态减一
if (tryRelease(arg)) {
Node h = head;
//因为所有的节点在将自己挂起之前,都会将前置节点设置成 SIGNAL,希望前置节点释放的时候,唤醒自己。
// 如果前置节点是 0 ,说明前置节点已经释放过了。
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
通过tryRelease方法,先将锁的state值减一
protected final boolean tryRelease(int releases) {
//将锁的state-1
//因为有重入锁,所以state可能大于1
int c = getState() - releases;
//如果当前线程不是当前拥有锁的线程,报异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//当拥有锁的线程取消后,将拥有锁线程的值变为null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
再去获得head节点,去查看head的waitStatus状态,因为所有的节点在将自己挂起之前,都会将前置节点设置成 SIGNAL,所以如果有后续需要被唤醒的线程的话,head的waitStatus一定是为1的。如果前置节点是 0 ,说明前置节点已经释放过了,不需要重复操作。
接着执行unparkSuccessor方法,将节点的waitStatus恢复为0初始状态,防止重复操作
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//判断当前节点的下一个节点
//如果节点为空,或者状态为取消,则需要向后移动以找到实际的非取消后继者。
Node s = node.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);
}
最后,唤醒下一个休眠的线程。唤醒之后呢,在接着将自己设置为head,并将前任head与自己的链接断掉。
总结
由于公平锁每次在占有锁之前都需要去判断一下是否存在队列。如果有线程在等待,需要将自己的挂起,同时唤醒最前面的线程,造成了大量的线程切换,而非公平锁则没有这个限制。这也导致非公平锁的效率会被公平锁更高。