LockSupport与AQS

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LockSupport与AQS相关的知识,希望对你有一定的参考价值。

参考技术A LockSupport类是Java6(JSR166-JUC)引入的一个类,提供了基本的线程同步原语。

每个线程都会有一个独有的permit(许可)。

相比较于wait/notify/notifyAll有何优点?

注意:LockSupport是不可重入的:unpark三次之后,park一次可以继续运行,再次park还是会被阻塞。可以理解为unpark是把某个标志位标为1,并不是加1。park是将这个标志位标为0,而非减1。

AbstractQueuedSynchronizer,即队列同步器。它是构建锁或者其他同步组件的基础框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等),它是JUC并发包中的核心基础组件。
AQS简单地说就是使用一个FIFO的等待队列和一个volatile int state来实现同步的。即通过CAS state判断是否被锁(CAS来保证原子性、volatile保证可见性),将阻塞的线程打包放入等待队列中。
1. AQS的使用者一般定义一个内部类来继承AQS,使用组合的方式使用。
2. AQS有两种模式:排他和共享。
排他模式:只有一个线程可以拥有锁。(排他锁)
共享模式:可以同时多个线程拥有锁。(读锁)
AQS中两种模式下的waiting thread共用一个queue,所以一般使用者都只是使用一种模式。ReentrantReadWriteLock是同时使用了两种模式。

使用者继承AQS,实现AQS中的几个未实现的方法。然后就可以调用AQS的方法来实现自己的接口功能了。

我们可以看到ReentrantLock使用一个内部类Sync来继承AQS,然后实现排他锁的三个方法。
我们知道ReentrantLock有lock和unlock接口,可以看到这两个接口的实现就是调用AQS原有的方法。

前三个是排他锁所要实现的,后两个是共享锁所要实现了。注意:这五个函数并不是abstract,原因是因为一般都是使用某一种模式(排他或共享模式),所以子类只需使用其中一组就可以了。

在使用AQS的类中用来加锁和解锁的方法。

这里我们可以看到“获取锁,如果失败则加入队列”这个行为是由AQS来实现的。而如何判断失败?这个是由子类来决定的。这个决定支持了可重入性、是否公平性等功能。

共享模式下的对应的四个方法。

我们知道公平锁:先来的一定先获取锁。
非公平锁:当多个线程在争取锁,谁先获取锁的顺序是不固定的。
AQS的公平性是由使用者来决定的。
我们知道AQS中的acquire函数是大致这样实现的。

因为每次acquire的步骤是:先try再入队列。所以就可以出现这种情况:队列中有两个线程在等待,当锁被释放时,刚好又来了一个线程,则try的时候成功了,这样这个线程就获得锁了。
如果想要实现公平锁:tryAcquire的时候判断一下,如果有线程在等待,这个函数直接返回false。
显然非公平锁要比公平锁的效果要高。

Java并发编程原理 - Unsafe && LockSupport类及AQS同步器的设计

[相关源码]

(https://github.com/Wasabi1234/Java-Concurrency-Progamming-Tutorial)

1 Unsafe类的park和unpark

public native void park(boolean var1, long var2);
public native void unpark(Object var1);
  • park方法用来阻塞一个线程,第一个参数用来指示后面的参数是绝对时间还是相对时间,true表示绝对时间,false表示从此刻开始后的相对时间.调用park的线程就阻塞在此处.
  • unpark用来释放某个线程的阻塞,线程用参数var1表示

举个例子:

2 LockSupport

直接使用Unsafe还是有诸多不便之处,因此lock包提供了一个辅助类LockSupport封装了park和unpark

举个例子:

可以看出,使用LockSupport要比直接只用Unsafe更加便捷。

此外,LockSupport还可以用来给线程设置一个Blocker对象,便于调试和检测线程,其原理是使用Unsafe的putObject方法直接设置Thread对象的parkBlocker属性,并在合适的时候读取这个Blocker对象,例子如下:

3 AQS同步器

各种锁ReentrantLock、ReentrantReadWriteLock以及各种同步器诸Semaphore、CountDownLatch等,核心都是AbstractQueuedSynchronizer

3.1 使用 AQS 手写排它锁

让我们先具体感知它是如何使用的。

这里有一个非常简单的例子SimpleLock,实现了一个最简单的排它锁。

  • 当有线程获得锁时,其他线程只能等待
  • 当这个线程释放锁时,其他线程可以竞争获取锁


运行结果表明,通过简单的几行代码,就实行了一个锁的所有功能。

根据JUC作者的建议,AQS的使用方法要遵循上面这个模式。

使用一个内部类Sync来继承AQS,并实现AQS的相关方法

一般是

  • tryAcquire
  • tryRelease(排它锁)

或者

  • tryAcquireShared
  • tryReleaseShared(共享锁)

在内部使用代理模式实现锁的功能

这样可以让暴露出的同步、互斥方法名由程序员自行决定。
例如各种锁可以使用

  • lock
  • unlock

Semaphore可以使用

  • acquire
  • release

CountDownLatch可以使用

  • await
  • countDown

2 AQS基本原理

要实现一个同步器,需要三个条件:

  1. 一个同步状态值,且可原子操作该状态值.显然CAS胜任
  2. 阻塞线程和解除阻塞线程的机制,可以用LockSupport来实现
  3. 维护一个阻塞线程的队列,并在队列的每个节点中存储线程的状态等信息

让我们看看AQS又是如何设计满足的这三个条件。

2.1 状态值及相应的操作方法

private volatile int state;

protected final int getState() {
    return state;
}

protected final void setState(int newState) {
    state = newState;
}

protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

state 为 volatile int型,它的CAS方法,提供原子的比较更新操作。

一般,AQS认为

  • state == 0 时,同步器处于释放状态,多线程此时可竞争获取同步器
  • state ≠ 0 时,同步器处于已获取状态,后续线程需进入队列,等待同步器(可重入同步器允许获取同步器的线程再次进入该同步器,此时使用state计数)
  • 当然,很多情况下,程序员也可自己定义state的值的含义,特别是在实现读写锁时,需要将state一分为二的用。

2.2 阻塞和解除阻塞

LockSupport 提供了阻塞和解除阻塞的功能。因此,所有同步器的阻塞操作其实都是基于LockSupport的,也就是基于Unsafe的park和unpark方法的。

2.3 线程等待队列

AQS内部提供了一个Node类型,它是用来形成“线程等待队列”的节点类型,以及一个由Node类型组成的队列。

以上是关于LockSupport与AQS的主要内容,如果未能解决你的问题,请参考以下文章

提升--08---LockSupport淘宝面试题与源码阅读方法论

多线程之LockSupport,线程阻塞与唤醒(十四)

jdk源码之LockSupport

并发3LockSupport阻塞与唤醒,相较与wait和notify

Java并发:挂起与唤醒线程LockSupport工具类详解

LockSupport与AQS