java并发编程汇总6:locks框架、AQS详解

由于是准备校招面试,这些知识不再是初学了,再按照初学那么整理进度太慢;

决定直接看之前写的博客,这里仅做框架式的知识梳理,便于形成架构;

若有疏漏的知识点再做详细补充;

Java 并发编程(五):详解 Lock、AbstractQueuedSynchronizer

一、关于lock:

0、concurrent包架构:

1、locks包架构、简介:

2、lock与synchronized的简单对比

  • Lock是一个接口,是代码层面的实现,synchronized是关键字,是内置的语言实现(JVM层面)。
  • Lock是显示地获取释放锁,扩展性更强,synchronized是隐式地获取释放锁,更简捷。
  • Lock在发生异常时,如果没有主动通过unlock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁,synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生。
  • Lock可以让等待锁的线程响应中断,而使用synchronized时等待锁的线程会一直等待下去,不能响应中断
  • Lock可以尝试非阻塞、可中断、超时地获取锁;synchronized不可以。
  • Lock可以知道是否成功获取锁,synchronized无法知道。

3、Lock结构的常用方法:lock()、unlock()、lockInterruptibly()、tryLock()

4、简单提一提 ReadWriteLock:

ReadWriteLock接口是一个单独的接口(未继承Lock接口),该接口提供了获取读锁和写锁的方法。

所谓读写锁,是一对相关的锁——读锁和写锁,读锁用于只读操作,写锁用于写入操作。读锁可以由多个线程同时保持,而写锁是独占的,只能由一个线程获取。

之前我们提到的锁(包括Lock接口、synchronized关键字)都是排他锁,这些锁在同一时刻只允许一个线程进行访问。而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读 线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写 锁,使得并发性相比一般的排他锁有了很大提升。

一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。

Java并发包提供读写锁的实现是 ReentrantReadWriteLock

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

读写锁本身的实现就远比独占锁复杂,因此,读写锁比较适用于以下情形:

  • 高频次的读操作,相对较低频次的写操作;
  • 读操作所用时间不会太短。(否则读写锁本身的复杂实现所带来的开销会成为主要消耗成本)。

二、关于AQS:

1、同步器的含义:

同步器是用来构建锁和其他同步组件的基础框架,它的实现主要依赖一个int成员变量来表示同步状态以及通过一个FIFO队列构成等待队列。它的子类必须重写AQS的几个protected修饰的用来改变同步状态的方法,其他方法主要是实现了排队和阻塞机制。状态的更新使用getState,setState以及compareAndSetState这三个方法

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

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

2、总结:

  1. 同步组件(这里不仅仅指锁,还包括CountDownLatch等)的实现依赖于同步器AQS,在同步组件实现中,使用AQS的方式被推荐定义继承AQS的静态内存类;
  2. AQS采用模板方法进行设计,AQS的protected修饰的方法需要由继承AQS的子类进行重写实现,当调用AQS的子类的方法时就会调用被重写的方法;
  3. AQS负责同步状态的管理,线程的排队,等待和唤醒这些底层操作,而Lock等同步组件主要专注于实现同步语义
  4. 在重写AQS的方式时,使用AQS提供的getState(),setState(),compareAndSetState()方法进行修改同步状态;

3、AQS提供的模板方法可以分为3类:

  1. 独占式获取与释放同步状态;
  2. 共享式获取与释放同步状态;
  3. 查询同步队列中等待线程情况;

4、同步组件实现角度:

4.1、同步组件实现者的角度:

通过可重写的方法:独占式tryAcquire()(独占式获取同步状态),tryRelease()(独占式释放同步状态);

共享式 :tryAcquireShared()(共享式获取同步状态),tryReleaseShared()(共享式释放同步状态);

4.2、AQS的角度

而对AQS来说,只需要同步组件返回的true和false即可,因为AQS会对true和false会有不同的操作,true会认为当前线程获取同步组件成功直接返回,而false的话就AQS也会将当前线程插入同步队列等一系列的方法。

