并发编程实战学习笔记(十)-构建自定义的同步工具

来源:互联网 发布:风之利刃数据 编辑:程序博客网 时间:2024/06/05 18:38

并发编程实战学习笔记(十)-构建自定义的同步工具

核心概念【状态依赖】

程序在做某一个操作之前,需要依赖另一个操作的完成或者状态的就绪,这样的一种关系就叫做“状态依赖”。

状态依赖的实现类,就是并发工具的原语。

例如FutureTask、Semaphore和BlockingQueue等。在这些类的一些操作中有着基于状态的前提条件,例如,不能从一个空队列删除元素,或者获取一个尚未结束的任务的计算结果,在这些操作可以执行之前,必须等待队列进入“非空”状态,或者任务进入“已完成”状态。

实现了状态依赖的底层类有:内置的条件队列、显示的Condition对象以及AbstractQueuedSynchronizer框架。

并发机制原语,包含有两个

  • 原子操作,最终的操作才能保证线程安全性。

锁机制或者直接使用原子类都能实现应用程序中的原子操作,但原子操作的原语则不能依赖锁机制,必须依赖于操作系统本身才行,否则会陷入先有鸡还是先有蛋的悖论,因为锁机制原语本身是有依赖原子操作的。

  • 阻塞线程,并能在依赖状态满足时即时唤醒。

阻塞与唤醒、自旋都是实现状态依赖的思路。

条件队列

概念

它合得一组线程(称之为等待线程集合)能够通过某种方式来等待特定的条件变为真。传统的队列是一个个数据,而与之不同的是,条件队列中的元素是一个个正在等待相关条件的线程。

要点

  • Object中的wait、notify和notifyAll方法构成了内部条件队列的API。

  • 对象的内置锁与内部条件是相互关联的,要调用对象X中的条件队列的任何一个方法,必须持有对象X上的锁。

这是因为“等待由状态构成的条件”与“维护状态一致性”这两种机制必须被紧密地绑定在一起。只有能对状态进行检查时,才能在某个条件上等待,并能只有能修改状态时,才能从条件等待中释放另一个线程。

  • Object.wait会自动释放锁,并请求操作系统挂起当前线程,从而使其它线程能够获得这个锁并修改对象的状态。当被挂起的线路醒来时,它将在返回之前重新获取锁。(需要重新竞争,并没有优先获取权)

条件谓词

概念

条件谓词是使某个操作成为状态依赖操作的前提条件。在有界缓存中,只有当缓存不为空时,take方法才能执行,否则必须等待。对take方法来说,它的条件谓词就是“缓存不为空”,take方法在执行之前必须首先测试该条件谓词。

条件谓词与条件队列的关系

每一次wait调用都会隐式地与特定的条件谓词关联起来。当调用某个特定条件谓词的wait时,调用者必须已经持有与条件队列相关的锁,并且这个锁必须保护着构成条件谓词的状态变量。

当使用条件等待时要满足的条件(Object.wait或Condition.wait)

  • 通常都有一个条件谓词——包括一些对象状态的测试,线程在执行前必须首先通过这些测试。
  • 在调用wait之前测试条件谓词,并且从wait中返回时再次进行测试。
  • 在一个循环中调用wait。
  • 确保使用与条件队列相关的锁来保护构成条件谓词的各个状态变量。
  • 当调用wait/notify/notifyAll等方法时,一定要持有与条件队列相关的锁。
  • 在检查条件谓词之后以及开始执行相应的操作之前,不要释放锁。

丢失的信号

notify或者notifyAll操作发生在wait之前,就会造成通知信号的丢失,最终wait永远都得不到恢复或者不得不等待下一次重新通知而延迟了恢复时间。

通知

注意事项:发出通知的线程应该尽快地释放锁,从而确保正在等待的线程尽可能快地解除阻塞。如果这些等待中线程此时不能重新获得锁,那么无法从wait返回。

优先选择notifyAll而不是单个的notify的原因

由于多个线程可以基于不同的条件谓词在同一个条件队列上等待,因此如果使用notify而不是notifyAll,那么将是一个危险的操作,因为单一的通知很容易导致类似信号丢失的问题。为什么说是类似,因为导致的问题是相同的:线程正在等待一个已经(或者本应该)发生过的信号。

使用单一的notify而不是notifyAll的条件

  • 所有等待线程的类型都相同。只有一个条件谓词与条件队列相关,并且每个线程在从wait返回后将执行相同的操作。
  • 单进单出。在条件变量上的每次通知,最多只能唤醒一个线程来执行。

子类的安全问题

对于状态依赖的类,要么将其等待和通知等协议完全向子类公开(并且写入正式文档),要么完全阻止子类参与到等待与通知等过程中。

这是对“要么围绕着继承来设计和文档化,要么禁止使用继承”这条规则的一种扩展【EJ ITEM 15】

显示Condition对象

内置条件队列的局限性

每个内置锁都只能有一个相关联的条件队列,因而在像BoundBuffer这种类中,多个线程可能在同一个条件队列上等待不同的条件谓词,并且在最常见的加锁模式下公开条件队列对象。

显示条件队列的优势

  • 可以编写一个带有多个条件谓词的并发对象,或者获得除了条件队列可见性之外的更多控制权,这是一种灵活的选择。
  • 对于每个Lock,可以有任意数量的Condition对象。Condition对象继承了相关的Lock对象的公平性,对于公平的锁,线程会依照FIFO顺序从Condition await中释放。

特别注意:Condition对象中,三个与条件队列相关的API是:await,signal,signalAll。不要使用错了。

AbstractQueuedSynchronizer

  • 无论是获取和释放锁、信号量等,它们的实现都可以抽象为状态依赖的“获取”和“释放”的操作。这也是AQS实现的理论基础。
  • JDK中大部分的并发工具类,都是基于AQS来实现的。使用AQS来构造工具类是使用复合而不是继承的方式,这样可以保护AQS的脆弱实现。

关于AQS底层原理的研究

AQS中对于并发状态操作最核心的原语,如线程挂起唤醒、原子操作等,都是通过LockSupport(最终由sun.misc.Unsafe中的native方法)提供。

Unsafe中提供对内存直接操作、序列化、并发原语、CAS操作、线程挂起与唤醒等,不建议在我们自己的代码中使用。

0 0