您的位置:首页 > 编程语言 > Java开发

java多线程系列--AQS-01之独占锁原理浅析

2017-05-01 21:19 465 查看

概述

锁是用来控制多个线程在同一时间访问共享资源的方式,一般来讲锁能防止多个线程同时访问共享资源从而达到线程安全的访问(有些共享锁是允许多个线程同时访问共享资源的,比如读写锁)。

我们已经有了synchronized关键字,JUC为啥要新增Lock接口用来实现锁

支持锁的中断响应:当获取锁时线程被中断,中断异常将被抛出,同时锁会被释放

锁申请等待限时:超过给定时间还未能获取到锁则返回,而不会一直阻塞等待

锁的公平性:支持公平锁的选择,synchrinzed只能是非公平锁

尝试非阻塞地获取锁:tryLock(),如果获取锁失败则直接返回,而不会阻塞

队列同步器(AQS)

队列同步器AbstractQueuedSynchronizer,是用来构建锁或者其他同步器的基础框架,使用一个int类型的全局变量来表示同步状态,通过CAS的方法将未获取到锁的线程封装成一个个node添加一个双向链表中。

AQS内部属性

//头结点,头结点的next属性永远指向双向链表的第一个节点,但它本身是不指向第一个节点的
//头结点永远指向获取锁成功的节点(初始化的时候除外,那时指向的是一个临时节点)
private transient volatile Node head;
//尾节点,永远指向双向链表的最后一个节点
private transient volatile Node tail;
//共享变量state
private volatile int state;


node属性

static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
//线程被取消
static final int CANCELLED =  1;
// “当前线程的后继线程需要被unpark(唤醒)”,对应的waitStatus的值。
// 一般发生情况是:当前线程的后继线程处于阻塞状态,而当前线程被
//release或cancel掉,因此需要唤醒当前线程的后继线程。
static final int SIGNAL    = -1;
//线程(处在Condition休眠状态)在等待Condition唤醒
static final int CONDITION = -2;
//(共享锁)其它线程获取到“共享锁”
static final int PROPAGATE = -3;
//线程的等待状态对应上面的null、1、-1、-2、-3的几个状态
volatile int waitStatus;
//存放线程对象
volatile Thread thread;
Node nextWaiter;
//当前节点指向的下一个节点
volatile Node next;
}


我们可以看到AQS实际上就是一个双线链表,将获取锁失败的线程封装成一个Node节点,里面包含线程的引用、等待状态以及前驱和后继节点。

排它锁构造方法

public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}


我们发现属性sync有两种实现,默认是非公平锁,那我们先看非公平锁NonfairSync的lock()方法的底层实现.

//可以看到非公平锁一开始就直接尝试获取锁
final void lock() {
if (compareAndSetState(0, 1))//state默认是0,通过CAS的方式尝试修改成1
setExclusiveOwnerThread(Thread.currentThread());//设置同步器的线程占用者设置成当前线程
else
acquire(1);
}

protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}

public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
selfInterrupt();
}
}


lock()方法if里面的业务逻辑相对简单,如果成功将state设置1,表示获取锁成功线程将跳出lock()继续向下执行,否则执行acquire(1),首先看tryAcquire(arg)的实现(arg=1)

//再次尝试获取锁,获取成功返回true否则返回false,如果是可重入锁则直接修改可重入次数
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//如果state=0即同步器没有被占用则再次尝试修改state为1
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;
}


首先获取state的值,如果state=0,则重复之前的动作再次尝试修改state值并修改同步器的线程拥有者为当前线程,获取锁成功返回true,线程将跳出lock()方法继续向下执行。

如果同步器的状态不等于0但同步器的线程占用者为当前线程则将state加一,这里是可重入锁的业务逻辑,获取锁成功然后跳出lock()方法继续向下执行

如果state!=0且当期线程不是同步器的拥有者返回false去执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

我们先看addWaiter(Node.EXCLUSIVE)方法

//构造双向链表,将节点添加到双向链表的尾部
private Node addWaiter(Node mode) {
//将当前线程封装成一个node节点
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
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;
//首先初始化AQS中head、tail属性,然后跳出进行下次循环执行else代码
//假设h=new Node();那么if里面的这段代码等价于tail=h;head=h;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//将node的前驱节点指向head(此时head和tail都是同一引用)
//这段代码等价于node.prev=h;
node.prev = t;
//将AQS的tail属性设置为node
//等价于tail=node
if (compareAndSetTail(t, node)) {
//等价于h.next=node;即等价于head.next=node;
t.next = node;
return t;
}
}
}
}


