一、Lock简介(JDK1.5提供的)
锁:控制多个线程访问共享资源。
- 在Lock接口出现之前,java程序靠synchronized关键字实现锁功能。
- Lock接口失去了像synchronized关键字隐式加锁解锁的便捷性,但拥有锁获取和释放的可操作性、可中断的获取锁、超时获取锁等同步特性。
- synchronized同步块执行完或遇到异常时锁会自动释放,Lock必须调用unlock()释放锁。
Lock lock = new ReetrantLock();
try{
lock.lock();
//以下代码只有一个线程可以运行
...
}finally{
lock.unlock();//显式解锁
}
二、lock常见API
lock体系拥有可中断获取锁、超时获取锁以及共享锁等特性。
- void lock();//获取锁
- void lockInterruptibly() throws InterruptedException();//响应中断锁
- boolean trylock();//获取锁返回true,反之返回false
- boolean trylock(long time,TimeUnit unit);//超时获取锁,在规定时间内未获取到锁返回false
- Condition newCondition();//获取与lock绑定的等待通知组件
- void unlock();释放锁
Lock接口的实现子类ReetrantLock中所有的方法实际上都是调用了其静态内部类Sync中的方法,而Sync继承了AbstractQueuedSynchronizer(AQS-简称同步器)
自定义类 implements Lock{
lock();
unlock();
static class Sync extends AbstractQueuedSynchronzer{
}
}
三、AQS—同步器
定义:用来构建锁以及其他同步组件的基础框架,他的实现主要是依赖一个int状态变量以及通过一个FIFO队列构成同步队列。
- 子类必须重写AQS的protected修饰的用来改变同步状态的方法。其他方法主要是实现了排队与阻塞机制。int状态的更新使用getState()、setState()以及compareAndSetState()。
- 子类推荐使用静态内部类来继续AQS实现自己的同步语义。同步器既支持独占锁,也支持共享锁。
锁与AQS的关系:
- 锁面对使用者,定义了使用者与锁交互的接口。
- 同步器面向锁的实现,简化了锁的实现方式,屏蔽同步状态管理,线程排队、等待、唤醒等操作。
四、AQS的模板模式
定义:AQS使用模板方法模式,将一些与状态相关的核心方法开放给子类重写,而后AQS会使用子类重写的关于状态的方法进行线程的排队、阻塞以及唤醒等操作。
- AQS采用模板方法进行设计,AQS的protected修饰的方法需要由继承AQS的子类进行重写实现,当调用AQS的子类的方法时就会调用被重写的方法。
五、AQS详解
作用:在同步组件中,AQS是最核心的部分,同步组件的实现依赖AQS提供的模板方法来实现同步组件语义。
- AQS实现了对同步状态的管理,以及对阻塞线程进行排队、等待通知等底层实现。
- AQS核心组成:同步队列、独占锁的获取与释放、共享锁的获取与释放、可中断锁、超时锁。这一系列功能的实现依赖于AQS提供的模板方法。
1、独占锁
(1)void acquire(int arg);
独占式获取同步状态,如果获取失败插入同步队列进行等待。
(2)void acquireInterruptibly(int arg);
在(1)的基础上,此方法可以在同步队列中响应中断。
(3)boolean tryAcquireNanos(int arg,long nanosTimeOut);
在(2)的基础上增加了超时等待功能,到了预计时间还未获得锁,直接返回。
(4)boolean tryAcquire(int arg);
互殴锁成功返回true,否则返回false。
(5)boolean release(int arg);
释放同步状态,此方法会唤醒在同步队列的下一个节点。
2、共享式锁
(1)void acquireShared(int arg);
共享获得同步状态,同一时刻多个线程获取同步状态。
(2)void acquireSharedInterruptibly(int arg);
在(1)的基础上增加响应中断。
(3)boolean tryAcquireSharedNanos(int arg,long nanosTimeOut);
在(2)的基础上增加超时等待。
(4)boolean releaseShared(int arg);
共享式释放同步状态。
3、同步队列
在AQS内部有一个静态内部类Node,这是同步队列中每个具体的结点。
节点中有如下属性:
- int waitStatus:节点状态
- Node prev:同步队列中前驱节点
- Node next:同步队列中后继节点
- Thread thread:当前结点包装的线程对象
- Node nextWaiter:等待队列中下一个节点
节点状态值如下:
- int INITIAL = 0;//初始状态
- int CANCELLED = 1;//当前结点从同步队列中取消
- int SIGNAL = -1;//后继节点处于等待状态。如果当前结点释放同步状态会通知后继节点,使后继节点继续运行
- int CONDITION = -2;//结点处于等待队列中。当其他线程对Condition调用signal()方法后,该结点会从等待队列移到同步队列中。
- int PROPAGATE = -3;//共享式同步状态会无条件的传播
AQS同步队列采用带有头尾结点的双向链表。
六、独占锁的获取:acquire(int arg)
获取锁失败后调用AQS提供的acquire(int arg)模板方法
- tryAcquire(arg):再次尝试获取同步状态,成功直接方法退出,失败调用addWaiter();
- addWaiter(Node.EXCLUSIVE),arg):将当前线程以指定模式(独占式、共享式)封装为Node节点后尾插置入同步队列。
private Node addWaiter(Node node){
Node node = new Node(Thread.currentThread(),node);
Node pred = tail;
if(perd != null){
node.prev = pred;
if(compareAndSetTail(pred,node)){
pred.next = node;
return node;
}
}
enq(node);
return node;
}
- enq(Node node):当前队列为空或者CAS尾插失败,调用此方法来初始化队列或不断尾插。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
// 头结点初始化
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t; // CAS尾插,失败进行自旋重试直到成功为止。
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
- acquireQueued()获取锁成功条件:结点入队后排队获取同步状态。
当前节点前驱为头结点,并且再次获取同步状态成功。 - 结点在同步队列中获取锁,失败后调用shouldParkAfterFailedAcquire(Node prev,Node node)
此方法主要逻辑是
使用CAS将前驱节点状态置为SIGNAL,表示需要将当前结点阻塞。
如果CAS失败,不断自旋直到前驱节点状态置为SIGNAL为止。 - acquireQueued();
1.如果当前节点的前驱节点为头结点,并且能够成功获取同步状态,当前线程获取锁成功,方法退出。
2.如果获取锁失败,先不断自旋将前驱节点状态置为SIGINAL,而后调用LockSupport.park()方法将当前线程阻塞。 - 结点获取到同步状态的前置条件:
当前节点的前驱节点为头结点,并且调用tryAcquire获取到了同步状态->前驱头节点出队
独占锁的释放:release()
unlock()方法实际调用AQS提供的release()模板方法
- release()方法是unlock()方法的具体实现。首先获取头结点的后继节点,当后继节点不为null,会调用LockSupport.unpark()方法唤醒后继节点包装的过程。因此,每一次锁释放后就会唤醒队列中该节点的后继节点所包装的线程。
独占式锁获取与释放总结:
1.线程获取锁失败,将线程调用addWiter()封装成Node进行入队操作。addWriter()中方法enq()完成对同步队列的头结点初始化以及CAS尾插失败后的重试处理。
2.入队后排队获取锁的核心方法acquireQueued(),结点排队获取锁是一个自旋过程。当且仅当,当前节点的前驱节点为头结点并且成功获取同步状态时,节点出队并且该节点引用的线程获取到锁。否则,不满足条件时会不断自旋将前驱节点的状态置为SIGNAL而后调用LockSupport.park()将当前线程阻塞。
3.释放锁时会唤醒后继节点(后继节点不为null)。
七、独占锁的特性
1.获取锁时响应中断:
原理与acquire()几乎一样,唯一区别在于当parkAndCheckInterrupt()返回true时表示线程阻塞时被中断,抛出中断异常后线程退出。
2.超时等待获取锁:
tryAcquireNanos(),该方法在三种情况下会返回:
在超时时间内,当前线程成功获取到锁。
当前线程在超时时间内被中断
超时时间结束,仍未获取到锁,线程退出返回false。
总结:超时获取锁逻辑与可中断获取锁基本一致。唯一区别在于获取锁失败后,增加了一个时间处理。如果当前时间超过截止时间,线程不再等待,直接退出,返回false。否则将线程阻塞置为等待状态排队获取锁。