重读Java并发编程艺术(4) - Java中的锁

本文深入探讨Java并发编程的关键概念,包括Lock接口、队列同步器AQS、ReentrantLock、读写锁ReadWriteLock、LockSupport工具和Condition接口。详细分析了各种锁的实现原理,如重入锁的支持、公平与非公平锁的区别、读写锁的特点以及LockSupport和Condition的使用方法。
摘要由CSDN通过智能技术生成

1. Lock接口

1.1背景

  • synchronized 隐式获取锁,简化同步的管理,但是缺乏扩展性。
  • 手动进行锁获取和释放,还具有以下 synchronized 关键字不具备的主要特性:
特性描述
尝试非阻塞地获取锁当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,这成功获取并持有锁
能被中断地获取锁与 synchronized 不同,获取到锁地线程能够响应中断,当获取到锁地线程被中断时,中断异常将会被抛出,同时锁会被释放
超时获取锁在指定地截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回

1.2 使用方式

Lock lock = new ReentrantLock();
lock.lock();
try{

}finally{
	lock.unlock();//在 finally 块中保证最终能够释放锁
}

不要将获取锁的过程写在 try 块中,因为如果在获取锁(自定义锁的实现)时发生异常,异常抛出的同时,也会导致锁无故释放

1.3 Lock 的 API

方法名称描述
void lock()获取锁,调用该方法当前线程将会获取锁,当锁获得后,从该方法返回
void lockInterruptibly() throws InterruptedException可中断地获取锁,和 lock() 方法地不同之处在于该方法会响应中断,即在锁地获取中可以中断当前线程
boolean tryLock()尝试非阻塞地获取锁,调用该方法后立刻返回,如果能够获取则返回 true,饭否这返回 false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException超时的获取锁,当前线程在以下3种情况下会返回:① 当前线程在超时时间内获得了锁 ② 当前线程在超时时间内被中断 ③ 超时时间结束,返回 false
void unlock()释放锁
Condition newCondition()获取等待通知得组件,该组件和当前得锁绑定,当前线程只有获得了锁,才能调用该组件得 wait() 方法, 而调用后当前线程将释放锁

1.4 实现

Lock接口的实现基本都是通过聚合一个同步器的之类来完成线程访问控制的。

2. 队列同步器(AQS)

2.1 简介

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

2.2 使用方式

主要使用方式是继承,同步器基于模板方法模式设计。
使用者继承同步器并重写指定方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。

同步器是实现锁的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。
(锁面向使用者,同步器面向锁的实现者)

2.3 接口方法

2.3.1 基本方法

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

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

2.3.2 同步器可重写的方法

方法名称描述
protected boolean tryAcquire(int arg)独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行 CAS 设置同步状态
protected boolean tryRelease(int arg)独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态
protected int tryAcquireShared(int arg)共享式获取同步状态,返回大于等于0的值,表示获取成功,反之,获取失败
protected boolean tryReleaseShared(int arg)共享式释放同步状态
protected boolean isHeldExclusively()当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占

2.3.4 同步器提供的模板方法

实现自定义同步组件时,将会调用同步器提供的模板方法,部分如下:

方法名称描述
void acquire(int arg)独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用重写的 tryAcquire(int arg) 方法
void acquireInterruptibly(int arg)与 acquire(int arg) 相同,但是该方法响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前线程被中断,则该方法会抛出 InterruptedException 并返回
boolean tryAcquireNanos(int arg, long nanos)在 acquireInterruptibly(int arg) 基础上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,那么将会返回 false,如果获取到了返回 true
void acquireShared(int arg)共享式的获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式获取的主要区别是在同一时刻可以有多个线程获取到同步状态
void acquireSharedInterruptibly(int arg)与 acquireShared(int arg) 相同,该方法响应中断
boolean tryAcquiredSharedNanos(int arg, long nanos)在 acquireSharedInterruptibly(int arg) 基础上增加了超时限制
boolean release(int arg)独占式的释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒
boolean releaseShared(int arg)共享式的释放同步状态
Collection<> getQueuedThreads()获取等待在同步队列上的线程集合

