一、状态依赖性管理

  • 对于单线程程序,某个条件为假,那么这个条件将永远无法成真
  • 在并发程序中,基于状态的条件可能会由于其他线程的操作而改变
     可阻塞的状态依赖操作的结构
    
     acquire lock on object state
    while (precondition does not hold)
    {
    release lock
    wait until precondition might hold
    optionally fail if interrupted or timeout expires
    reacquire lock
    }
    perform action
    release lock
 
 1 //有界缓存实现的基类
2 public abstract class BaseBoundedBuffer<V> {
3 private final V[] buf;
4 private int tail;
5 private int head;
6 private int count;
7
8 protected BaseBoundedBuffer(int capacity){
9 this.buf = (V[]) new Object[capacity];
10 }
11
12 protected synchronized final void doPut(V v){
13 buf[tail] = v;
14 if (++tail == buf.length){
15 tail = 0;
16 }
17 ++count;
18 }
19
20 protected synchronized final V doTake(){
21 V v = buf[head];
22 buf[head] = null; //let gc collect
23 if (++head == buf.length){
24 head = 0;
25 }
26 --count;
27 return v;
28 }
29
30 public synchronized final boolean isFull(){
31 return count == buf.length;
32 }
33
34 public synchronized final boolean isEmpty(){
35 return count == 0;
36 }
37 }

1、示例:将前提条件的失败传递给调用者

 public class GrumyBoundedBuffer<V> extends BaseBoundedBuffer<V> {
public GrumyBoundedBuffer(int size){
super(size);
} public synchronized void put(V v){
if (isFull()){
throw new BufferFullException();
}
doPut(v);
} public synchronized V take(){
if (isEmpty())
throw new BufferEmptyExeption();
return doTake();
}
} 当不满足前提条件时,有界缓存不会执行相应的操作

缺点:已满情况不应为异常;调用者自行处理失败;sleep:降低响应性;自旋等待:浪费CPU;yield让出CPU

2、示例:通过轮询与休眠来实现简单的阻塞

 public class SleepyBounedBuffer<V> extends BaseBoundedBuffer<V> {
private static long SLEEP_TIME;
public SleepyBounedBuffer(int size) {
super(size);
} public void put(V v) throws InterruptedException{
while (true){
synchronized(this){
if (!isFull()){
doPut(v);
return;
}
}
Thread.sleep(SLEEP_TIME);
}
} public V take() throws InterruptedException{
while (true){
synchronized(this){
if (!isEmpty()){
return doTake();
}
}
Thread.sleep(SLEEP_TIME);
}
}
} “轮询与休眠“重试机制

优点:对于调用者,无需处理失败与异常,操作可阻塞,可中断(休眠时候不要持有锁)

缺点:对于休眠时间设置的权衡(响应性与CPU资源)

3、条件队列——使得一组线程(称之为等待线程集合)能够通过某种方式来等待特定的条件变成真(元素是一个个正在等待相关条件的线程)

  • 每个对象都可以作为一个条件队列(API:wait、notify和notifyAll)

    • Object.wait会自动释放锁,并请求操作系统挂起当前线程,从而使其他线程能够获得这个锁并且修改对象的状态
    • Object.notify/notifyAll通知被挂起的线程可以重新请求资源执行
  • 只有能对状态进行检查时,才能在某个条件上等待,并且只有能修改状态时,才能从条件等待中释放另一个线程
  • 条件队列在CPU效率、上下文切换开销和响应性等进行了优化
  • 如果某个功能无法通过“轮询和休眠”来实现,那么使用条件队列也无法实现
 1 public class BoundedBuffer<V> extends BaseBoundedBuffer<V> {
2
3 public BoundedBuffer(int capacity) {
4 super(capacity);
5 }
6
7 public synchronized void put(V v) throws InterruptedException{
8 while (isFull()){
9 wait();
10 }
11 doPut(v);
12 notifyAll();
13 }
14
15 public synchronized V take() throws InterruptedException{
16 while (isEmpty()){
17 wait();
18 }
19 V v = doTake();
20 notifyAll();
21 return v;
22 }
23 }

二、使用条件队列

1、条件谓词

  • 条件等待中存在一种重要的三元关系,包括加锁、wait方法和一个条件谓词
  • 条件谓词是由类中各个状态变量构成的表达式(while)
  • 在测试条件谓词之前必须先持有这个锁
  • 锁对象与条件队列对象(即调用wait和notify等方法所在的对象)必须是同一个对象
  • wait被唤醒后需要重新获得锁,并重新检查条件谓词

