CreateArtTechnology
/ Blog
Login
最新文章
Java
语言相关
库相关
虚拟机相关
CreateArtTechnology
项目搭建
使用的工具
自研的工具
开源工具
ELK
ElasticSearch
Jenkins
Markdown
GraphQL
Arthas
生产工具
Linux
Nginx
VersionControl
Subversion
Git
Redis
Archiva
Maven
Zookeeper
Spring
SpringBoot
MySql
HBase
Cassandra
容器化
Docker
Kubernetes
服务容器化从零开始
未分类笔记
算法相关
概念相关
豆知识
机器学习
机器学习从零开始
Java juc笔记 [2] - locks包接口部分
7
2020-09-28 19:59:05
Java
语言相关
## Locks包简介 java.util.concurrent包下的locks包提供了一系列同步工具,其功能在大体上跟synchronized差不多,但量级更轻更灵活,也更复杂。juc包下的多种用于同步的工具包括`CyclicBarrier`、`LinkedBlockingQueue`等都依赖这些同步工具。 本次先研读Lock相关的接口及LockSupport类源码以了解常见的使用方式及需要注意的点。 ## 介绍 ### Lock接口 给特定资源加锁,即同时只有一个线程能访问该资源,其他资源要么等待获取锁后访问资源,要么放弃访问资源(具有排他性)。语义同Synchronized但在使用上更为灵活,且需要手动释放锁。 在注释中提到的典型应用场景是: 获取A的锁->获取B的锁->释放A的锁->获取C的锁->释放B的锁->获取D的锁...... 很明显,这种方式一旦使用不当非常容易产生死锁,这就是灵活带来的代价。 一般的应用方式: ```java Lock l = ...; // 如果未获取到锁,线程会在这一步休眠 l.lock(); try { // 访问受保护资源 } finally { // 在finally中释放锁 l.unlock(); } ``` 一定要按照获取锁的顺序释放锁,如获取A锁->B锁后释放B锁->A锁,否则容易产生死锁。 **总览** 我们通常谈到的锁本质上是“钥匙”而并非“锁”。在逻辑上“受保护的资源”可以理解为是房间内的东西,由系统把关进入这个房间的“锁”(或“门”),而程序的多个线程通过Lock向系统申请进入房间,系统会在发现房间里没人时会按照一定方式(可理解为随机,与线程调度执行有关)从中选择给某个线程唯一的“钥匙”进入房间访问;而其他没有钥匙的线程则不能进入,通常会等待其他线程从房间出来后由系统回收钥匙分发给自己。等待分配钥匙的过程也就是“等待锁”的过程,获得了钥匙即“获取到锁”,而离开房间系统回收钥匙的过程即“释放锁”。 正因为系统分配锁基本是随机的,我们无法预知下一个获取锁的线程到底是谁,所以在使用锁的时候就必须考虑到释放锁的时机及释放锁后被非预期的线程获取到锁时应当如何处理,如公平锁、非公平锁的实现等。 Lock接口提供了阻塞获取锁、可中断的阻塞获取锁、不阻塞尝试获取锁、阻塞一定时间尝试获取锁这几种语义的加锁方法,通常根据使用方式的不同编码逻辑会稍有区别,但原则是一样的:获取到锁才能访问受保护资源,否则阻塞或继续执行不访问受保护资源时的逻辑。 **void lock()** 获取锁,否则休眠。只有2种结果: - 获取到锁,线程继续运行 - 未获取锁,阻塞直到获取到锁(其他线程释放锁) **void lockInterruptibly() throws InterruptedException** 获取锁并在本线程中断时抛出异常。4种结果: - 获取到锁,线程继续运行 - 未获取锁,休眠,直到获取到锁(其他线程释放锁) - 未获取锁,休眠,直到被中断并抛出异常(需要实现类支持) - 进入该方法时已经被设置了中断状态(也即被通知需中断),抛出InterruptedException并清除中断状态 **boolean tryLock()** 不等待而立刻返回加锁情况。2种结果: - 获取到锁,返回true - 未获取到锁,返回false 一般的应用方式: ```java Lock lock = ...; // tryLock不阻塞,需要使用if else语句处理 if (lock.tryLock()) { try { // 访问受保护资源 } finally { // 仅tryLock成功才需要unlock lock.unlock(); } } else { // 获取锁失败,做其他事 } ``` **boolean tryLock(long time, TimeUnit unit) throws InterruptedException** 结合了lockInterruptibly()和tryLock(),在一定时间内阻塞等待获取锁,在这段时间内表现和lockInterruptibly()一样;在超过指定时间之后,立刻返回加锁结果,和tryLock()一样。5种结果: - 获取到锁,返回true,线程继续运行 - 未获取到锁,休眠,直到获取到锁(其他线程释放锁),返回true - 未获取到锁,休眠,直到被中断抛出异常(需要实现类支持) - 未获取到锁,休眠,直到超过指定时间,返回false - 进入该方法时已经被设置了中断状态(也即被通知需中断),抛出InterruptedException并清除中断状态 **void unlock()** 释放锁。 **Condition newCondition()** 创建一个Condition实例,在当前线程持有锁的情况下,Condition.await()方法将释放当前锁并在重新获取该锁后返回。 ### ReadWriteLock接口 维护一对关联的读写锁,即允许多个线程在写锁空闲时同时持有读锁(共享锁),但只允许有一个线程持有写锁(排他锁)。这里的读写并非真正需要进行“读”或“写”操作,而是指“不修改数据”的共享操作和“会修改数据”的排他操作抽象而来的“读”与“写”。 理论上读写锁的读操作并发性能会比排他锁更高,但实际情况与读写操作的比例有关,即如果读操作数量远高于写操作,读写锁的收益将会很高。 **Lock readLock() / Lock writeLock()** 获取读锁/写锁。ReadWriteLock并未继承Lock接口,而是提供了两个获取Lock的方法。也就是说ReadWriteLock并不是锁而是锁的维护者。 ### Condition接口 提供对于同一锁的更细粒度控制,本质上多个Condition实例使用同一个Lock实例的锁,但可以在不满足条件时释放锁并在条件满足后重新获得锁(需要正确的逻辑保证)。在注释中实现了一个在`ArrayBlockingQueue`中提供的非常典型的生产者消费者场景: ```java class BoundedBuffer { final Lock lock = new ReentrantLock(); // 同一个锁的不同condition即应用条件 final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition(); final Object[] items = new Object[100]; int putptr, takeptr, count; public void put(Object x) throws InterruptedException { // 加锁,如果失败则阻塞 lock.lock(); try { while (count == items.length) /* 加锁成功但notFull条件不满足,会释放lock的锁并阻塞等待直到signal唤醒 注意是使用while循环,避免在释放锁后自己重新获取锁时仍然不满足条件 也就是注释中提到的spurious wakeup虚假唤醒 */ notFull.await(); // 访问受保护资源 items[putptr] = x; if (++putptr == items.length) putptr = 0; ++count; // 在释放锁前通知notEmpty条件,即可以take了,但不释放锁 // signal在try代码块内调用,保证逻辑正确 notEmpty.signal(); } finally { // 注意最后再释放锁,condition的await和signal方法必须以持有锁为前提 lock.unlock(); } } public Object take() throws InterruptedException { lock.lock(); try { while (count == 0) notEmpty.await(); Object x = items[takeptr]; if (++takeptr == items.length) takeptr = 0; --count; notFull.signal(); return x; } finally { lock.unlock(); } } } /** * 附 main方法测试 */ public static void main(String[] args) { BoundedBuffer buffer = new BoundedBuffer(); new Thread(() -> { try { int i = 0; while (i++ < 10) { buffer.put(i); System.out.println("put: " + i); } } catch (InterruptedException e) { e.printStackTrace(); } }).start(); new Thread(() -> { try { int i = 0; while (i++ < 10) { System.out.println("take: " + buffer.take().toString()); } } catch (InterruptedException e) { e.printStackTrace(); } }).start(); } ``` **总览** 更细粒度的Condition提供了阻塞/唤醒特定条件线程的方式,但唤醒操作如同加锁一样可能是无法预料是哪个线程被唤醒的,因此也需要考虑到非预期线程被唤醒时的处理方式。 Condition通常的使用原则:在调用任何Condition方法前需要获取其对应lock的锁,即lock成功;通常await相关方法在while循环中调用,while判断条件就是await等待的条件,使用while避免在非预期条件下获取到锁;通常在特定条件满足后调用signal或signalAll方法唤醒特定condition对应的线程。 需要注意的是lock对应的任何condition持有的锁与lock是同一个锁,lock.unlock等效于所有condition.signalAll。 使用Condition可能会有多次释放/获取锁的过程,即: - 调用lock获取锁 - 不满足条件时,进入await释放锁 - 其他线程调用signal/signalAll,当前线程获取了锁,await方法返回 - 访问受保护资源后,调用unlock释放锁 **void await() throws InterruptedException** 功能类似Lock.lockInterruptibly(),但唤醒条件不是其他线程释放锁,而是调用了该Condition实例的signalAll方法,抑或是调用signal方法时恰好当前线程获取到了锁。在await调用之前必须确保获取了对应lock的锁(见上述代码)否则抛异常,而返回前一定是当前线程获取到了锁。 **void awaitUninterruptibly()** 功能类似Lock.lock(),不支持中断及抛异常,使用方法参考await()。 **long awaitNanos(long nanosTimeout) throws InterruptedException** 功能类似await,因为获取锁的线程不确定,可能经常会出现“虚假唤醒”的情况,即被唤醒后条件仍不满足。这个方法能确认在不满足条件下是否等待过久。 返回=输入参数-实际等待时机,通常返回值用于下一次输入,典型应用方式: ```java boolean aMethod(long timeout, TimeUnit unit) { long nanos = unit.toNanos(timeout); lock.lock(); try { while (!conditionBeingWaitedFor()) { if (nanos <= 0L) // 等待过久了 return false; // 继续等待,使用返回值作为下次的输入参数 nanos = theCondition.awaitNanos(nanos); } // ... } finally { lock.unlock(); } } ``` **boolean await(long time, TimeUnit unit) throws InterruptedException** 等价于`awaitNanos(unit.toNanos(time)) > 0`,返回是否在指定时间内return。 **boolean awaitUntil(Date deadline) throws InterruptedException** 与`awaitNanos`类似,返回是否在指定时间内return,典型应用方式: ```java boolean aMethod(Date deadline) { boolean stillWaiting = true; lock.lock(); try { while (!conditionBeingWaitedFor()) { if (!stillWaiting) return false; stillWaiting = theCondition.awaitUntil(deadline); } // ... } finally { lock.unlock(); } } ``` **void signal()** 唤醒某个等待该condition的线程,类似unlock方法。在调用前必须持有lock的锁,见前述代码。 **void signalAll()** 唤醒所有等待该condition的线程,但对应的各个线程仍需要先获取锁。 ### LockSupport 提供暂停/唤醒线程的基础能力,实际上是对UNSAFE的一些方法进行包装,包括park/unpark在内的方法均通过UNSAFE实现。 **总览** 与Lock的语义类似,但从方法签名可以看出一些不同: - park和unpark方法均无返回参数,因此在park后无法直接获知是因什么原因从暂停到唤醒,可能的原因包括: 1. 被其他线程唤醒 2. 被其他线程中断(可以后续从Thread中断标识位判断) 3. 其他无故唤醒 - park和unpark方法均为静态方法 - unpark方法输入参数为线程对象,即当前线程唤醒其他线程 - park方法输入参数不包含线程对象,即只能暂停当前线程 **public static void unpark(Thread thread)** 唤醒目标线程。 **public static void park** 对应线程暂停,根据注释描述,有些情况下会无故返回且不暂停线程导致语义与预期不符,因此需要通过循环检查确保线程暂停,在juc包中的应用均采用该形式,在不满足条件前均循环park当前线程。 **public static void parkNanos / public static void parkUntil** 在一定时间内暂停线程。
发布文章 101
文章被阅读 1602
最近修改
什么是“丝滑”的曲线
2021-12-08 15:19:20
高效空间数据索引R树及其批量加载方法STR简介
2021-09-29 20:33:37
关于分库分表的一些事儿
2021-06-25 11:51:25
获得诺奖的稳定匹配理论之TTC算法与GS算法
2021-03-14 23:04:48
算法小白的机器学习入门实践,从零到上线
2021-01-13 14:28:27
分站宗旨
一站式资料平台,减少重复检索,减少重复采坑。