主要分为3类:

  • 独占式获取与释放同步状态
  • 共享式获取与释放同步状态
  • 查询同步队列中的等待线程情况

2.4 队列同步器的实现

2.4.1 同步队列

同步器依赖内部的同步队列(FIFO双向队列)来完成同步状态的管理,
当前线程获取同步状态失败时,同步器将当前线程及等待状态等信息构造成为一个节点 (Node) 并将其加入同步队列,同时阻塞当前线程;
当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
在这里插入图片描述

2.4.2 独占式同步状态获取与释放

2.4.2.1 方法

boolean tryAcquire(int arg) 根据结果 boolean 值判断是否能够获取成功

2.4.2.2 获取及释放流程

在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;一处队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用 tryRelease(int arg) 方法释放同步状态,然后唤醒头节点的后继节点。
在这里插入图片描述

2.4.3 共享式同步状态获取与释放

2.4.3.1 共享式和独占式区别

同一时刻能否有多个线程同时获取到同步状态。

2.4.3.2 应用

如文件读写,写操作要求对资源的独占式访问,读操作可以是共享式访问。

2.4.3.3 方法

int tryAcquireShared(int arg) 返回 int 值大于等于0时表示能获取到同步状态。

2.4.3.4 释放同步状态

和独占式主要区别在于:
boolean tryReleaseShared(int arg)必须确保同步状态(或资源数)线程安全释放,一般通过循环和 CAS 来保证, 因为释放同步状态的操作会同时来自多个线程。

2.4.4 独占式超时获取同步状态

2.4.4.1 方法

boolean doAcquireNanos(int arg, long nanosTimeout)
在指定时间段内获取同步状态,如果获取到则返回 true,否则返回 false

2.4.4.2 超时计算方法
  • nanosTimeout 需要睡眠的时间间隔
  • now 当前唤醒时间
  • lastTime 上次唤醒时

计算公式: nanosTimeout -= now - lastTime
计算之后如果 nanosTimeout 大于0则表示超时时间未到,需要继续睡眠 nanosTimeout 纳秒;反之,表示已经超时。

2.4.4.3 独占式超时获取同步状态的流程

在这里插入图片描述

3. 重入锁 ReentrantLock

3.1 定义

支持一个线程对资源的重复加锁,另外还支持获取锁时的公平和非公平性选择。

3.2 作用

支持占有锁的线程再次获取锁的场景,任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,多次调用 lock() 方法不会被自己所阻塞。

3.3 实现重进入

3.3.1 实现要点

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

3.3.2 代码实现原理

  • 获取:增加逻辑,判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则增加同步状态值并返回 true,表示获取同步状态成功。
  • 释放:如果该锁被获取了 n 次,那么前(n-1)次 tryRelease(int releases) 方法必须返回 false,只有同步状态为 0 时表示完全释放,此时才能返回 true,并将占有线程设置为 null,表示释放成功。

3.4 公平与非公平获得锁的区别

3.4.1 公平性

如果一个锁是公平的,那么锁的获取顺序应该符合请求的绝对时间顺序,也就是 FIFO。
ReentrantLock 提供一个构造函数控制锁是否公平。

3.4.2 作用

公平锁能够减少“饥饿”发生的概率,等待越久的请求越是能够得到优先满足。
(公平的锁机制往往没有非公平的效率高)

3.4.3 代码实现原理

在 tryAcquire(arg) 中多了 hasQueuedPredecessors() 的判断,判断同步队列中当前节点是否有前驱节点,该方法返回 true 则表示有线程比当前线程更早地请求获取锁,因此当前线程需要等待前驱线程获取并释放锁之后才能继续获取锁。

4. 读写锁 ReadWriteLock

4.1 定义

读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,并发性比排他锁有很大提升。

4.2 特点