2、过早唤醒——一个条件队列与多个条件谓词相关时,wait方法返回不一定线程所等待的条件谓词就变为真了

1 void stateDependentMethod() throws InterruptedException
2 {
3 synchronized(lock) // 必须通过一个锁来保护条件谓词
4 {
5 while(!condietionPredicate())
6 lock.wait();
7 }
8 }

当使用条件等待时(如Object.wait(), 或Condition.await()):

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

3、丢失信号量——线程必须等待一个已经为真的条件,但在开始等待之前没有检查条件谓词

如果线程A通知了一个条件队列,而线程B随后在这个条件队列上等待,那么线程B将不会立即醒来,而是需要另一个通知来唤醒它(导致活跃性下降)

4、通知——确保在条件谓词变为真时通过某种方式发出通知挂起的线程

  • 发出通知的线程持有锁调用notify和notifyAll,发出通知后应尽快释放锁
  • 多个线程可以基于不同的条件谓词在同一个条件队列上等待,使用notify单一的通知很容易导致类似于信号丢失的问题
  • 可以使用notify:同一条件谓词并且单进单出

使用notifyAll有时是低效的:唤醒的所有线程都需要竞争锁,并重新检验,而有时最终只有一个线程能执行

优化:条件通知

1 public synchronized void put(V v) throws InterruptedException
2 {
3 while(isFull())
4 wait();
5 boolean wasEmpty = isEmpty();
6 doPut(v);
7 if(wasEmpty)
8 notifyAll();
9 }

5、示例:阀门类

 public class ThreadGate {
private boolean isOpen;
private int generation; public synchronized void close() {
isOpen = false;
} public synchronized void open() {
++generation;
isOpen = true;
notifyAll();
} public synchronized void await() throws InterruptedException {
int arrivalGeneration = generation;
while (!isOpen && arrivalGeneration == generation)
wait();
}
} 可重新关闭的阀门

arrivalGeneration == generation为了保证在阀门打开时又立即关闭时,在打开时通知的线程都可以通过阀门

6、子类的安全问题

  • 如果在实施子类化时违背了条件通知或单词通知的某个需求,那么在子类中可以增加合适的通知机制来代表基类
  • 对于状态依赖的类,要么将其等待和通知等协议完全向子类公开(并且写入正式文档),要么完全阻止子类参与到等待和通知等过程中
  • 完全禁止子类化

7、封装条件队列

8、入口协议和出口协议

  • 入口协议:该操作的条件谓词
  • 出口协议:检查被该操作修改的所有状态变量,并确认它们是否使某个其他的条件谓词变为真,如果是,则通知相关的条件队列

三、显示的Condition对象

内置条件队列的缺点:每个内置锁都只能有一个相关联的条件队列,而多个线程可能在同一条件队列上等待不同的条件谓词,调用notifyAll通知的线程非等待同意谓词

Condition <-> Lock,内置条件队列 <-> 内置锁

  • Lock.newCondition()
  • 在每个锁上可存在多个等待、条件等待可以是可中断的或不可中断的、基于时限的等待,以及公平的或非公平的队列操作
  • Condition对象继承了相关的Lock对象的公平性
  • 与wait、notify和notifyAll方法对应的分别是await、signal和signalAll
  • 将多个条件谓词分开并放到多个等待线程集,Condition使其更容易满足单次通知的需求(signal比signalAll更高效)
  • 锁、条件谓词和条件变量:件谓词中包含的变量必须由Lock来保护,并且在检查条件谓词以及调用await和signal时,必须持有Lock对象
 1 public class ConditionBoundedBuffer<T> {
2 protected final Lock lock = new ReentrantLock();
3 private final Condition notFull = lock.newCondition();//条件:count < items.length
4 private final Condition notEmpty = lock.newCondition();//条件:count > 0
5 private final T[] items = (T[]) new Object[100];
6 private int tail, head, count;
7
8 public void put(T x) throws InterruptedException {
9 lock.lock();
10 try {
11 while (count == items.length)
12 notFull.await();//等到条件count < items.length满足
13 items[tail] = x;
14 if (++tail == items.length)
15 tail = 0;
16 ++count;
17 notEmpty.signal();//通知读取等待线程
18 } finally {
19 lock.unlock();
20 }
21 }
22
23 public T take() throws InterruptedException {
24 lock.lock();
25 try {
26 while (count == 0)
27 notEmpty.await();//等到条件count > 0满足
28 T x = items[head];
29 items[head] = null;
30 if (++head == items.length)
31 head = 0;
32 --count;
33 notFull.signal();//通知写入等待线程
34 return x;
35 } finally {
36 lock.unlock();
37 }
38 }
39 }

四、Synchronizer解析

  在ReentrantLock和Semaphore这两个接口之间存在许多共同点。两个类都可以用作一个”阀门“,即每次只允许一定数量的线程通过,并当线程到达阀门时,可以通过(在调用lock或acquire时成功返回),也可以等待(在调用lock或acquire时阻塞),还可以取消(在调用tryLock或tryAcquire时返回”假“,表示在指定的时间内锁是不可用的或者无法获取许可)。而且,这两个接口都支持中断不可中断的以及限时的获取操作,并且也都支持等待线程执行公平或非公平的队列操作。

原因:都实现了同一个基类AbstractQueuedSynchronizer(AQS)

 public class SemaphoreOnLock {//基于Lock的Semaphore实现
private final Lock lock = new ReentrantLock();
//条件:permits > 0
private final Condition permitsAvailable = lock.newCondition();
private int permits;//许可数 SemaphoreOnLock(int initialPermits) {
lock.lock();
try {
permits = initialPermits;
} finally {
lock.unlock();
}
} //颁发许可,条件是:permits > 0
public void acquire() throws InterruptedException {
lock.lock();
try {
while (permits <= 0)//如果没有许可,则等待
permitsAvailable.await();
--permits;//用一个少一个
} finally {
lock.unlock();
}
} //归还许可
public void release() {
lock.lock();
try {
++permits;
permitsAvailable.signal();
} finally {
lock.unlock();
}
}
} 使用Lock实现信号量
 public class LockOnSemaphore {//基于Semaphore的Lock实现
//具有一个信号量的Semaphore就相当于Lock
private final Semaphore s = new Semaphore(1); //获取锁
public void lock() throws InterruptedException {
s.acquire();
} //释放锁
public void unLock() {
s.release();
}
} 使用信号量实现Lock

五、AbstractQueuedSynchronizer

最基本的操作:

  • 获取操作是一种依赖状态的操作,并且通常会阻塞(同步器判断当前状态是否允许获得操作,更新同步器的状态)
  • 释放并不是一个可阻塞的操作时,当执行“释放”操作时,所有在请求时被阻塞的线程都会开始执行

状态管理(一个整数状态):

  • 通过getState,setState以及compareAndSetState等protected类型方法来进行操作
  • 这个整数在不同子类表示任意状态。例:剩余的许可数量,任务状态
  • 子类可以添加额外状态

六、java.util.concurrent 同步器类中的AQS

1、ReentrantLock

  ReentrantLock只支持独占方式的获取操作,因此它实现了tryAcquire、tryRelease和isHeldExclusively

  ReentrantLock将同步状态用于保存锁获取操作的次数,或者正要释放锁的时候,才会修改这个变量

2、Semaphore与CountDownLatch

  Semaphore将AQS的同步状态用于保存当前可用许可的数量;CountDownLatch使用AQS的方式与Semaphore很相似,在同步状态中保存的是当前的计数值

3、FutureTask

  在FutureTask中,AQS同步状态被用来保存任务的状态

  FutureTask还维护一些额外的状态变量,用来保存计算结果或者抛出的异常

4、ReentrantReadWriteLock

  • 单个AQS子类将同时管理读取加锁和写入加锁
  • ReentrantReadWriteLock使用了一个16位的状态来表示写入锁的计数,并且使用了另一个16位的状态来表示读取锁的计数
  • 在读取锁上的操作将使用共享的获取方法与释放方法,在写入锁上的操作将使用独占的获取方法与释放方法
  • AQS在内部维护了一个等待线程队列,其中记录了某个线程请求的是独占访问还是共享访问:写操作独占获取;读操作可使第一个写之前的读都获取

