学习笔记-Java多线程(AQS&CLH队列锁&ReentrantLock)-基础复习

324 阅读6分钟

AbstractQueuedSynchronizer

队列同步器AbstractQueuedSynchronizer(以下简称同步器或AQS),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。

AQS使用方式

AQS的主要使用方式是继承,子类通过继承 AQS 并实现它的抽象方法来管理同步状态,在 AQS 里由一个int型的state来代表这个状态, 在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法 getState()setState(int newState)compareAndSetState(int expect,int update) 来进行操作,因为它们能够保证状态的改变是安全的。

在实现上,子类推荐被定义为自定义同步组件的静态内部类,AQS自身没有实现任何同步接口, 他仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持 独占式 地获取同步状态,也可以支持 共享式 地获取同步状态,这样既可以方便的实现不同类型的组件 ReentrantLockReentrantReadWriteLockCountDownLatch

同步器是实现任意锁(同步组件)的关键,在锁的实现中聚合同步器,可以这样理解二者的关系。
锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节。
同步器面向的是锁的实现着,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。

实现着需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,模板方法将会调用使用者重写的方法。

AQS中的方法

模板方法
同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态共享式获取与释放同步状态和查询同步队列中的等待线程情况

可重写的方法

访问或修改同步状态的方法
重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态。

  • getState(): 获取当前同步状态。
  • setState(int newState):设置当前同步状态。
  • compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。

CLH队列锁

CLH队列锁Craig, Landin, and Hagersten (CLH) locks
CLH队列锁 是一种基于链表的可扩展、高性能、公平的自旋锁, 申请线程仅仅在本地变量上自旋,不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。
当一个线程需要获取锁时:

  1. 创建一个QNode, 将其中的 locked 设置为 true,表示需要获取锁,myPred 表示对其前驱节点的引用。

  1. 线程Atail域调用getAndSet方法,使自己成为队列的尾部,同时获取一个指向其前驱结点的引用myPred
    线程B需要获得锁,同样的流程再来一遍

3. 线程就在前驱节点的 locked 字段上旋转,直到前驱结点释放锁(即前驱节点的锁值 locked == false)
4. 当一个线程需要释放锁时,将当前结点的locked域设置为false,同时回收前驱结点。

如上图所示,前驱结点释放锁,线程AmyPred所指向的前驱结点的locked字段变为false,线程A就可以获取到锁
CLH 队列锁的优点是空间复杂度低( 如果有 n 个线程,L 个锁,每个线程每次只获取一个锁,那么需要的存储空间是O(L + n), n个线程有nmyNodeL个锁有Ltail)。CLH队列锁常用在SMP体系结构下。
Java 中的 AQSCLH队列锁的一种变体实现。

ReentrantLock的实现

锁的可重入

重进入是指任意线程在获取到锁之后,能够再次获取该锁而不会被锁所阻塞,该特性的实现需要解决两个问题:

  1. 线程再次获取锁 :锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
  2. 锁的最终释放 : 线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。

nonfairTryAcquire(int acquires) 方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。同步状态表示锁被一个线程重复获取的次数。
如果该锁被获取了n次,那么前(n-1)tryRelease(int releases)方法必须返回 false,而只有同步状态完全释放了,才能返回true。该方法将同步状态是否为 0 作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。

公平和非公平锁

ReentrantLock 的构造函数中,默认的无参构造函数将会把Sync对象创建为 NonfairSync 对象,这是一个"非公平锁"; 而另一个构造函数ReentrantLock(boolean fair) 传入参数为 true 时将会把 Sync 对象创建为"公平锁"FairSync
nonfairTryAcquire(int acquires)方法, 对于非公平锁只要 CAS 设置同步状态成功,则表示当前线程获取了锁。 而公平锁则不同。tryAcquire 方法 ,该方法与nonfairTryAcquire(int acquires)比较,唯一不同的位置为判断条件多了 hasQueuedPredecessors(),即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。