像 Mutex 和 ReentrantLock 等都是排他锁,同一时刻只允许一个线程访问,读写锁在同一时刻可以允许多个读线程访问,但是写线程访问时,所有的读线程和其他写线程均被阻塞。

大多数场景下读多于写,故读写锁能够比排他锁具有更好地并发性和吞吐量。
Java 并发包提供的实现 ReentrantReadWriteLock 提供如下特性

特性说明
公平性选择支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平
重进入支持重进入,以读写线程为例:读线程获取读锁后可以再次获取读锁;写线程获取写锁之后能够再次获取写锁,同时也可以获取读锁
锁降级遵循获取写锁、获取读锁再释放写锁的次序,写锁能降级成为读锁

4.3 实现

4.3.1 读写状态的设计

通过位运算“按位切割”,将一个整型变量分成两个部分,维护两种状态,高16位表示读的重入次数,低16位表示写的重入次数。

设当前同步状态值S,

  • 写状态等于 S & 0x0000FFFF(抹去高16位);
  • 读状态等于 S >>> 16 (无符号补0右移16位);
  • 当写状态增加1时,等于 S+1;
  • 当读状态增加1时,等于 S+(1<<16);

4.3.2 写锁的获取与释放

写锁是一个支持重进入的排它锁。

  • 如果当前线程已获取了写锁,则增加写状态;
  • 如果当前线程在获取写锁时,读锁已被获取(读状态不为0),或该线程不是已经获取写锁的线程,则当前线程进入等待状态。

4.3.3 读锁的获取与释放

读锁时一个支持重进入的共享锁,能够被多个线程同时获取。

  • 在没有其他写线程访问(写状态位0)时,读锁总会被成功地获取,增加读状态。
  • 如果当前线程已经获取了读锁,增加读状态。
  • 如果当前线程在获取读锁时,写锁已经被其他线程获取,则进入等待状态。

4.3.4 锁降级

指"写锁"降级成为"读锁"。把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。

/**
 * 代码示例:锁降级
 * 因为数据不常变化,故多个线程可以并发进行数据处理,
 * 当数据变更后,如果当前线程感知到数据变化,则进行数据的准备工作,
 * 同时其他处理线程被阻塞,知道当前线程完成数据的准备工作。
 */
public void processData(){
    readLock.lock();
    //使用update变量(volatile修饰的boolean值)作为状态
    if(!update){
	//必须先释放读锁
	readLock.unlock();
	//锁降级从写锁获取开始
	writeLock.lock();
	try{
	    if(!update){
		//准备数据的流程(略)
		update = true}
	    //重新获取读锁保证数据的可见性
	    readLock.lock();
	}finally{
	    writeLock.unlock();
	}
	//锁降级完成
    }
    try{
	//使用数据的流程(略)
    }finally{
	readLock.unlock();
    }
}

RentrantReadWriteLock 不支持锁升级(把持读锁、获取写锁、释放读锁),目的也是保证数据可见性。(如果多个线程获取读锁,其中任意线程成功获取写锁并更新数据,其更新对其他获取到读锁的线程是不可见的)

5. LockSupport 工具

5.1 介绍

LockSupport 为构建同步组件的基础工具,定义了一组公共静态方法,提供最基本的线程阻塞和唤醒功能。

5.2 方法

方法名称描述
void park()阻塞当前线程,如果调用 unpark(Thread thread) 方法或者当前线程被中断,才能从 park() 方法返回
void parkNanos(long nanos)阻塞当前线程,最长不超过 nanos 纳秒,返回条件在 park() 的基础上增加了超时返回
void parkUntil(long deadline)阻塞当前线程,知道 deadline 时间(从 1970 年开始到 deadline 时间的毫秒数)
void unpark(Thread thread)唤醒处于阻塞状态的线程 thread

5.3 新方法

