JUC锁

JUC锁有一些比较重要的部分,包括LockSupport、AQS、ReentrantLock、ReentrantReadWriteLock。


LockSupport

LockSupport是一个提供锁机制的工具类,可以说是JUC锁实现的基础。

其一般用来支持线程的阻塞和唤醒操作,比传统的wait()notify()更加灵活和强大。

核心函数分析

LockSupport的核心函数都是基于Unsafe类中定义的park和unpark函数:

1
2
public native void park(boolean isAbsolute, long time);
public native void unpark(Thread thread);
  • park():阻塞线程,直到出现以下几类情况:
    • 调用unpark()函数,释放该线程的许可
    • 该线程被中断
    • 设置的时间到了(依据参数time)
  • unpark():释放线程的许可,这个函数是不安全的,因为不确定目标线程是否仍是存活的

一些问题

关于常用的线程阻塞/唤醒方法,有一些比较问题。

Thread.sleep()和Object.wait()的区别

  • Thread.sleep()不会释放占有的锁,Object.wait()会释放占有的锁;

  • Thread.sleep()必须传入时间,Object.wait()可传可不传,不传表示一直阻塞下去;

  • Thread.sleep()到时间了会自动唤醒,然后继续执行;

    Object.wait()不带时间的,需要另一个线程使用Object.notify()唤醒;Object.wait()带时间的,假如没有被notify,到时间了会自动唤醒,这时又分为两种情况,一是立即获取到了锁,线程自然会继续执行;二是没有立即获取锁,线程进入同步队列等待获取锁

Object.wait()和Condition.await()的区别

Object.wait()和Condition.await()的原理基本一致,不同的是Condition.await()底层是调用LockSupport.park()来阻塞当前线程的。

Thread.sleep()和LockSupport.park()的区别

  • 都是阻塞当前线程的执行,且都不会释放当前线程占有的锁资源;

  • Thread.sleep()只能自己醒过来;LockSupport.park()方法可以被另一个线程调用LockSupport.unpark()方法唤醒;

  • Thread.sleep()方法声明上抛出了InterruptedException中断异常,所以调用者需要捕获这个异常或者再抛出;LockSupport.park()方法不需要捕获中断异常;

  • Thread.sleep()本身就是一个native方法;LockSupport.park()底层是调用的Unsafe类的native方法

Object.wait()和LockSupport.park()的区别

  • Object.wait()方法需要在synchronized块中执行;LockSupport.park()可以在任意地方执行;

  • Object.wait()方法声明抛出了中断异常,调用者需要捕获或者再抛出;LockSupport.park()不需要捕获中断异常;

  • Object.wait()不带超时的,需要另一个线程执行notify()来唤醒,但不一定继续执行后续内容;LockSupport.park()不带超时的,需要另一个线程执行unpark()来唤醒,一定会继续执行后续内容;

如果在wait()之前执行了notify()会怎样

如果当前的线程不是此对象锁的所有者,却调用该对象的notify()或wait()方法时抛出IllegalMonitorStateException异常;

如果当前线程是此对象锁的所有者,wait()将一直阻塞,因为后续将没有其它notify()唤醒它。

如果在park()之前执行了unpark()会怎样

线程不会被阻塞,直接跳过park(),继续执行后续内容


AQS

AQS(AbstractQueuedSynchronizer)是JUC锁的核心部分,提供了一个基于FIFO的队列,可用于构建锁或其他同步器的基础框架。

例如ReentrantLock、Semaphore、ReentrantReadWriteLock、SynchronousQueue、FutureTask等都是基于AQS实现的。

设计理念

核心思想:

如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态;

如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中

CLH队列是一个虚拟的双向队列(不存在队列实例,仅存在结点之间的关联关系)。AQS将每条请求共享资源的线程封装成一个CLH队列的一个结点来实现锁的分配。

AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。使用CAS对这个状态变量进行维护。

1
private volatile int state;	//使用volatile保证可见性

这个状态信息可通过protected类型的getState、setState、compareAndSetState来进行操作:

1
2
3
4
5
6
7
8
9
10
11
12
//返回同步状态的当前值
protected final int getState() {
return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

AQS对资源的共享方式

AQS定义了两种资源共享的方式:

  • 独占Exclusive:只有一个线程可以得到资源,比如ReentrantLock。又可分为公平锁与非公平锁。
    • 公平锁:按照线程在队列中的顺序,先到者先得到锁
    • 非公平锁:当线程获取锁时,无视队列顺序,竞争获取
  • 共享Share:多个线程同时执行,如Semaphore等

ReentrantReadWriteLock可看作是组合式锁。

数据结构

AQS将每条线程封装成一个CLH队列的结点。其中Sync Queue是同步队列,用双向链表实现;Condition Queue不是必须的,用单向链表实现,只有当使用Condition时,才会存在,且有可能有多个。

AQS的实现原理如图所示:

CLH

以可重入的互斥锁 ReentrantLock 为例,它的内部维护了一个 state 变量,用来表示锁的占用状态。

state 的初始值为 0,表示锁处于未锁定状态。当线程 A 调用 lock() 方法时,会尝试通过 tryAcquire() 方法独占该锁,并让 state 的值加 1。

  • 如果成功了,那么线程 A 就获取到了锁。
  • 如果失败了,那么线程 A 就会被加入到一个等待队列(CLH 队列)中,直到其他线程释放该锁。

假设线程 A 获取锁成功了,释放锁之前,线程A是可以重复获取此锁的(state 会累加)。这就是可重入性的体现:一个线程可以多次获取同一个锁而不会被阻塞。但是,这也意味着,一个线程必须释放与获取的次数相同的锁,才能让 state 的值回到 0,也就是让锁恢复到未锁定状态。只有这样,其他等待的线程才能有机会获取该锁。

基于AQS的常见同步工具类

基于AQS实现的同步工具类一般有:Semaphore信号量、CountDownLatch倒计时器、CyclicBarrier循环栅栏。

Semaphore 信号量

Semaphore可以用来控制同时访问特定资源的线程数量。例如,下面的代码表示同一时刻只有5个线程可以获取到共享资源。

1
2
3
final Semaphore semaphore = new Semaphore(5);	//初始化共享资源份数
semaphore.acquire(); //获取1个许可
semaphore.release(); //释放1个许可

Semaphore有公平式和非公平式两种模式。

CountDownLatch 倒计时器

CountDownLatch允许count个线程阻塞在一个地方,直到所有线程都执行完毕。这里面的count实际上就是对应AQS中的state值。

两种常见用法:

  • 某一线程在开始运行前等待n个线程执行完毕 :

    CountDownLatch的计数器初始化为n,每当一个任务线程执行完毕,就将计数器减1;

    当计数器的值变为 0 时,在CountDownLatch await()的线程就会被唤醒。

    一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。

  • 实现多个线程开始执行任务的最大并行性:

    注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行

    做法是初始化一个共享的CountDownLatch对象,将其计数器初始化为1

    多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。

CyclicBarrier 循环栅栏

CyclicBarrier 循环栅栏实际上是基于ReentrantLock和Condition来实现的。

用来让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。

与CountDownLatch的区别主要在于:

  • CyclicBarrier是可重用的,CountDownLatch用过一次之后就无法重用了
  • 线程“苏醒”的点不同,CyclicBarrier是所有线程到达屏障时,CountDownLatch是计数器归零

ReentrantLock

ReentrantLock基于AQS,实现了Lock接口,是一个可重入独占的锁。

ReentrantLock一共有三个内部类,分别是Sync、NonfairSync、FairSync;其中,Sync继承了AbstractQueuedSynchronizer抽象类,NonfairSync和FairSync继承了Sync。

ReentrantLock和synchronized的区别

相同点:两者都是可重入锁、都是独占式的

不同点:

  • synchronized是JVM层面实现的,ReentrantLock是JDK层面实现的
  • ReentrantLock功能比synchronized更加丰富:
    • 等待可中断:lock.lockInterruptibly()可让正在等待资源的线程放弃等待,改为处理其他事情
    • 实现公平锁:通过构造函数中的fair参数决定是否公平,synchronized只能非公平

ReentrantReadWriteLock

ReentrantReadWriteLock是基于ReentrantLock和AQS实现的。

包含五个内部类:Sync、NonfairSync、FairSync、ReadLock、WriteLock。其中,ReadLock、WriteLock实现了Lock接口。