java并发编程实战:第十四章----构建自定义的同步工具的更多相关文章

  1. 《java并发编程实战》读书笔记11--构建自定义的同步工具,条件队列,Condition,AQS

    第14章 构建自定义的同步工具 本章将介绍实现状态依赖性的各种选择,以及在使用平台提供的状态依赖机制时需要遵守的各项规则. 14.1 状态依赖性的管理 对于并发对象上依赖状态的方法,虽然有时候在前提条 ...

  2. 《Java并发编程实战》第十四章 构建自己的同步工具定义 札记

    一.状态依赖性的管理 有界缓存实现的基类 @ ThreadSafe public abstract class BaseBoundedBuffer<E> { @GuardeBy( &quo ...

  3. java并发编程实战:第四章----对象的组合

    一.设计线程安全的类 找出构造对象状态的所有变量(若变量为引用类型,还包括引用对象中的域) 约束状态变量的不变性条件 建立对象状态的并发访问管理策略(规定了如何维护线程安全性) 1.收集同步需求(找出 ...

  4. 《Java并发编程实战》第四章 对象的组合 读书笔记

    一.设计线程安全的类 在设计线程安全类的过程中,须要包括下面三个基本要素:  . 找出构成对象状态的全部变量.  . 找出约束状态变量的不变性条件.  . 建立对象状态的并发訪问管理策略. 分析对象的 ...

  5. 【Java并发编程实战】----- AQS(四):CLH同步队列

    在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形.其主要从两方面进行了改造:节点的结构与节点等待机制.在结构上引入了头 ...

  6. 【Java并发编程实战】—– AQS(四):CLH同步队列

    在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形. 其主要从双方面进行了改造:节点的结构与节点等待机制.在结构上引入了 ...

  7. 《Java并发编程实战》第三章 对象的共享 读书笔记

    一.可见性 什么是可见性? Java线程安全须要防止某个线程正在使用对象状态而还有一个线程在同一时候改动该状态,并且须要确保当一个线程改动了对象的状态后,其它线程能够看到发生的状态变化. 后者就是可见 ...

  8. java并发编程的艺术——第四章总结

    第四章并发编程基础 4.1线程简介 4.2启动与终止线程 4.3线程间通信 4.4线程应用实例 java语言是内置对多线程支持的. 为什么使用多线程: 首先线程是操作系统最小的调度单元,多核心.多个线 ...

  9. 《Java并发编程实战》第六章 任务运行 读书笔记

    一. 在线程中运行任务 无限制创建线程的不足 .线程生命周期的开销很高 .资源消耗 .稳定性 二.Executor框架 Executor基于生产者-消费者模式.提交任务的操作相当于生产者.运行任务的线 ...

随机推荐

  1. 在pydev安装完成后在eclipse不显示的问题

    Java配置: http://www.jb51.net/os/win10/370409.html http://blog.csdn.net/wwd0501/article/details/521308 ...

  2. 转转转--oracle 去重并按时间排序取第一条

    select t.* from (select a.*, row_number() over(partition by 需要分组的字段 order by 更新时间 desc) rw from 表 a) ...

  3. Python将数据写入excel或者txt,读入csv格式或xls文件,写入csv(写一行空一行解决办法)

    1.写入excel,一开始不需要自己新建一个excel,会自动生成 attribute_proba是我写入的对象 import xlwt myexcel = xlwt.Workbook() sheet ...

  4. Hive 安装操作

    本篇为安装篇较简单: 前提:1: 安装了hadoop-1.0.4(1.0.3也可以)正常运行2:安装了hbase-0.94.3, 正常运行 接下来,安装Hive,基于已经安装好的hadoop,步骤如下 ...

  5. selenium+python自动化79-文件下载(SendKeys)

    前言 文件下载时候会弹出一个下载选项框,这个弹框是定位不到的,有些元素注定定位不到也没关系,就当没有鼠标,我们可以通过键盘的快捷键完成操作. SendKeys库是专业的处理键盘事件的,所以这里需要用S ...

  6. JS执行删除前的判断

    JS执行删除前如何实现判断. 一. <script> function del(){ if(confirm("确认删除吗")){ alert("yes&quo ...

  7. leetcode119

    public class Solution { public IList<int> GetRow(int rowIndex) { List<List<int>> l ...

  8. 如何量化考核技术人的 KPI?

    如何量化考核技术人的 KPI? 原创: 张建飞 阿里技术今天 阿里妹导读:对技术人来说,技术是成长的"核心".然而,在实际工作协作中,技术的重要性常常被业务所掩盖,造成先业务后技术 ...

  9. sudo免密码

    很多都是修改/etc/sudoers权限为740再加上一句 ALL=NOPASSWD:ALL 或者加一句 yourname ALL=(ALL) NOPASSWD: ALL 然后权限改回440 先说第一 ...

  10. 迷你MVVM框架 avalonjs 学习教程2、模块化、ViewModel、作用域

    一个项目是由许多人分工写的,因此必须要合理地拆散,于是有了模块化.体现在工作上,PM通常它这为某某版块,某某频道,某某页面.某一个模块,必须是包含其固有的数据,样式,HTML与处理逻辑.在jQuery ...