上面就是初始化头尾节点和将头结点的next指向新增加的节点将尾节点直接指向新增加节点,注意了,head.next才指向的是新节点,而tail是直接指向新节点的,即head.next=node;tail=node;最终形成是数据结构如下图所示



当再新增新的节点后继续循环将直接执行else中的代码,if中的代码不会再执行因为t!=null了,最后AQS双向链表如下图



然后我们在看看acquireQueued()方法

//处理双向链表中的节点,未获取到锁的线程被park阻塞,等到重新被唤醒的时候再继续尝试获取锁
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//获取node的前置节点
final Node p = node.predecessor();
//如果head的后继节点是当前线程则尝试获取锁
//这里不会和等待队列中的线程发生竞争,但可能会与正在获取锁但尚未
//进入等待队列中的线程竞争,因为非公平锁在未进入
//等待队列就会尝试获取锁,这里的p==head一定要加这里是公平锁的保证
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
//将该node节点设置成头结点,并将该该节点的前置节点设置null
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}


我们首先可以看到这里使用了一个死循环,先看第一个if里面的逻辑,我们发现当占用锁的线程释放锁后,只有前置是头结点的节点线程才有机会获取锁。并且要将state修改成1才是真正成功。最终双向链表如图所示(前提的第一个if执行成功),此时的node1是成功获取锁的节点



我们再看看第二个if里面的逻辑

//判断当前线程是否需要被阻塞,pred是当前节点的前置节点,node是当前节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}


当shouldParkAfterFailedAcquire()方法第一次被调用那么pred就是head,ws=null;这个方法将直接执行compareAndSetWaitStatus(pred, ws, Node.SIGNAL);此时前置节点即ws.waitStatus= Node.SIGNAL;返回false再次进行循环继续调用该方法;此时将会直接执行

if(ws == Node.SIGNAL){
return true;
}


跳出循环,然后执行parkAndCheckInterrupt()方法

//阻塞当前线程返回线程的中断状态
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
//线程会被阻塞在这里,知道被unpark()或者中断(公平锁的中断没什么卵用
//后面还是会被park()因为获取锁的时候会判断是否是头结点)
return Thread.interrupted();
}


这里直接阻塞了该线程,线程会一直阻塞在这里,直到被中断或被调用unpark()然后返回该现在的中断状态.

非公平锁

非公平锁其实和公平锁差不多,主要是在获取锁的时候有细微的差别

//非公平锁
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//公平锁
final void lock() {
acquire(1);
}


//非公平锁,直接就尝试获取锁
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()) {
...
}
return false;
}
//公平锁会先调用hasQueuedPredecessors()判断该线程节点是不是位于CLH队列头部才获取锁
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()) {
...
}
return false;
}


释放

释放都是调用unlock(),没有公平和非公平的区别

public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}


我们首先看看tryRelease(arg)

protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}


这段代码的逻辑很简单,释放一次就将state的值减一,如果state的值不等于0,表示释放不成功,因为是可重入锁就有可能调用lock()多次,那么如果这里不是调用相同次数的unlock()那么无法真正释放。如果释放成功,那么将同步器的当前线程拥有者设置为null,返回true。然后执行unparkSuccessor(h)

private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
//将头结点的state标识置为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);

Node s = node.next;
//如果没有使用其他juc的api(比如condition、semphore...)这里的ws就等于-1,那么就不会执行if里面的逻辑
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;
}
//直接唤醒head的后置节点
if (s != null)
LockSupport.unpark(s.thread);
}


该方法传入的参数是head头结点,如果这里没有使用其他juc的api(比如condition、semphore…)这里的ws就等于-1,然后从头开始遍历找,知道找到节点状态为SIGNAL的然后唤醒他。此时他将执行acquireQueued()中死循环里面的方法获取锁,如果成功将获取锁跳出死循环结束。

小结

无论是公平锁还是非公平锁都必须调用一下几个方法,而且是顺序执行

方法名方法功能描述
tryAcquire()尝试获取锁,主要是试图修改state的值
addWaiter()上面获取锁失败,则构造双向链表,将未获取到锁的线程封装成节点通过CAS新增到链表的尾部
acquireQueued()使用一个死循环逐个的去执行CLH中的线程,如果获取锁成功则返回,否则调用park()进行休眠,直到被唤醒并获取锁返回。
公平锁中的lock()方法对中断不敏感,因为中断虽然能是park()方法的线程被唤醒,但是被唤醒后还是被执行acquireQueued(),里面在尝试获取锁的同时会判断是否是头结点,如果不是那么将被继续park().
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: