《Java并发编程的艺术》读书笔记 第五章 Java中的锁
文章目录
1.Lock接口
Lock接口提供的synchronized关键字不具备的主要特性如下所示:
特性 | 描述 |
---|---|
尝试非阻塞的获取锁 | 当线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁 |
能被中断的获取锁 | 与synchronized 不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断的时候,中断异常将会被抛出,同时锁会被释放。 |
超时获取锁 | 在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回 |
Lock是个接口,定义了锁获取和释放的基本操作,Lock的API如下所示。
方法名称 | 描述 |
---|---|
void lock() | 获取锁,调用该方法当前线程将会获取锁,当锁获得之后,从该方法返回 |
void lockInterruptibly() throws InterruptedException | 可中断的获取锁,和lock()方法的不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程 |
boolean tryLock() | 尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false。 |
boolean tryLock(long time,TimeUnit unit) throws InterruptedException | 超时的获取锁,当前线程在以下3种情况下会返回: 1)当前线程在超时时间内获得了锁 2)当前线程在超时时间内被中断 3)超时时间结束,返回false |
void unlock() | 释放锁 |
Condition New Condition() | 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而调用后,当前线程将释放锁 |
2.队列同步器
队列同步器AbstractQueuedSynchronized
,是用来构建锁或者其他同步组件的基础框架。使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
同步器主要使用方式是继承,一般通过下面3个方法对同步状态进行更改,它们能够保证状态的改变是安全的。
getState()
setState(int newState)
compareAndSetState(int expect,int update)
锁是面向使用者的,它定义了使用者和锁交互的接口,隐藏了实现细节,同步器面向的是锁的实现者,简化锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。
3.重入锁
ReentrantLock,支持重进入的锁,表示该锁能够支持一个线程对资源的重复加锁。此外,还支持获取锁时的公平和非公平性选择。
实现重进入
该特性的实现需要解决下面2个问题。
- 线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取
- 锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁,锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放的时候,计数自减,当计数表示等于0的时候表示锁已经成功释放。
公平与非公平获取锁的区别
1.公平锁中, 线程严格按照先进先出(FIFO)的顺序, 获取锁资源
2.非公平锁中, 拥有锁的线程在释放锁资源的时候, 当前尝试获取锁资源的线程可以和等待队列中的第一个线程竞争锁资源, 这就是ReentrantLcok中非公平锁的含义; 但是已经进入等待队列的线程, 依然是按照先进先出的顺序获取锁资源
4.读写锁
之前提到的Mutex和ReentrantLock基本都是排他锁,这些锁同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问的时候,所有的读线程和其它写线程都会被阻塞。读写锁维护了一对锁,一个读锁一个写锁。
Java并发包提供读写锁的实现是ReentrantReadWriteLock,提供的特性如下:
特性 | 说明 |
---|---|
公平性选择 | 支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平 |
重进入 | 该锁支持重进入,以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁。而写线程在获取了写锁之后能够再次获取写锁,同时也可以获取读锁 |
锁降级 | 遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级为读锁 |
5.LockSupport工具
LockSupport定义了一组以park
开头的方法用来阻塞当前线程,以及unpark(Thread thread)方法来唤醒一个被阻塞的线程。Park有停车的意思,假设线程为车辆,那么park方法代表着停车,而unpark方法则是指车辆启动离开。
Java 6种,有提供了三个含表示当前线程阻塞对象的方法,它们分别是:
park(Object blocker)
parkNanos(Object blocker,long nanos)
parkUntil(Object blocker,long deadline)
blocker就是阻塞对象,该对象主要用于问题排查和系统监控。以parkNanos(Object blocker,long nanos)和parkNanos(long nanos)为例,有阻塞对象的parkNanos方法能够传递给开发人员更多的现场信息。
6.Condition接口
Condition接口提供了类似Object的监视器方法,与Lock配合可以实现等待-通知模式,但是这两者在使用方式以及功能特性上还是有区别的。
Condition对象是由Lock对象创建出来的Lock对象的newCondition()方法
,换句话说Condition是依赖Lock对象。
使用Condition的时候,主要在调用方法前应该获取锁。
6.1 Condition的实现分析
ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含一个队列,该队列是Condition对象实现等待/通知的关键。
下面分析Condition的实现,如果不加以说明,提到的Condition都是指ConditionObject。
等待队列
等待队列是一个FIFO的队列,在队列的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。同步队列和等待队列种节点类型都是同步器的静态内部类AbstractQueuedSynchronizer.Node。
一个Condition包含一个等待队列,Condition拥有首节点和尾节点,当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列,等待队列的基本结构如下:
Condition拥有首尾节点的引用,而新增节点只需要将原有的尾节点指向它,并且更新尾节点即可。
在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包种的Lock拥有一个同步队列和多个等待队列。
等待
调用Condition的await()方法,会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从awiat()方法返回的时候,当前线程一定获取了Condition相关联的锁。
调用await()方法的线程成功获取了锁的线程,也就是同步队列的首节点,该方法会将当前线程构造成节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。
当等待队列中的节点被唤醒,则唤醒节点的线程开始尝试获取同步状态。如果不是通过其它线程调用Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出InterruptedException
。
通知
调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点,也就是首节点,在唤醒节点之前,会将节点移动到同步队列中。
在源码中,可以看到首先对当前线程是否获取了锁进行判断isHeldExclusively()
,如果当前线程获取了锁,那么获取等待队列的首节点,将它移动到同步队列中并使用LockSupport唤醒节点中的线程。
Condition的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal方法,效果为将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。