真就是套娃结构23333
一般自行实现一个同步组件需要实现Lock接口,而实现Lock接口又必须得需要一个队列同步器(AQS),一般用静态内部类的方式实现它,而ASQ内部又有个等待队列(ConditionObject).
为啥这样搞?答案也很明显,前面讲 synchronized 说过,要获取一个锁,本质是获取它的监视器(monitor),如果没有成功获取,则进入该对象的同步队列,然后状态变为阻塞(BLOCKED),这里也就揭示了,实现同步组件(与锁相关的玩意),就必须得有同步队列,那等待队列呢?当调用某对象的wait()方法时,只有获得了该对象才会进入等待队列,状态变为等待(WAIT),注意只有获得了该对象时,才有进入等待队列的资格,那换种说法是不是等待队列得和那个对象绑定啊,而同步队列是知道线程获得对象与否的,所以啊,等待队列应该与同步队列相绑定(注意这里是个一对多的关系,一个同步队列可以对应多个等待队列)。
Lock接口实现的锁与synchronized 的区别:
synchronized | Lock | |
---|---|---|
主动释放锁 | 不需要 | 需要 |
获取超时返回 | 不支持 | 支持 |
重入锁 | 是 | 可以 |
可中断 | 不能 | 可以 |
公平锁 | 非公平 | 都可 |
独占锁 | 独占 | 都可 |
性能 | 少量并发较好 | 大量并发较好 |
开销归属 | 由JVM线程 | 有尝试获得锁的线程 |
由上表可知,Lock有个很大的优势是很灵活啊(废话可以自己实现啊),方便根据需求定制锁,当然也提供了一些现成的实现。
注:别忘了释放锁,而且一定写到finally块里啊
它是一切同步组件的简本框架,它使用一个int成员变量表示同步状态。它是一个抽象类。基于模板方法模式 (定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。它是一种类行为型模式。) 设计。如果说同步组件是面向使用者的,同步器则是面向实现者的。
重写对应方法需要使用AQS类中方法来获取和修改状态,以保证线程安全。
除了首节点外的每个结点用来保存获取同步状态失败的线程引用,首节点则是获取成功的那个。
它包含的属性有:
其中状态分为:
示例代码:
//一个独占锁,1表示加锁,0表示解锁。
public class Mutex implements Lock {
private static class Syn extends AbstractQueuedSynchronizer{
@Override
protected boolean isHeldExclusively() {
//是否处于被占有状态
return getState()==1;
}
@Override
protected boolean tryAcquire(int arg) {
//尝试获取同步状态
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;
}
Condition newCondition(){
//返回一个等待队列
return new ConditionObject();
}
}
private final Syn sync = new Syn();
@Override
public void lock() {
//加锁
//设定本线程结点状态为1(SIGNAL),尝试获取锁或者进入同步队列
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,unit.toNanos(time));
}
@Override
public void unlock() {
//释放锁
sync.release(1);
}
@Override
public Condition newCondition() {
//获取队列
return sync.newCondition();
}
}
大致结构如图所示
获取同步状态失败的结点会 以CAS的方式加入到同步队列的尾部,
因为尝试同步态态的或许不止一个线程,
而能获取成功的只会有一部分,另一部分则需要进入同步队列,
多个插入那肯定得竞争啊,就用CAS的方式保证线程安全。
private Node addWaiter(Node mode) {
Node node = new Node(mode);
for (;;) {
//目前的尾结点
Node oldTail = tail;
//尾结点不为空
if (oldTail != null) {
//将node的pre设置为oldTail
node.setPrevRelaxed(oldTail);
//如果现在的尾结点还是上次看到的尾结点的话,将为节点
//更新为目前这个node
if (compareAndSetTail(oldTail, node)) {
//整理结点
oldTail.next = node;
return node;
}
} else {
//初始化
initializeSyncQueue();
}
}
}
当前驱结点为头节点时尝试获取同步状态,则成功获取同步状态且将自己设为头节点,因为是独占锁只有一个线程会进入此状态,所以是线程安全的,无需使用CSA更新头节点。
若不是,则判断自己的前驱结点的状态,若前驱结点是被取消状态移除被取消的结点直到不是为止。
再重复上述判断,若前驱结点还不是头节点,那就阻塞自己的同时判断下自己是否因为被中断而被唤醒的。
若执行过程中抛出任何异常了,则将该线程移除且根据原来的中断情况设置中断位。
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
for (;;) {
final Node p = node.predecessor();
//如果前驱结点为首节点,且获取成功
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
}
//若被前驱结点被取消(状态为CANCELLED),
//则不断的移除到非前驱结点为止
//若为其他状态则设置为SIGNAL
if (shouldParkAfterFailedAcquire(p, node))
//前驱状态为SIGNAL
//获取当前线程是否被中断的同时阻塞线程
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
//取消该结点
cancelAcquire(node);
if (interrupted)
//将自己中断,使它从阻塞中恢复
selfInterrupt();
throw t;
}
}
当获取同步状态的线程执行完成后就会调用relase方法释放同步状态同时唤醒后继节点。
public final boolean release(int arg) {
//尝试释放,若该线程所有的重入状态都释放了
if (tryRelease(arg)) {
Node h = head;
//该线程确实获得了锁而且还有后续线程
if (h != null && h.waitStatus != 0)
//唤醒后继节点
unparkSuccessor(h);
//释放成功
return true;
}
//释放失败,还有重入的锁
return false;
}
独占式还有个超时获取。其实与上述类似,设置一个时间超时了,就把线程中断然后移出队列即可。
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
//将Node添加到尾部
final Node node = addWaiter(Node.EXCLUSIVE);
try {
for (;;) {
final Node p = node.predecessor();
//若为前驱为头且获取成功
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return true;
}
nanosTimeout = deadline - System.nanoTime();
//若超时,取消
if (nanosTimeout <= 0L) {
cancelAcquire(node);
return false;
}
//若在时间范围内,阻塞线程,但是时间还在计时
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD)
LockSupport.parkNanos(this, nanosTimeout);
//若被中断则被移除
if (Thread.interrupted())
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
当具有共享锁的线程抢先获取资源时(一般为读操作),独占线程是会被阻塞的
反之当独占锁线程优先进入时,所有的锁都不能进入。
如果前驱结点时为头时,便直接唤醒并设置为头,然后接着唤醒后面的,同时保证前面的线程被释放(是CAS的方式保证全部安全释放)
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean interrupted = false;
try {
for (;;) {
final Node p = node.predecessor();
//前驱结点为头
if (p == head) {
//尝试获取同步状态
int r = tryAcquireShared(arg);
if (r >= 0) {
//唤醒后续共享结点,一起来
setHeadAndPropagate(node, r);
p.next = null; // help GC
return;
}
}
//如果不是头,就等咯
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
} finally {
if (interrupted)
selfInterrupt();
}
}
private void doReleaseShared() {
for (;;) {
//唤醒下一个结点,并以CAS的方式释放所有共享同步状态
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!h.compareAndSetWaitStatus(0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}