JUC锁
JUC锁
JUC锁有一些比较重要的部分,包括LockSupport、AQS、ReentrantLock、ReentrantReadWriteLock。
LockSupport
LockSupport是一个提供锁机制的工具类,可以说是JUC锁实现的基础。
其一般用来支持线程的阻塞和唤醒操作,比传统的wait()
和notify()
更加灵活和强大。
核心函数分析
LockSupport的核心函数都是基于Unsafe类中定义的park和unpark函数:
1 | public native void park(boolean isAbsolute, long time); |
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 | //返回同步状态的当前值 |
AQS对资源的共享方式
AQS定义了两种资源共享的方式:
- 独占Exclusive:只有一个线程可以得到资源,比如ReentrantLock。又可分为公平锁与非公平锁。
- 公平锁:按照线程在队列中的顺序,先到者先得到锁
- 非公平锁:当线程获取锁时,无视队列顺序,竞争获取
- 共享Share:多个线程同时执行,如Semaphore等
ReentrantReadWriteLock可看作是组合式锁。
数据结构
AQS将每条线程封装成一个CLH队列的结点。其中Sync Queue是同步队列,用双向链表实现;Condition Queue不是必须的,用单向链表实现,只有当使用Condition时,才会存在,且有可能有多个。
AQS的实现原理如图所示:
以可重入的互斥锁 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 | final Semaphore semaphore = new Semaphore(5); //初始化共享资源份数 |
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接口。