并发编程(八):Lock,AQS和ReentrantLock

目录


学习资料

《Java并发编程的艺术》第5章 5.1~5.3


1.Lock接口

和Synchronized类似,只是需要显式的获取锁和释放锁,不太便捷,但是更加灵活,提供了很多Synchronized所无法提供的特性(控制并操作锁,尝试获取锁,中断获取锁,超时获取锁等)

Lock的简单使用:

Lock lock = new ReetrantLock();
lock.lock();	//获取锁
try{
}finally{
    lock.unlock();
}

Lock的API:

  • lock():当前线程获取锁
  • lockInterruptibly():可以响应中断,抛出中断异常并释放锁
  • tryLock():尝试非阻塞获取锁,能获取返回true,否则false
  • tryLock(long time,TimeUnit unit):获取到了锁,中断,超时都会返回
  • unlock():释放锁
  • Condition newCondition():获取等待通知组件Condition

Lock接口的实现基本都是通过聚合一个同步器的子类来完成访问控制的


2.AQS同步队列

2.1 概述

1.队列同步器AbstractQueuedSynchronizer(AQS),用来构建锁或者其他同步线程的基础框架;

​ AQS使用int变量表示同步状态,内置FIFO队列表示资源获取线程的排队工作;

2.基于模版方法模式,主要方式为继承,子类推荐定义为自定义同步组件的静态内部类(如ReentrantLock等)

3.同步器是实现锁(或其他同步组件)的关键:

  • 锁是面向使用者的,定义了使用者与锁交互的接口,隐藏了实现细节
  • 同步器面向锁的实现者,简化了锁的实现方式,屏蔽了同步状态管理,线程排队,等待唤醒等底层操作
  • 锁与同步器隔离和使用者和实现者

2.2 同步器接口

同步状态相关方法

  • getState():获取当前同步状态
  • setState(int n):设置当前同步状态
  • compareAndSetState(int expect,int update):使用CAS设置当前状态,保证状态设置的原子性

同步器可重写方法:需要修改锁状态

  • tryAcquire(int):独占式获取同步状态
  • tryRelease(int):独占式释放同步状态
  • tryAcquireShared(int):共享式获取同步状态
  • tryReleaseShared(int):共享式释放同步状态
  • isHeldExclusively():当前同步器是否被独占

同步器提供的模板方法

  • 独占式:
    • acquire(int):独占式获取同步状态
    • release(int):独占式释放同步状态
    • aquireInterruptibly(int):响应中断独占式获取同步状态
    • tryAquireNanos(int,long):独占式超时获取锁(也响应中断)
  • 共享式:
    • acquireShared(int):共享式获取同步状态
    • releaseShared(int):共享式释放同步状态
    • aquireSharedInterruptibly(int):响应中断共享式获取同步状态
    • tryAquireSharedNanos(int,long):共享式超时获取锁(也响应中断)
  • 其他:getQueueThreads获取同步队列上的线程集合

2.3 AQS使用示例

独占锁:统一时刻只能有一个线程获取到锁,其他只能处于同步队列中等待,如Mutex类的实现:

将AQS实现类作为静态内部类,外部同步组件将操作代理到该实现类中


3.AQS实现原理分析

四个方面:同步队列,独占式同步状态获取与释放,共享式同步状态获取与释放,超时获取同步状态

3.1 同步队列的实现

同步器依赖于内部同步队列(一个FIFO双向队列)来完成同步状态的管理:

  • 当前线程获取同步状态失败(aquire()),同步器会将当前线程和等待状态信息构造成一个节点并将其加入同步队列,同时阻塞当前线程
  • 同步状态释放时,会把首节点线程唤醒并尝试获取同步状态(tryAquire())

节点中的waitStatus代表等待状态:

//超时或中断,需要取消
static final int CANCELLED =  1;
//后续处于等待状态,当前节点持有同步状态
static final int SIGNAL    = -1;
//在等待队列中等待
static final int CONDITION = -2;
//下一次共享式同步状态获取会被传播下去
static final int PROPAGATE = -3;

同步队列基本结构:

tryAquire()失败的线程节点加入到同步队列尾部:需要使用CAS确保线程安全compareAndSetTail(..)

同步队列首节点是获取同步状态成功的节点,后继节点会在获取(tryAquire())到同步状态成功后将自己设置为首节点,不需要CAS保证线程安全:


3.2 独占式同步状态获取与释放

独占式同步状态获取

独占式获取同步状态:acquire(int)

public final void acquire(int arg) {
   	/**
   	tryAcquire:获取同步状态
   	Node.EXCLUSIVE:构造了一个节点
   	addWaiter:添加到了同步队列尾部
   	acquireQueued:自旋获取同步状态
   	*/
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

简单流程图:(也是acquire()方法的流程)

加入同步队列尾部通过addWaiter(Node)中的enq(Node)方法实现,通过死循环执行CAS操作compareAndSetTail来保证节点正确添加

节点自旋获取同步状态(aquireQueued(Node,int)方法),只有当前驱节点是首节点并且尝试获取同步状态成功才会将该节点设置为首节点

独占式同步状态释放

独占式释放同步状态:release(int)

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()中使用了LockSupport类的unpark(Thread)方法来唤醒处于等待状态的线程


3.3 共享式同步状态获取与释放

共享式同步状态获取

共享式和独占式主要区别在于能否多个线程同时获取到同步状态

一般写操作需要独占式访问,读操作可以共享式访问

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
    //执行共享式锁获取
}

doAcquireShared方法中基本流程:

  1. 失败构造节点加入同步队列(addWaiter(Node.SHARED)
  2. 自旋(for死循环实现)
  3. 前驱节点为头结点 & tryAcquireShared(int)>0
  4. 获取同步状态成功,从自旋中退出

共享式同步状态释放

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

与独占式不同,共享式同步状态释放(tryReleaseShared())必须保证线程安全,一般通过循环CAS保证


3.4 独占式超时获取同步状态

通过调用同步器的doAquireNanos(int arg,long nanosTimeout)方法可以超时获取同步状态,并能抛出中断异常

nanosTimeout设置超时时间,单位为纳秒,大于0表示时间未到,小于0表示超时,超时时间小于自旋时间spinForTimeoutThreshold=1000ns时,会进入自旋而不是超时等待

流程如下:


4.重入锁

重入锁ReentrantLock,支持一个线程对资源重复加锁,还支持公平锁和非公平锁的选择

  • 公平锁:等待时间长的线程优先获取锁(FIFO)
  • 非公平锁:不按顺序获取

synchronized隐式支持重进入

ReentrantLock内部有一个计数器表示重复获取锁的次数,在调用lock方法时,如果已经获取到了线程的锁,再次调用lock()方法不会返回false,而是将计数器自增并返回true;释放锁的时候,当计数器>0时,都会返回false,计数器自减,只有计数器为0时才返回true(需要多次调用unlock)

公平锁与非公平锁实现:(同步队列中)

  • 公平锁:前驱节点是首节点并且tryAquire()为true才能获取同步状态
  • 非公平锁:只要tryAquire()为true就能获取同步状态

非公平锁优点(公平锁相反)

  • 不必为了维持FIFO进行大量线程切换(会消耗大量时间)
  • 可能会造成线程饥饿
  • 提高了吞吐量

posted @ 2021-03-11 20:59  菜鸟kenshine  阅读(109)  评论(0编辑  收藏  举报