总的来说,同步组件通过重写AQS的方法实现自己想要表达的同步语义,而AQS只需要同步组件表达的true和false即可,AQS会针对true和false不同的情况做不同的处理,至于底层实现,可以看这篇文章。(这里的详解见下)

5、深入理解AQS

5.1、独占式锁:

  • void acquire(int arg):独占式获取同步状态,如果获取失败则插入同步队列进行等待;
  • void acquireInterruptibly(int arg):与acquire方法相同,但在同步队列中进行等待的时候可以检测中断;
  • boolean tryAcquireNanos(int arg, long nanosTimeout):在acquireInterruptibly基础上增加了超时等待功能,在超时时间内没有获得同步状态返回false;
  • boolean release(int arg):释放同步状态,该方法会唤醒在同步队列中的下一个节点;

共享式锁:

  • void acquireShared(int arg):共享式获取同步状态,与独占式的区别在于同一时刻有多个线程获取同步状态;
  • void acquireSharedInterruptibly(int arg):在acquireShared方法基础上增加了能响应中断的功能;
  • boolean tryAcquireSharedNanos(int arg, long nanosTimeout):在acquireSharedInterruptibly基础上增加了超时等待的功能;
  • boolean releaseShared(int arg):共享式释放同步状态

5.2、同步队列:

AQS中的同步队列则是通过链式方式进行实现。

在AQS有一个静态内部类Node,其中有这样一些属性:

volatile int waitStatus //节点状态
volatile Node prev      //当前节点/线程的前驱节点
volatile Node next;     //当前节点/线程的后继节点
volatile Thread thread;//加入同步队列的线程引用
Node nextWaiter;       //等待队列中的下一个节点

很显然:这是一个双向队列

简单概括:

  1. 节点的数据结构,即AQS的静态内部类Node,节点的等待状态等信息
  2. 同步队列是一个双向队列,AQS通过持有头尾指针管理同步队列

5.3、独占锁

节点如何进行入队和出队是怎样做的了?实际上这对应着锁的获取和释放两个操作:获取锁失败进行入队操作获取锁成功进行出队操作

独占锁获取acquire():

  • 成功:方法结束返回;
  • 失败:先调用addWaiter()(CAS尾插),然后在调用acquireQueued()方法

独占锁释放release():

上述两者可以做一下总结:

  1. 线程获取锁失败,线程被封装成Node进行入队操作,核心方法在于addWaiter()和enq(),同时enq()完成对同步队列的头结点初始化工作以及CAS操作失败的重试;
  2. 线程获取锁是一个自旋的过程,当且仅当 当前节点的前驱节点是头结点并且成功获得同步状态时,节点出队即该节点引用的线程获得锁,否则,当不满足条件时就会调用LookSupport.park()方法使得线程阻塞
  3. 释放锁的时候会唤醒后继节点;

总体来说:在获取同步状态时,AQS维护一个同步队列,获取同步状态失败的线程会加入到队列中进行自旋;移除队列(或停止自旋)的条件是前驱节点是头结点并且成功获得了同步状态。在释放同步状态时,同步器会调用unparkSuccessor()方法唤醒后继节点。

5.4、共享锁:

共享锁获取acquireShared()

逻辑几乎和独占式锁的获取一模一样,这里的自旋过程中能够退出的条件是当前节点的前驱节点是头结点并且tryAcquireShared(arg)返回值大于等于0即能成功获得同步状态

共享锁的释放releaseShared():

跟独占式锁释放过程有点点不同,在共享式锁的释放过程中,对于能够支持多个线程同时访问的并发组件,必须保证多个线程能够安全的释放同步状态,这里采用的CAS保证,当CAS操作失败continue,在下一次循环中进行重试。

猜你喜欢

转载自blog.csdn.net/ScorpC/article/details/113857403