在 Java 6 中,LockSupport 增加了三个方法:

  • park(Object blocker)
  • parkNanos(Object blocker, long nanos)
  • parkUntil(Object blocker, long deadline)
    其中参数 blocker 用来标识当前线程等待的对象(阻塞对象),主要用于问题排查和系统监控。(如线程 dump 中可以看到阻塞对象)

6. Condition 接口

6.1 和对象监视器方法对比

对比项Object Monitor MethodsCondition
前置条件获取对象的锁1. 调用 Lock.lock() 获取锁;
2. 调用Lock.newCondition()获取Condition对象
调用方式直接调用
如:obj.wait()
直接调用
如:condition.wait()
等待队列个数一个多个
当前线程释放锁并进入等待状态支持支持
当前线程释放锁并进入等待状态,在等待状态中不响应中断不支持支持
当前线程释放锁并进入超时等待状态支持支持
当前线程释放锁并进入等待状态到将来的某个时间不支持支持
唤醒等待队列中的一个线程支持支持
唤醒等待队列中的全部线程支持支持

6.2 接口示例

Lock lock = new ReentrantLock();
//一般将condition对象作为成员变量
Condition condition = lock.newCondition();

public void conditionWait() throws InterruptedException{
    lock.lock();
    try{
        condition.await();
    } finally {
        lock.unlock();
    }
}

public void conditionSignal() throws InterruptedException{
    lock.lock();
    try{
        condition.signal();
    } finally {
        lock.unlock();
    }
}

Condition 部分方法如下表:

方法名称描述
void await() throws InterruptedException当前线程进入等待状态知道被通知(signal)或中断,当前线程将进入运行状态且从await()方法返回的情况,包括:
- 其他线程调用该Condition的signal()或signalAll()方法,而当前线程被选中唤醒
- 其他线程(调用interrupt()方法)中断当前线程
- 如果当前等待线程从await()方法返回,那么表明该线程已经获取了Condition对象所对应的锁
void awaitUninterruptibly()当前线程进入等待状态直到被通知,对中断不敏感
long awaitNanos(long nanosTimeout) throws InterruptedException当前线程进入等待状态直到被通知、中断或超时。返回值表示剩余的时间,如果在nanosTimeout纳秒之前被唤醒,则返回值为(nanosTimeout-实际耗时)。如果返回值是0或负数,则可认定为已超时
boolean awaitUnitl(Date deadline) throws InterruptedException当前线程进入等待状态直到被通知、中断或到达某个时间点。如果没有到指定时间就被通知,方法返回true,否则,表示到了指定时间,返回false
void signal()唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition相关联的锁
void signal()唤醒所有等待在Condition上的线程,能够从等待方法返回的线程必须获得与Condition相关联的锁

6.3 实现分析

以ConditionObject示例:同步器 AbstractQueuedSynchronizer 的内部类。
每个Condition对象都包含一个等待队列,是实现等待/通知功能的关键。

6.3.1 等待队列

一个 FIFO 队列,每个节点包含一个线程应用,该线程就是在 Condition 对象上等待的线程。
如果一个线程调用了 Condition.await() 方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。
在这里插入图片描述

  • Object 监视器模型一个对象拥有一个同步队列和等待队列。
  • 并发包中的 Lock(同步器)拥有一个同步队列和多个等待队列。
    在这里插入图片描述
    Condition 的实现是同步器的内部类,故每个 Condition 实例都能够访问同步器提供的方法。

6.3.2 等待

调用 Condition 的 await() 方法,会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从 await() 方法返回时,当前线程一定获得了 Condition 相关联的锁。

从队列角度看,调用 await() 方法时,同步队列的首节点(获取了锁的节点)移动到 Condition 的等待队列中。

6.3.3 通知

调用 Condition 的 signal() 方法,将唤醒在等待队列中等待时间最长的节点(首节点),并将节点移到同步队列中,然后当前线程再使用 LockSupport 唤醒该节点的线程。
调用该方法前置条件时当前线程必须下获取了锁。
signalAll() 方法相当于对等待队列中的每个节点均执行了一次 signal() 方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值