这些是之前的文章,方便边阅读边看看之前学习过的内容:
《一、多线程基本操作与实现》
《二、synchronized底层实现与优化》
《三、生产者消费者模型》
一、LOCK体系简介
JDK1.5之后增加Java.Util.concurrent.locks
提供了与内建锁完全不同的实现多线程共享资源访问机制。内建锁使用的还是native方法实现的,底层是monitor指令,而Lock体系的锁全是纯Java实现的一个锁体系。
Lock体系的锁失去了内建锁隐式地加锁与解锁过程,增加了可中断的获取锁以及超时获取锁以及共享。Lock锁的标准使用形式:
Lock lock = new ReentrantLock();
try{
lock.lock();
}finally{
lock.unlock();
}
二、Lock常用API
1、void lock();
获取锁
2、void lockInterruptibly() throws InterruptedException;
获取锁的过程能响应中断(Lock独有)。
3、boolean tryLock();
获取锁返回true,反之返回false,可响应中断。
4、boolean tryLock(long time,TimeUnit unit);
在三的基础上增加了超时等待机制,规定时间内未获取到锁,线程直接返回(Lock独有)
5、void unlock();
解锁
6、Condition newCondition();
获取与lock绑定的等待通知组件,当前线程必须先获得了锁才能等待,等待会释放锁,再次获取到锁才能从等待中返回。
AbstractQueuedSynchronizer(同步器),Lock体系最核心的存在
同步器作用:用来构建锁与其他同步组件的基础框架,它的实现主要依赖一个int成员变量来表示同步状态以及通过一个FIFO队列形成同步队列,要使用AQS,推荐使用静态内部类继承AQS,覆写AQS中的protected用来改变同步状态的方法,其他方法主要是实现排队与阻塞机制。状态更新使用getState()、setState()、compareAndSetState()。
子类推荐使用静态内部类来继承AQS实现自己的同步语义。同步器即支持独占锁,也支持共享锁。
class NewBeeLock implements Lock {
private Sync sync = new Sync();
static class Sync extends AbstractQueuedSynchronizer{
@Override
//规定同步状态为1
protected boolean tryAcquire(int arg) {
if(arg != 1){
throw new RuntimeException("arg不为1");
}
//CAS操作把0变成1
if(compareAndSetState(0,1)){
//当前线程成功获取到锁
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
if(getState() == 0){
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
@Override
protected boolean isHeldExclusively() {
return getState()==1;
}
}
//Lock接口方法------------------------------------------------
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1,time);
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return null;
}
}
//------------------------Test----------------
class MyThread implements Runnable{
private Lock lock = new NewBeeLock();
@Override
public void run() {
try{
lock.lock();
Thread.sleep(5000);
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public class Demo {
public static void main(String[] args) {
MyThread myThread = new MyThread();
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(myThread);
thread.start();
}
}
}
通过我们的自定义Lock的实现类可以看出:Lock面向使用者,定义了使用者与锁交互的接口,隐藏了实现细节,AQS面向锁的实现者,简化了锁的实现方式屏蔽同步状态的管理、线程排队、线程等待与唤醒等底层操作。
三、ASQ提供的模板方法
独占锁:
1、void acquire(int arg)
:独占式获取同步状态,如果获取失败则将当前线程插入同步队列进行等待
2、void acquireInterruptibly(int arg)
:在1的基础上增加响应中断
3、void acquireInterruptibly(int arg,long nanosTimeOut)
:在2的基础上增加超时等待,在规定时间为获取到同步状态返回false
4、boolean tryAcquire(int arg)
:获取状态成功返回true,否则返回false
5、boolean release(int arg)
:释放同步状态,该方法会唤醒在同步队列中的下一个结点
共享锁:
1、void acquireShared(int arg)
: 共享式获取同步状态,与独占锁的区别在于同一时刻有多个线程获取同步状态。
2、void acquireSharedInterruptibly(int arg)
: 增加了响应中断的功能
3、boolean tryAcquireSharedNanos(int arg,lone nanosTimeout)
: 在2的基础上增加了超时等待功能
4、boolean releaseShared(int arg)
: 共享锁释放同步状态。
四、同步队列
要想掌握AQS的底层实现,其实也就是对这些模板方法的逻辑进行学习。在学习这些模板方法之前,我们得首先了解下AQS中的同步队列是一种什么样的数据结构,因为同步队列是AQS对同步状态的管理的基石。
AQS中的同步队列是一个尾带有头的双向链表,节点组成为:Node prev,Node next,Thread thread,将线程封装为Node结点后进行入队与出队处理,如下图:
节点的属性如下:
- volatile int waitStatus; // 节点状态
- volatile Node prev; // 当前节点的前驱节点
- volatile Node next; // 当前节点的后继节点
- volatile Thread thread; // 当前节点所包装的线程对象
- Node nextWaiter; // 等待队列中的下一个节点
节点的状态如下:
- int INITIAL = 0; // 初始状态
- int CANCELLED = 1; // 当前节点从同步队列中取消
- int SIGNAL = -1; // 后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继节点的线程继续运行。
- int CONDITION = -2; // 节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取中。
- int PROPAGATE = -3; // 表示下一次共享式同步状态获取将会无条件地被传播下去。
五、深入理解AQS
独占锁获取:acquire(int arg)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire()方法就是再次使用CAS尝试获取同步状态,如果成功方法直接返回,当前线程置为持有锁线程!若再次尝试失败,调用addWaiter()
addWaiter()将当前线程封装为Node节点之后插入同步队列中:
private Node addWaiter(Node mode) {
//将当前线程以指定模式封装为Node节点(独占式/共享式)
Node node = new Node(Thread.currentThread(), mode);
//拿到当前同步队列的尾节点
Node pred = tail;
if (pred != null) {
node.prev = pred;
//CAS操作把当前节点尾插入同步队列,失败的话会调用enq()
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//当前队列为空,或者CAS尾插失败时调用enq()
enq(node);
return node;
}
当前同步队列为空或者尾插失败:调用enq(),enq()的源码如下:
可以看出当同步队列为空时完成队列初始化操作以及不断CAS自旋将当前节点尾插入同步队列!
private Node enq(final Node node) {
for (;;) { //不断自旋
//拿到尾节点
Node t = tail;
//当前队列为空
if (t == null) { // Must initialize
//完成队列的初始化操作,lazy-load模式
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
//不断的将当前节点使用CAS尾插入同步队列中直到成功为止
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueued的源码:
final boolean acquireQueued(final Node node, int arg) {
//设置失败状态,初始化为true
boolean failed = true;
try {
//设置中断状态,默认为false
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())
interrupted = true;
}
} finally {
if (failed)
//将当前节点置为取消状态,node.waitStutas = 1;
cancelAcquire(node);
}
}
由上面这段代码可以看出:节点从同步队列获取同步状态的前提条件:if(p == head && tryAcquire(arg))
,只有当前驱节点为头结点时,线程才有机会获取同步状态。
当线程获取同步状态失败的时候,首先调用shouldParkAfterFailedAcquire方法,shouldParkAfterFailedAcquire方法的源码:
private static boolean shouldParkAfterFailedAcquire
(Node pred, Node node) {
//获取前驱节点状态
int ws = pred.waitStatus;
//SIGNAL是-1,表示当前节点的后继节点处于等待状态
if (ws == Node.SIGNAL)
//表示当前节点阻塞
return true;
//前驱节点被取消
if (ws > 0) {
//一直向前找到节点状态不是取消状态的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//将前驱节点状态置为SIGNAL:-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
shouldParkAfterFailedAcquire(p,node)一直在acquireQueue()方法中自旋直到将前驱节点状态置为SIGNAL,表示此事应该将当前节点阻塞!前驱节点状态置为-1,调用parkAndCheckInterrupt()阻塞当前节点线程:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
整个acquire()方法的流程图:
独占锁的释放:release()
unparkSuccessor():唤醒头结点的下一个结点(唤醒距离头结点最近的一个非空结点 —— 保证公平性),如果它存在的话,此时头结点是当前已经获取到锁的结点
public final boolean release(int arg) {
if (tryRelease(arg)) {
// 获取到当前同步队列的头节点
Node h = head;
if (h != null && h.waitStatus != 0)
// 唤醒头节点的下一个节点
unparkSuccessor(h);
return true;
}
return false;
}
unparkSuccessor:唤醒头节点的下一个节点,其实是唤醒距离头结点最近的一个非空节点,这是为了最大限度的保证公平性,在头部的等待的时间是最长的,所以应该先唤醒距离头结点最近的一个非空节点!
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);
}
总结一下:
- 线程获取锁失败,线程被封装成Node进行入队操作,核心方法在于addWaiter()和enq(),同时enq()完成对同步队列的头结点初始化工作以及CAS操作失败的重试;
- 线程获取锁是一个自旋的过程,当且仅当 当前节点的前驱节点是头结点并且成功获得同步状态时,节点出队即该节点引用的线程获得锁,否则,当不满足条件时就会调用LookSupport.park()方法使得线程阻塞;
- 释放锁的时候会唤醒后继节点;
总体来说:在获取同步状态时,AQS维护一个同步队列,获取同步状态失败的线程会加入到队列中进行自旋;移除队列(或停止自旋)的条件是前驱节点是头结点并且成功获得了同步状态。在释放同步状态时,同步器会调用unparkSuccessor()方法唤醒后继节点。