Java编程思想 并发总结

并发的多面性
并发编程令人困惑的一个主要原因是 使用并发时需要解决的问题有多个 而实现并发的方式也有多种 并且在这两者之间没有明显的映射关系(而且通常只具有模糊的界线) 因此 你必须理解所有这些问题和特例 以便有效地使用并发
用并发解决的问题大体上可以分为 速度 和 设计可管理性 两种

更快的执行
速度问题初听起来很简单 如果你想要一个程序运行得更快 那么可以将其断开为多个片段 在单独的处理器上运行每个片段 并发是用于多处理器编程的基本工具 当前 Moore定律已经有些过时了(至少对于传统芯片是这样) 速度提高是以多核处理器的形式而不是更快的芯片的形式出现的 为了使程序运行得更快 你必须学习如何利用这些额外的处理器 而这正是并发赋予你的能力

改进代码设计
在单CPU机器上使用多任务的程序在任意时刻仍旧只在执行一项工作 因此从理论上讲 肯定可以不用任何任务而编写出相同的程序 但是 并发提供了一个重要的组织结构上的好处 你的程序设计可以极大地简化 某些类型的问题 例如仿真 没有并发的支持是很难解决的

基本的线程机制
并发编程使我们可以将程序划分为多个分离的 独立运行的任务 通过使用多线程机制 这些独立任务(也被称为子任务)中的每一个都将由执行线程来驱动 一个线程就是在进程中的一个单一的顺序控制流 因此 单个进程可以拥有多个并发执行的任务 但是你的程序使得每个任务都好像有其自己的CPU一样 其底层机制是切分CPU时间 但通常你不需要考虑它
线程模型为编程带来了便利 它简化了在单一程序中同时交织在一起的多个操作的处理 在使用线程时 CPU将轮流给每个任务分配其占用时间 每个任务都觉得自己在一直占用CPU 但事实上CPU时间是划分成片段分配给了所有的任务(例外情况是程序确实运行在多个CPU之上) 线程的一大好处是可以使你从这个层次抽身出来 即代码不必知道它是运行在具有一个还是多个CPU的机器上 所以 使用线程机制是一种建立透明的 可扩展的程序的方法 如果程序运行得太慢 为机器增添一个CPU就能很容易地加快程序的运行速度 多任务和多线程往往是使用多处理器系统的最合理方式

定义任务
线程可以驱动任务 因此你需要一种描述任务的方式 这可以由Runnable接口来提供 要想定义任务 只需实现Runnable接口并编写run()方法 使得该任务可以执行你的命令 例如 下面的LiftOff任务将显示发射之前的倒计时
Java编程思想 并发总结_第1张图片

在下面的实例中 这个任务的run()不是由单独的线程驱动的 它是在main()中直接调用的(实际上 这里仍旧使用了线程 即总是分配给main()的那个线程)
Java编程思想 并发总结_第2张图片
在这里插入图片描述
当从Runnable导出一个类时 它必须具有run()方法 但是这个方法并无特殊之处 它不会产生任何内在的线程能力 要实现线程行为 你必须显式地将一个任务附着到线程上

Thread类
将Runnable对象转变为工作任务的传统方式是把它提交给一个Thread构造器 下面的示例展示了如何使用Thread来驱动LiftOff对象
Java编程思想 并发总结_第3张图片

你可以很容易地添加更多的线程去驱动更多的任务 下面 你可以看到所有任务彼此之间是如何互相呼应的
Java编程思想 并发总结_第4张图片

使用Executor
Java SE5的java.util.concurrent包中的执行器(Executor)将为你管理Thread对象 从而简化了并发编程 Executor在客户端和任务执行之间提供了一个间接层 与客户端直接执行任务不同 这个中介对象将执行任务 Executor允许你管理异步任务的执行 而无须显式地管理线程的生命周期 Executor在Java SE5/6中是启动任务的优选方法
我们可以使用Executor来代替在MoreBasicThreads.java中显式地创建Thread对象 LiftOff对象知道如何运行具体的任务 与命令设计模式一样 它暴露了要执行的单一方法 ExecutorService(具有服务生命周期的Executor 例如关闭)知道如何构建恰当的上下文来执行Runnable对象 在下面的示例中 CachedThreadPool将为每个任务都创建一个线程 注意 ExecutorService对象是使用静态的Executor方法创建的 这个方法可以确定其Executor类型
Java编程思想 并发总结_第5张图片
在这里插入图片描述

你可以很容易地将前面示例中的CachedThreadPool替换为不同类型的Executor FixedThreadPool使用了有限的线程集来执行所提交的任务
Java编程思想 并发总结_第6张图片

SingleThreadExecutor就像是线程数量为1的FixedThreadPool 这对于你希望在另一个线程中连续运行的任何事物(长期存活的任务)来说 都是很有用的 例如监听进入的套接字连接的任务 它对于希望在线程中运行的短任务也同样很方便 例如 更新本地或远程日志的小任务 或者是事件分发线程
如果向SingleThreadExecutor提交了多个任务 那么这些任务将排队 每个任务都会在下一个任务开始之前运行结束 所有的任务将使用相同的线程 在下面的示例中 你可以看到每个任务都是按照它们被提交的顺序 并且是在下一个任务开始之前完成的 因此 SingleThreadExecutor会序列化所有提交给它的任务 并会维护它自己(隐藏)的悬挂任务队列
Java编程思想 并发总结_第7张图片

从任务中产生返回值
Runnable是执行工作的独立任务 但是它不返回任何值 如果你希望任务在完成时能够返回一个值 那么可以实现Callable接口而不是Runnable接口 在Java SE5中引入的Callable是一种具有类型参数的泛型 它的类型参数表示的是从方法call()(而不是run())中返回的值 并且必须使用ExecutorService.submit()方法调用它 下面是一个简单示例
Java编程思想 并发总结_第8张图片
Java编程思想 并发总结_第9张图片

休眠
影响任务行为的一种简单方法是调用sleep() 这将使任务中止执行给定的时间 在LiftOff类中 要是把对yield()的调用换成是调用sleep() 将得到如下结果
Java编程思想 并发总结_第10张图片
在这里插入图片描述

优先级
线程的优先级将该线程的重要性传递给了调度器 尽管CPU处理现有线程集的顺序是不确定的 但是调度器将倾向于让优先权最高的线程先执行 然而 这并不是意味着优先权较低的线程将得不到执行(也就是说 优先权不会导致死锁) 优先级较低的线程仅仅是执行的频率较低
在绝大多数时间里 所有线程都应该以默认的优先级运行 试图操纵线程优先级通常是一种错误
下面是一个演示优先级等级的示例 你可以用getPriority()来读取现有线程的优先级 并且在任何时刻都可以通过setPriority()来修改它
Java编程思想 并发总结_第11张图片
Java编程思想 并发总结_第12张图片

让步
如果知道已经完成了在run()方法的循环的一次迭代过程中所需的工作 就可以给线程调度机制一个暗示 你的工作已经做得差不多了 可以让别的线程使用CPU了 这个暗示将通过调用yield()方法来作出(不过这只是一个暗示 没有任何机制保证它将会被采纳) 当调用yield()时 你也是在建议具有相同优先级的其他线程可以运行

后台线程
所谓后台(daemon)线程 是指在程序运行的时候在后台提供一种通用服务的线程 并且这种线程并不属于程序中不可或缺的部分 因此 当所有的非后台线程结束时 程序也就终止了 同时会杀死进程中的所有后台线程 反过来说 只要有任何非后台线程还在运行 程序就不会终止 比如 执行main()的就是一个非后台线程
Java编程思想 并发总结_第13张图片

SimpleDaemons.java创建了显式的线程 以便可以设置它们的后台标志 通过编写定制的ThreadFactory可以定制由Executor创建的线程的属性(后台 优先级 名称)
Java编程思想 并发总结_第14张图片
这与普通的ThreadFactory的唯一差异就是它将后台状态全部设置为了true 你现在可以用一个新的DaemonThreadFactory作为参数传递给Executor.newCachedThreadPool()
Java编程思想 并发总结_第15张图片
每个静态的ExecutorService创建方法都被重载为接受一个ThreadFactory对象 而这个对象将被用来创建新的线程
Java编程思想 并发总结_第16张图片
可以通过调用isDaemon()方法来确定线程是否是一个后台线程 如果是一个后台线程 那么它创建的任何线程将被自动设置成后台线程 如下例所示
Java编程思想 并发总结_第17张图片
Java编程思想 并发总结_第18张图片

后台进程在不执行finally子句的情况下就会终止其run()方法
Java编程思想 并发总结_第19张图片
Java编程思想 并发总结_第20张图片

编码的变体
到目前为止 在你所看到的示例中 任务类都实现了Runnable 在非常简单的情况下 你可能会希望使用直接从Thread继承这种可替换的方式 就像下面这样
Java编程思想 并发总结_第21张图片
在这里插入图片描述

另一种可能会看到的惯用法是自管理的Runnable
Java编程思想 并发总结_第22张图片

有时通过使用内部类来将线程代码隐藏在类中将会很有用 就像下面这样
Java编程思想 并发总结_第23张图片
Java编程思想 并发总结_第24张图片
Java编程思想 并发总结_第25张图片
Java编程思想 并发总结_第26张图片
Java编程思想 并发总结_第27张图片
Java编程思想 并发总结_第28张图片
在这里插入图片描述

术语
在Java中Thread类自身不执行任何操作 它只是驱动赋予它的任务 但是线程研究中总是不变地使用 线程执行这项或那项动作 这样的语言 因此 你得到的印象就是 线程就是任务 当我第一次碰到Java线程时 这种印象非常强烈 以至于我看到了一种明显的 是一个 关系 这就像是在说 很明显我应该从Thread继承出一个任务 另外 Runnable接口的名字选择很糟糕 所以我认为Task应该是好得多名字 如果接口只是其方法的泛型封装 那么 它执行能做的事情 这种命名方式将是恰当的 但是如果它是要表示更高层的抽象 例如Task 那么概念名将有用

加入一个线程
一个线程可以在其他线程之上调用join()方法 其效果是等待一段时间直到第二个线程结束才继续执行 如果某个线程在另一个线程t上调用t.join() 此线程将被挂起 直到目标线程t结束才恢复(即t.isAlive()返回为假)
也可以在调用join()时带上一个超时参数(单位可以是毫秒 或者毫秒和纳秒) 这样如果目标线程在这段时间到期时还没有结束的话 join()方法总能返回
对join()方法的调用可以被中断 做法是在调用线程上调用interrupt()方法 这时需要用到try-catch子句
下面这个例子演示了所有这些操作
Java编程思想 并发总结_第29张图片
Java编程思想 并发总结_第30张图片

创建有响应的用户界面
如前所述 使用线程的动机之一就是建立有响应的用户界面 下面给出了一个基于控制台用户界面的简单示例 下面的例子有两个版本 一个关注于运算 所以不能读取控制台输入 另一个把运算放在任务里单独运行 此时就可以在进行运算的同时监听控制台输入
Java编程思想 并发总结_第31张图片

线程组
线程组持有一个线程集合
最好把线程组看成是一次不成功的尝试 你只要忽略它就好了

捕获异常
由于线程的本质特性 使得你不能捕获从线程中逃逸的异常 一旦异常逃出任务的run()方法 它就会向外传播到控制台 除非你采取特殊的步骤捕获这种错误的异常 在Java SE5之前 你可以使用线程组来捕获这些异常 但是有了Java SE5 就可以用Executor来解决这个问题 因此你就不再需要了解有关线程组的任何知识了
下面的任务总是会抛出一个异常 该异常会传播到其run()方法的外部 并且main()展示了当你运行它时所发生的事情
Java编程思想 并发总结_第32张图片
输出如下(将某些限定符修整为适合显式)
在这里插入图片描述
将main的主题放到try-catch语句块中是没有作用的
Java编程思想 并发总结_第33张图片
Java编程思想 并发总结_第34张图片
这将产生与前面示例相同的结果 未捕获的异常
为了解决这个问题 我们要修改Executor产生线程的方式 Thread.UncaughtExceptionHandler是Java SE5中的新接口 它允许你在每个Thread对象上都附着一个异常处理器 Thread.UncaughtExceptionHandler.uncaughtException()会在线程因未捕获的异常而临近死亡时被调用 为了使用它 我们创建了一个新类型的ThreadFactory 它将在每个新创建的Thread对象上附着一个Thread.UncaughtExceptionHandler 我们将这个工厂传递给Executors创建新的ExecutorService的方法
Java编程思想 并发总结_第35张图片

上面的示例使得你可以按照具体情况逐个地设置处理器 如果你知道将要在代码中处处使用相同的异常处理器 那么更简单的方式是在Thread类中设置一个静态域 并将这个处理器设置为默认的未捕获异常处理器
Java编程思想 并发总结_第36张图片

共享受限资源

不正确地访问资源
考虑下面的例子 其中一个任务产生偶数 而其他任务消费这些数字 这里 消费者任务的唯一工作就是检查偶数的有效性 首先 我们定义EvenChecker 即消费者任务 因为它将在随后所有的示例中被复用 为了将EvenChecker与我们要试验的各种类型的生成器解耦 我们将创建一个名为IntGenerator的抽象类 它包含EvenChecker必须了解的必不可少的方法 即一个next()方法 和一个可以执行撤销的方法 这个类没有实现Generator接口 因为它必须产生一个int 而泛型不支持基本类型的参数
Java编程思想 并发总结_第37张图片

任务IntGenerator都可以用下面的EvenChecker类来测试
Java编程思想 并发总结_第38张图片

我们看到的第一个IntGenerator有一个可以产生一系列偶数值的next()方法
在这里插入图片描述
Java编程思想 并发总结_第39张图片

解决共享资源竞争
基本上所有的并发模式在解决线程冲突问题的时候 都是采用序列化访问共享资源的方案 这意味着在给定时刻只允许一个任务访问共享资源 通常这是通过在代码前面加上一条锁语句来实现的 这就使得在一段时间内只有一个任务可以运行这段代码 因为锁语句产生了一种互相排斥的效果 所以这种机制常常称为互斥量(mutex)

Java以提供关键字synchronized的形式 为防止资源冲突提供了内置支持 当任务要执行被synchronized关键字保护的代码片段的时候 它将检查锁是否可用 然后获取锁 执行代码 释放锁

在生成偶数的代码中 你已经看到了 你应该将类的数据成员都声明为private的 而且只能通过方法来访问这些数据 所以可以把方法标记为synchronized来防止资源冲突 下面是声明synchronized方法的方式
在这里插入图片描述

针对每个类 也有一个锁(作为类的Class对象的一部分) 所以synchronized static方法可以在类的范围内防止对static数据的并发访问

如果你正在写一个变量 它可能接下来将被另一个线程读取 或者正在读取一个上一次已经被另一个线程写过的变量 那么你必须使用同步 并且 读写线程都必须用相同的监视器锁同步

同步控制EvenGenerator
通过在EvenGenerator.java中加入synchronized关键字 可以防止不希望的线程访问
Java编程思想 并发总结_第40张图片

使用显示的Lock对象
Java SE5的java.util.concurrent类库还包含有定义在java.util.concurrent.locks中的显式的互斥机制 Lock对象必须被显式地创建 锁定和释放 因此 它与内建的锁形式相比 代码缺乏优雅性 但是 对于解决某些类型的问题来说 它更加灵活 下面用显式的Lock重写的是SyschronizedEventGenerator.java
Java编程思想 并发总结_第41张图片

大体上 当你使用synchronized关键字时 需要写的代码量更少 并且用户错误出现的可能性也会降低 因此通常只有在解决特殊问题时 才使用显式的Lock对象 例如 用synchronized关键字不能尝试着获取锁且最终获取锁会失败 或者尝试着获取锁一段时间 然后放弃它 要实现这些 你必须使用concurrent类库
Java编程思想 并发总结_第42张图片
Java编程思想 并发总结_第43张图片

显式的Lock对象在加锁和释放锁方面 相对于内建的synchronized锁来说 还赋予了你更细粒度的控制力 这对于实现专有同步结构是很有用的 例如用于遍历链接列表中的节点的节节传递的加锁机制(也称为锁耦合) 这种遍历代码必须在释放当前节点的锁之前捕获下一个节点的锁

原子性与易变性

原子性可以应用于除long和double之外的所有基本类型之上的 简单操作 对于读取和写入除long和double之外的基本类型变量这样的操作 可以保证它们会被当作不可分(原子)的操作来操作内存 但是JVM可以将64位(long和double变量)的读取和写入当作两个分离的32位操作来执行 这就产生了在一个读取和写入操作中间发生上下文切换 从而导致不同的任务可以看到不正确结果的可能性(这有时被称为字撕裂 因为你可能会看到部分被修改过的数值) 但是 当你定义long或double变量时 如果使用volatile关键字 就会获得(简单的赋值与返回操作的)原子性(注意 在Java SE5之前 volatile一直未能正确地工作) 不同的JVM可以任意地提供更强的保证 但是你不应该依赖于平台相关的特性

使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域 你的第一选择应该是使用synchronized关键字 这是最安全的方式 而尝试其他任何方式都是有风险的
什么才属于原子操作呢 对域中的值做赋值和返回操作通常都是原子性的 但是 在C++中甚至下面的操作都可能是原子性的
在这里插入图片描述

在Java中 上面的操作肯定不是原子性的 正如从下面的方法所产生的JVM指令中可以看到的那样
Java编程思想 并发总结_第44张图片
Java编程思想 并发总结_第45张图片

如果你盲目地应用原子性概念 那么就会看到在下面程序中的getValue()符合上面的描述
Java编程思想 并发总结_第46张图片

正如第二个示例 考虑一些更简单的事情 一个产生序列数字的类 每当nextSerialNumber()被调用时 它必须向调用者返回唯一的值
Java编程思想 并发总结_第47张图片

为了测试SerialNumberGenerator 我们需要不会耗尽内存的集(Set) 以防需要花费很长的时间来探测问题 这里所示的CircularSet重用了存储int数值的内存 并假设在你生成序列数时 产生数值覆盖冲突的可能性极小 add()和contains()方法都是synchronized 以防止线程冲突
Java编程思想 并发总结_第48张图片
Java编程思想 并发总结_第49张图片

原子类
Java SE5引入了诸如AtomicInteger AtomicLong AtomicReference等特殊的原子性变量类 它们提供下面形式的原子性条件更新操作
在这里插入图片描述
这些类被调整为可以使用在某些现代处理器上的可获得的 并且是在机器级别上的原子性 因此在使用它们时 通常不需要担心 对于常规编程来说 它们很少会派上用场 但是在涉及性能调优时 它们就大有用武之地了 例如 我们可以使用AtomicInteger来重写AtomicityTest.java
Java编程思想 并发总结_第50张图片
Java编程思想 并发总结_第51张图片

下面是用AtomicInteger重写的MutexEvenGenerator.java
Java编程思想 并发总结_第52张图片

临界区
有时 你只是希望防止多个线程同时访问方法内部的部分代码而不是防止访问整个方法 通过这种方式分离出来的代码段被称为临界区(critical section) 它也使用synchronized关键字建立 这里 synchronized被用来指定某个对象 此对象的锁被用来对花括号内的代码进行同步控制
在这里插入图片描述
这也被称为同步控制块 在进入此段代码前 必须得到syncObject对象的锁 如果其他线程已经得到这个锁 那么就得等到锁被释放以后 才能进入临界区
通过使用同步控制块 而不是对整个方法进行同步控制 可以使多个任务访问对象的时间性能得到显著提高 下面的例子比较了这两种同步控制方法 此外 它也演示了如何把一个非保护类型的类 在其它类的保护和控制之下 应用于多线程的环境
Java编程思想 并发总结_第53张图片
Java编程思想 并发总结_第54张图片
Java编程思想 并发总结_第55张图片
在这里插入图片描述

你还可以使用显式的Lock对象来创建临界区
Java编程思想 并发总结_第56张图片
Java编程思想 并发总结_第57张图片

在其他对象上同步
有时必须在另一个对象上同步 但是如果你要这么做 就必须确保所有相关的任务都是在同一个对象上同步的 下面的示例演示了两个任务可以同时进入同一个对象 只要这个对象上的方法是在不同的锁上同步的即可
Java编程思想 并发总结_第58张图片
Java编程思想 并发总结_第59张图片

线程本地存储
防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享 线程本地存储是一种自动化机制 可以为使用相同变量的每个不同的线程都创建不同的存储 因此 如果你有5个线程都要使用变量x所表示的对象 那线程本地存储就会生成5个用于x的不同的存储块 主要是 它们使得你可以将状态与线程关联起来
创建和管理线程本地存储可以由java.lang.ThreadLocal类来实现 如下所示
Java编程思想 并发总结_第60张图片
Java编程思想 并发总结_第61张图片

终结任务
在前面的某些示例中 cancle()和isCanceled()方法被放到了一个所有任务都可以看到的类中 这些任务通过检查isCanceled()来确定何时终止它们自己 对于这个问题来说 这是一种合理的方式 但是 在某些情况下 任务必须更加突然地终止
首先 让我们观察一个示例 它不仅演示了终止问题 而且还是一个资源共享的示例

装饰性花园
在这个仿真程序中 花园委员会希望了解每天通过多个大门进入公园的总人数 每个大门都有一个十字转门或某种其他形式的计数器 并且任何一个十字转门的计数值递增时 就表示公园中的总人数的共享计数值也会递增
Java编程思想 并发总结_第62张图片
Java编程思想 并发总结_第63张图片
Java编程思想 并发总结_第64张图片

在阻塞时终结
前面示例中的Entrance.run()在其循环中包含对sleep()的调用 我们知道 sleep()最终将唤醒 而任务也将返回循环的开始部分 去检查canceled标志 从而决定是否跳出循环 但是 sleep()一种情况 它使任务从执行状态变为被阻塞状态 而有时你必须终止被阻塞的任务

线程状态
一个线程可以处于以下四种状态之一

  1. 新建(new)
  2. 就绪(Runnable)
  3. 阻塞(Blocked)
  4. 死亡(Dead)

进入阻塞状态

一个任务进入阻塞状态 可能有如下原因

  1. 通过调用sleep(milliseconds)使任务进入休眠状态 在这种情况下 任务在指定的时间内不会运行
  2. 你通过调用wait()使线程挂起 直到线程得到了notify()或notifyAll()消息(或者在Java SE5的java.util.concurrent类库中等价的signal()或signalAll()消息) 线程才会进入就绪状态
  3. 任务在等待某个输入/输出完成
  4. 任务试图在某个对象上调用其同步控制方法 但是对象锁不可用 因为另一个任务已经获取了这个锁

中断
Thread类包含interrupt()方法 因此你可以终止被阻塞的任务 这个方法将设置线程的中断状态 如果一个线程已经被阻塞 或者试图执行一个阻塞操作 那么设置这个线程的中断状态将抛出InterruptedException 当抛出该异常或者该任务调用Thread.interrupted()时 中断状态将被复位 正如你将看到的 Thread.interrupted()提供了离开run()循环而不抛出异常的第二种方式

下面的示例用Executor展示了基本的interrupt()用法
Java编程思想 并发总结_第65张图片
Java编程思想 并发总结_第66张图片
Java编程思想 并发总结_第67张图片
在这里插入图片描述

从输出中可以看到 你能够中断对sleep()的调用(或者任务要求抛出InterruptedException的调用) 但是 你不能中断正在试图获取synchronized锁或者试图执行I/O操作的线程 这有点令人烦恼 特别是在创建执行I/O的任务时 因为这意味着I/O具有锁住你的多线程程序的潜在可能 特别是对于基于Web的程序 这更是关乎利害
对于这类问题 有一个略显笨拙但是有时确实行之有效的解决方案 即关闭任务在其上发生阻塞的底层资源
Java编程思想 并发总结_第68张图片
在这里插入图片描述
在shutdownNow()被调用之后以及在两个输入流上调用close()之前的延迟强调的是一旦底层资源被关闭 任务将解除阻塞 请注意 有一点很有趣 interrupt()看起来发生在关闭Socket而不是关闭System.in的时刻
幸运的是 各种nio类提供了更人性化的I/O中断 被阻塞的nio通道会自动地响应中断
Java编程思想 并发总结_第69张图片

被互斥所阻塞
就像在Interrupting.java中看到的 如果你尝试着在一个对象上调用其synchronized方法 而这个对象的锁已经被其他任务获得 那么调用任务将被挂起(阻塞) 直至这个锁可获得 下面的示例说明了同一个互斥可以如何能被同一个任务多次获得
Java编程思想 并发总结_第70张图片

就像在前面在不可中断的I/O中所观察到的那样 无论在任何时刻 只要任务以不可中断的方式被阻塞 那么都有潜在的会锁住程序的可能 Java SE5并发类库中添加了一个特性 即在ReentrantLock上阻塞的任务具备可以被中断的能力 这与在synchronized方法或临界区上阻塞的任务完全不同
Java编程思想 并发总结_第71张图片

检查中断
注意 当你在线程上调用interrupt()时 中断发生的唯一时刻是在任务要进入到阻塞操作中 或者已经在阻塞操作内部时(如你所见 除了不可中断的I/O或被阻塞的synchronized方法之外 在其余的例外情况下 你无可事事) 但是如果根据程序运行的环境 你已经编写了可能会产生这种阻塞调用的代码 那又该怎么办呢 如果你只能通过在阻塞调用上抛出异常来退出 那么你就无法总是可以离开run()循环 因此 如果你调用interrupt()以停止某个任务 那么在run()循环碰巧没有产生任何阻塞调用的情况下 你的任务将需要第二种方式来退出
这种机会是由中断状态来表示的 其状态可以通过调用interrupt()来设置 你可以通过调用interrupted()来检查中断状态 这不仅可以告诉你interrupt()是否被调用过 而且还可以清除中断状态 清除中断状态可以确保并发结构不会就某个任务被中断这个问题通知你两次 你可以经由单一的InterruptedException或单一的成功的Thread.interrupted()测试来得到这种通知 如果想要再次检查以了解是否被中断 则可以在调用Thread.interrupted()时将结果存储起来
下面的示例展示了典型的惯用法 你应该在run()方法中使用它来处理在中断状态被设置时 被阻塞和不被阻塞的各种可能
Java编程思想 并发总结_第72张图片
Java编程思想 并发总结_第73张图片

线程之间的协作

wait()与notifyAll()
wait()使你可以等待某个条件发生变化 而改变这个条件超出了当前方法的控制能力 通常 这种条件将由另一个任务来改变 你肯定不想在你的任务测试这个条件的同时 不断地进行空循环 这被称为忙等待 通常是一种不良的CPU周期使用方式 因此wait()会在等待外部世界产生变量的时候将任务挂起 并且只有在notify()或notifyAll()发生时 即表示发生了某些感兴趣的事物 这个任务才会被唤醒并去检查所产生的变化 因此 wait()提供了一种在任务之间对活动同步的方式

有两种形式的wait() 第一种版本接收毫秒数作为参数 含义与sleep()方法里参数的意思相同 都是指 在此期间暂停 但是与sleep()不同的是 对于wait()而言

  1. 在wait()期间对象锁是释放的
  2. 可以通过notify() notifyAll() 或者令时间到期 从wait()中恢复执行
    第二种 也是更常用形式的wait()不接受任何参数 这种wait()将无限等待下去 直到线程接收到notify()或者notifyAll()消息

可以让另一个对象执行某种操作以维护其自己的锁 要这么做的话 必须首先得到对象的锁 比如 如果要向对象x发送notifyAll() 那么就必须在能够取得x的锁的同步控制块中这么做
在这里插入图片描述
让我们看一个简单的示例 WaxOMatic.java有两个过程 一个是将蜡涂到Car上 一个是抛光它 抛光任务在涂蜡任务完成之前 是不能执行其工作的 而涂蜡任务在涂另一层蜡之前 必须等待抛光任务完成 WaxOn和WaxOff都使用了Car对象 该对象在这些任务等待条件变化的时候 使用wait()和notifyAll()来挂起和重新启动这些任务
Java编程思想 并发总结_第74张图片
Java编程思想 并发总结_第75张图片
Java编程思想 并发总结_第76张图片

前面的示例强调你必须用一个检查感兴趣的条件的while循环包围wait() 这很重要 因为

  • 你可能有多个任务出于相同的原因在等待同一个锁 而第一个唤醒任务可能会改变这种状况(即使你没有这么做 有人也会通过继承你的类去这么做) 如果属于这种情况 那么这个任务应该被再次挂起 直至其感兴趣的条件发生变化
  • 在这个任务从其wait()中被唤醒的时刻 有可能会有某个其他的任务已经做出了改变 从而使得这个任务在此时不能执行 或者执行其操作已显得无关紧要 此时 应该通过再次调用wait()来将其重新挂起
  • 也有可能某些任务出于不同的原因在等待你的对象上的锁(在这种情况下必须使用notifyAll()) 在这种情况下 你需要检查是否已经由正确的原因唤醒 如果不是 就再次调用wait()

错失的信号
当两个线程使用notify()/wait()或notifyAll()/wait()进行协作时 有可能会错过某个信号 假设T1是通知T2的线程 而这两个线程都是使用下面(有缺陷的)方式实现的
Java编程思想 并发总结_第77张图片
是防止T2调用wait()的一个动作 当然前提是T2还没有调用wait()

该问题的解决方案是防止在someCondition变量上产生竞争条件 下面是T2正确的执行方式
在这里插入图片描述

notify()与notifyAll()
因为在技术上 可能会有多个任务在单个Car对象上处于wait()状态 因此调用notifyAll()比只调用notify()要更安全 但是 上面程序的结构只会有一个任务实际处于wait()状态 因此你可以使用notify()来代替notifyAll()

在有关Java的线程机制的讨论中 有一个令人困惑的描述 notifyAll()将唤醒 所有正在等待的任务 这是否意味着在程序中任何地方 任何处于wait()状态中的任务都将被任何对notifyAll()的调用唤醒呢 在下面的示例中 与Task2相关的代码说明了情况并非如此 事实上 当notifyAll()因某个特定锁而被调用时 只有等待这个锁的任务才会被唤醒
Java编程思想 并发总结_第78张图片
Java编程思想 并发总结_第79张图片
Java编程思想 并发总结_第80张图片

生产者与消费者
请考虑这样一个饭店 它有一个厨师和一个服务员 这个服务员必须等待厨师准备好膳食 当厨师准备好时 他会通知服务员 之后服务员上菜 然后返回继续等待 这是一个任务协作的示例 厨师代表生产者 而服务员代表消费者 两个任务必须在膳食被生产和消费时进行握手 而系统必须以有序的方式关闭 下面是对这个叙述建模的代码
Java编程思想 并发总结_第81张图片
Java编程思想 并发总结_第82张图片
Java编程思想 并发总结_第83张图片

注意 wait()被包装在一个while()语句中 这个语句在不断地测试正在等待的事物 乍看上去这有点怪 如果在等待一个订单 一旦你被唤醒 这个订单就必定是可获得的 对吗 正如前面注意到的 问题是在并发应用中 某个其他的任务可能会在WaitPerson被唤醒时 会突然插足并拿走订单 唯一安全的方式是使用下面这种wait()的惯用法(当然要在恰当的同步内部 并采用防止错失信号可能性的程序设计)
在这里插入图片描述

使用显示的Lock和Condition对象
在Java SE5的java.util.concurrent类库中还有额外的显式工具可以用来重写WaxOMatic.java 使用互斥并允许任务挂起的基本类是Condition 你可以通过在Condition上调用await()来挂起一个任务 当外部条件发生变化 意味着某个任务应该继续执行时 你可以通过调用signal()来通知这个任务 从而唤醒一个任务 或者调用signalAll()来唤醒所有在这个Condition上被其自身挂起的任务(与使用notifyAll()相比 signalAll()是更安全的方式)
下面是WaxOMatic.java的重写版本 它包含一个Condition 用来在waitForWaxing()或waitForBuffering()内部挂起一个任务
Java编程思想 并发总结_第84张图片
Java编程思想 并发总结_第85张图片
Java编程思想 并发总结_第86张图片

生产者-消费者与队列
wait()和notifyAll()方法以一种非常低级的方式解决了任务互操作问题 即每次交互时都握手 在许多情况下 你可以瞄向更高的抽象级别 使用同步队列来解决任务协作问题 同步队列在任何时刻都只允许一个任务插入或移除元素 在java.util.concurrent.BlockingQueue接口中提供了这个队列 这个接口有大量的标准实现 你通常可以使用LinkedBlockingQueue 它是一个无界队列 还可以使用ArrayBlockingQueue 它具有固定的尺寸 因此你可以在它被阻塞之前 向其中放置有限数量的元素

下面是一个简单的测试 它将多个LiftOff对象的执行串行化了 消费者是LiftOffRunner 它将每个LiftOff对象从BlockingQueue中推出并直接运行(即 它通过显式地调用run()而使用自己的线程来运行 而不是为每个任务启动一个新线程)
Java编程思想 并发总结_第87张图片
Java编程思想 并发总结_第88张图片
在这里插入图片描述

吐司BlockingQueue
考虑下面这个使用BlockingQueue的示例 有一台机器具有三个任务 一个制作吐司 一个给吐司抹黄油 另一个在抹过黄油的吐司上涂果酱 我们可以通过各个处理过程之间的BlockingQueue来运行这个吐司制作程序
Java编程思想 并发总结_第89张图片
Java编程思想 并发总结_第90张图片
Java编程思想 并发总结_第91张图片
Java编程思想 并发总结_第92张图片
Java编程思想 并发总结_第93张图片

任务间使用管道进行输入/输出
通过输入/输出在线程间进行通信通常很有用 提供线程功能的类库以 管道 的形式对线程间的输入/输出提供了支持 它们在Java输入/输出类库中的对应物就是PipedWriter类(允许任务向管道写)和PipedReader类(允许不同任务从同一个管道中读取) 这个模型可以看成是 生产者-消费者 问题的变体 这里的管道就是一个封装好的解决方案 管道基本上是一个阻塞队列 存在于多个引入BlockingQueue之前的Java版本中
下面是一个简单例子 两个任务使用一个管道进行通信
Java编程思想 并发总结_第94张图片

死锁
一个对象可以有synchronized方法或其他形式的加锁机制来防止别的任务在互斥还没有释放的时候就访问这个对象 你已经学习过 任务可以变成阻塞状态 所以就可能出现这种情况 某个任务在等待另一个任务 而后者又等待别的任务 这样一直下去 直到这个链条上的任务又在等待第一个任务释放锁 这得到了一个任务之间相互等待的连续循环 没有哪个线程能继续 这被称之为死锁

由Edsger Dijkstra提出的哲学家就餐问题是一个经典的死锁例证 该问题的基本描述中是指定五个哲学家(不过这里的例子中将允许任意数目) 这些哲学家将花部分时间思考 花部分时间就餐 当他们思考的时候 不需要任何共享资源 但当他们就餐时 将使用有限数量的餐具 在问题的原始描述中 餐具是叉子 要吃到桌子中央盘子里的意大利面条需要用两把叉子 不过把餐具看成是筷子更合理 很明显 哲学家要就餐就需要两根筷子
问题中引入的难点是 作为哲学家 他们很穷 所以他们只能买五根筷子(更一般地讲 筷子和哲学家的数量相同) 他们围坐在桌子周围 每人之间放一根筷子 当一个哲学家要就餐的时候 这个哲学家必须同时得到左边和右边的筷子 如果一个哲学家左边或右边已经有人在使用筷子了 那么这个哲学家就必须等待 直至可得到必需的筷子
Java编程思想 并发总结_第95张图片

当一个Philosopher任务调用take()时 这个Philosopher将等待 直至taken标志变为false(直至当前持有Chopstick的Philosopher释放它) 然后这个任务会将taken标志设置为true 以表示现在由新的Philosopher持有这根Chopstick 当这个Philosopher使用完这根Chopstick时 它会调用drop()来修改标志的状态 并notifyAll()所有其他的Philosopher 这些Philosopher中有些可能就在wait()这根Chopstick
Java编程思想 并发总结_第96张图片
Java编程思想 并发总结_第97张图片

现在我们可以建立这个程序的将会产生死锁的版本了
Java编程思想 并发总结_第98张图片

要修正死锁问题 你必须明白 当以下四个条件同时满足时 就会发生死锁

  1. 互斥条件 任务使用的资源中至少有一个是不能共享的 这里 一根Chopstick一次就只能被一个Philosopher使用
  2. 至少有一个任务它必须持有一个资源且正在等待获取一个当前被别的任务持有的资源 也就是说 要发生死锁 Philosopher必须拿着一根Chopstick并且等待另一根
  3. 资源不能被任务抢占 任务必须把资源释放当作普通事件 Philosopher很有礼貌 他们不会从其他Philosopher那里抢Chopstick
  4. 必须有循环等待 这时 一个任务等待其他任务所持有的资源 后者又在等待另一个任务所持有的资源 这样一直下去 直到有一个任务在等待第一个任务所持有的资源 使得大家都被锁住 在DeadlockingDiningPhilosophers.java中 因为每个Philosopher都试图先得到右边的Chopstick 然后得到左边的Chopstick 所以发生了循环等待
    因为要发生死锁的话 所有这些条件必须全部满足 所以要防止死锁的话 只需破坏其中一个即可 在程序中 防止死锁最容易的方法是破坏第4个条件 有这个条件的原因是每个Philosopher都试图用特定的顺序拿Chopstick 先右后左 正因为如此 就可能会发生 每个人都拿着右边的Chopstick 并等待左边的Chopstick 的情况 这就是循环等待条件 然而 如果最后一个Philosopher被初始化成先拿左边的Chopstick 后拿右边的Chopstick 那么这个Philosopher将永远不会阻止其右边的Philosopher拿起他们的Chopstick 在本例中 这就可以防止循环等待 这只是问题的解决方法之一 也可以通过破坏其他条件来防止死锁
    Java编程思想 并发总结_第99张图片
    Java编程思想 并发总结_第100张图片

新类库中的构件

CountDownLatch
它被用来同步一个或多个任务 强制它们等待由其他任务执行的一组操作完成

CountDownLatch的典型用法是将一个程序分为n个互相独立的可解决任务 并创建值为0的CountDownLatch 当每个任务完成时 都会在这个锁存器上调用countDown() 等待问题被解决的任务在这个锁存器上调用await() 将它们自己拦住 直至锁存器计数结束 下面是演示这种技术的一个框架示例
Java编程思想 并发总结_第101张图片
Java编程思想 并发总结_第102张图片

类库的线程安全
注意 TaskPortion包含一个静态的Random对象 这意味着多个任务可能会同时调用Random.nextInt() 这是否安全呢
如果存在问题 在这种情况下 可以通过向TaskPortion提供其自己的Random对象来解决 也就是说 通过移除static限定符的方式解决 但是这个问题对于Java标准类库中的方法来说 也大都存在 哪些是线程安全的 哪些不是
遗憾的是 JDK文档并没有指出这一点 Random.nextInt()碰巧是安全的 但是 你必须通过使用Web引擎 或者审视Java类库代码 去逐个地揭示这一点 这对于被设计为支持 至少理论上支持并发的程序设计语言来说 并非是一件好事

CyclicBarrier
CyclicBarrier适用于这样的情况 你希望创建一组任务 它们并行地执行工作 然后在进行下一个步骤之前等待 直至所有任务都完成(看起来有些像join()) 它使得所有的并行任务都将在栅栏处列队 因此可以一致地向前移动 这非常像CountDownLatch 只是CountDownLatch是只触发一次的事件 而CyclicBarrier可以多次重用
并发是使仿真成为可能的一个关键因素 一个用BASIC编写的(由于文件名的限制而命名为HOSRAC.BAS)的赛马游戏 下面是那个程序的面向对象的多线程版本 其中使用了CyclicBarrier
Java编程思想 并发总结_第103张图片
Java编程思想 并发总结_第104张图片
Java编程思想 并发总结_第105张图片

DelayQueue
这是一个无界的BlockingQueue 用于放置实现了Delayed接口的对象 其中的对象只能在其到期时才能从队列中取走 这种队列是有序的 即队头对象的延迟到期的时间最长 如果没有任何延迟到期 那么就不会有任何头元素 并且poll()将返回null(正因为这样 你不能将null放置到这种队列中)
下面是一个示例 其中的Delayed对象自身就是任务 而DelayedTaskConsumer将最 紧急 的任务(到期时间最长的任务)从队列中取出 然后运行它 注意 这样DelayQueue就成为了优先级队列的一种变体
Java编程思想 并发总结_第106张图片
Java编程思想 并发总结_第107张图片
Java编程思想 并发总结_第108张图片

Delayed接口有一个方法名为getDelay() 它可以用来告知延迟到期有多长时间 或者延迟在多长时间之前已经到期 这个方法将强制我们去使用TimeUnit类 因为这就是参数类型 这会产生一个非常方便的类 因为你可以很容易地转换单位而无需作任何声明 例如 delta的值是以毫秒为单位存储的 但是Java SE5的方法System.nanoTime()产生的时间则是以纳秒为单位的 你可以转换delta的值 方法是声明它的单位以及你希望以什么单位来表示 就像下面这样
在这里插入图片描述

PriorityBlockingQueue
这是一个很基础的优先级队列 它具有可阻塞的读取操作 下面是一个示例 其中在优先级队列中的对象是按照优先级顺序从队列中出现的任务 PrioritizedTask被赋予了一个优先级数字 以此来提供这种顺序
Java编程思想 并发总结_第109张图片
Java编程思想 并发总结_第110张图片
Java编程思想 并发总结_第111张图片
Java编程思想 并发总结_第112张图片

使用ScheduledExecutor的温室控制器
在前几节中介绍过可以应用于假想温室的控制系统的示例 它可以控制各种设施的开关 或者是对它们进行调节 这可以被看作是一种并发问题 每个期望的温室事件都是一个在预定时间运行的任务 ScheduledThreadPoolExecutor提供了解决该问题的服务 通过使用schedule()(运行一次任务)或者scheduleAtFixedRate()(每隔规则的时间重复执行任务) 你可以将Runnable对象设置为在将来的某个时刻执行 将下面的程序与前几节中使用的方式相比 就会注意到 当你使用像ScheduledThreadPoolExecutor这样的预定义工具时 要简单许多
Java编程思想 并发总结_第113张图片
Java编程思想 并发总结_第114张图片
Java编程思想 并发总结_第115张图片
Java编程思想 并发总结_第116张图片
Java编程思想 并发总结_第117张图片

Semaphore
正常的锁(来自concurrent.locks或内建的synchronized锁)在任何时刻都只允许一个任务访问一项资源 而计数信号量允许n个任务同时访问这个资源 你还可以将信号量看作是在向外分发使用资源的 许可证 尽管实际上没有使用任何许可证对象
作为一个示例 请考虑对象池的概念 它管理着数量有限的对象 当要使用对象时可以签出它们 而在用户使用完毕时 可以将它们签回 这种功能可以被封装到一个泛型类中
Java编程思想 并发总结_第118张图片

为了创建一个示例 我们可以使用Fat 这是一种创建代价高昂的对象类型 因为它的构造器运行起来很耗时
Java编程思想 并发总结_第119张图片
我们在池中管理这些对象 以限制这个构造器所造成的影响 我们可以创建一个任务 它将签出Fat对象 持有一段时间之后再将它们签入 以此来测试Pool这个类
在这里插入图片描述
在这里插入图片描述

Exchanger
Exchanger是在两个任务之间交换对象的栅栏 当这些任务进入栅栏时 它们各自拥有一个对象 当它们离开时 它们都拥有之前由对象持有的对象 Exchanger的典型应用场景是 一个任务在创建对象 这些对象的生产代价很高昂 而另一个任务在消费这些对象 通过这种方式 可以有更多的对象在被创建的同时被消费
为了演练Exchanger类 我们将创建生产者和消费者任务 它们经由泛型和Generator 可以工作于任何类型的对象 然后我们将它们应用于Fat类 ExchangerProducer和ExchangerConsumer使用一个List作为要交换的对象 它们都包含一个用于这个List的Exchanger 当你调用Exchanger.exchanger()方法时 它将阻塞直至对方任务调用它自己的exchange()方法 那时 这两个exchange()方法将全部完成 而List则被互换
Java编程思想 并发总结_第120张图片
Java编程思想 并发总结_第121张图片
Java编程思想 并发总结_第122张图片

仿真

银行出纳员仿真
这个经典的仿真可以表示任何属于下面这种类型的情况 对象随机地出现 并且要求由数量有限的服务器提供随机数量的服务时间 通过构建仿真可以确定理想的服务器数量
在本例中 每个银行顾客要求一定数量的服务时间 这是出纳员必须花费在顾客身上 以服务顾客需求的时间单位的数量 服务时间的数量对每个顾客来说都是不同的 并且是随机确定的 另外 你不知道在每个时间间隔内有多少顾客会到达 因此这也是随机确定的
Java编程思想 并发总结_第123张图片
Java编程思想 并发总结_第124张图片
Java编程思想 并发总结_第125张图片
Java编程思想 并发总结_第126张图片
Java编程思想 并发总结_第127张图片
Java编程思想 并发总结_第128张图片

饭店仿真
这个仿真添加了更多的仿真组件 例如Order和Plate 从而充实了本节前面描述的Restaurant.java示例 并且它重用了前几节中的menu类 它还引入了Java SE5的SynchronousQueue 这是一种没有内容容量的阻塞队列 因此每个put()都必须等待一个take() 反之亦然 这就好像是你在把一个对象交给某人 没有任何桌子可以放置这个对象 因此只有在这个人伸出手 准备好接收这个对象时 你才能工作 在本例中 SynchronousQueue表示设置在用餐者面前的某个位置 以加强在任何时刻只能上一道菜这个概念
本例中剩下的类和功能都遵循Restaurant.java的结构 或者是对实际的饭店操作的相当直接的映射
Java编程思想 并发总结_第129张图片
Java编程思想 并发总结_第130张图片
Java编程思想 并发总结_第131张图片
Java编程思想 并发总结_第132张图片
Java编程思想 并发总结_第133张图片
Java编程思想 并发总结_第134张图片

分发工作
下面的仿真示例将本章的许多概念都结合在了一起 考虑一个假想的用于汽车的机器人组装线 每辆Car都将分多个阶段创建 从创建底盘开始 紧跟着是安装发动机 车厢和轮子
Java编程思想 并发总结_第135张图片
Java编程思想 并发总结_第136张图片
Java编程思想 并发总结_第137张图片
Java编程思想 并发总结_第138张图片
Java编程思想 并发总结_第139张图片
Java编程思想 并发总结_第140张图片
Java编程思想 并发总结_第141张图片

性能调优

比较各类互斥技术
既然Java包括老式的synchronized关键字和Java SE5中新的Lock和Atomic类 那么比较这些不同的方式 更多地理解它们各自的价值和使用范围 就会显得很有意义
比较天真的方式是在针对每种方式都执行一个简单的测试 就像下面这样
Java编程思想 并发总结_第142张图片
Java编程思想 并发总结_第143张图片

本例演示了所谓的 微基准测试 危险 这个术语通常指在隔离的 脱离上下文环境的情况下对某个特性进行性能测试 当然 你仍旧必须编写测试来验证诸如 Lock比synchronized更快 这样的断言 但是你需要在编写这些测试的时候意识到 在编译过程中和在运行时实际会发生什么

为了创建有效的测试 我们必须使程序更加复杂 首先我们需要多个任务 但并不只是会修改内部值的任务 还包括读取这些值的任务(否则优化器可以识别出这些值从来都不会被使用) 另外 计算必须足够复杂和不可预测 以使得编译器没有机会执行积极优化 这可以通过预加载一个大型的随机int数组(预加载可以减少在主循环上调用Random.nextInt())所造成的影响) 并在计算总和时使用它们来实现
在这里插入图片描述
Java编程思想 并发总结_第144张图片
Java编程思想 并发总结_第145张图片
Java编程思想 并发总结_第146张图片
Java编程思想 并发总结_第147张图片
Java编程思想 并发总结_第148张图片
Java编程思想 并发总结_第149张图片
Java编程思想 并发总结_第150张图片

免锁容器

免锁容器背后的通用策略是 对容器的修改可以与读取操作同时发生 只要读取者只能看到完成修改的结果即可 修改是在容器数据结构的某个部分的一个单独的副本(有时是整个数据结构的副本)上执行的 并且这个副本在修改过程中是不可视的 只有当修改完成时 被修改的结构才会自动地与主数据结构进行交换 之后读取者就可以看到这个修改了

乐观锁
只要你主要是从免锁容器中读取 那么它就会比其synchronized对应物快许多 因为获取和释放锁的开销被省掉了
从一个泛型框架着手 它专门用于在任何类型的容器上执行测试 包括各种Map在内 其中泛型参数C表示容器的类型
Java编程思想 并发总结_第151张图片
Java编程思想 并发总结_第152张图片

为了使用这个框架(其中你可以识别出模板方法设计模式) 我们必须让想要测试的特定类型的容器继承Tester 并提供适合的Reader和Writer类
Java编程思想 并发总结_第153张图片
Java编程思想 并发总结_第154张图片
Java编程思想 并发总结_第155张图片

比较各种Map实现
我们可以使用相同的框架来得到synchronizedHashMap和ConcurrentHashMap在性能方面的比较结果
Java编程思想 并发总结_第156张图片
Java编程思想 并发总结_第157张图片
Java编程思想 并发总结_第158张图片
Java编程思想 并发总结_第159张图片

乐观加锁
尽管Atomic对象将执行像decrementAndGet()这样的原子性操作 但是某些Atomic类还允许你执行所谓的 乐观加锁 这意味着当你执行某项计算时 实际上没有使用互斥 但是在这项计算完成 并且你准备更新这个Atomic对象时 你需要使用一个称为compareAndSet()的方法 你将旧值和新值一起提交给这个方法 如果旧值与它在Atomic对象中发现的值不一致 那么这个操作就失败 这意味着某个其他的任务已经于此操作执行期间修改了这个对象 记住 我们在正常情况下将使用互斥(synchronized或Lock)来防止多个任务同时修改一个对象 但是这里我们是 乐观的 因为我们保持数据为未锁定状态 并希望没有任何其他任务插入修改它 所有这些又都是以性能的名义执行的 通过使用Atomic来替代synchronized或Lock 可以获得性能上的好处
如果compareAndSet()操作失败会发生什么 这正是棘手的地方 也是你在应用这项技术时的受限之处 即只能针对能够吻合这些需求的问题 如果compareAndSet()失败 那么就必须决定做些什么 这是一个非常重要的问题 因为如果不能执行某些恢复操作 那么你就不能使用这项技术 从而必须使用传统的互斥 你可能会重试这个操作 如果在第二次成功 那么万事大吉 或者可能会忽略这次失败 直接结束 在某些仿真中 如果数据点丢失 在重要的框架中 这就是最终需要做的事情(当然 你必须很好地理解你的模型 以了解情况是否确实如此)
考虑一个假想的仿真 它由长度为30的100000个基因构成 这可能是某种类型的遗传算法的起源 假设伴随着遗传算法的每次 进化 都会发生某些代价高昂的计算 因此你决定使用一台多处理器机器来分布这些任务以提高性能 另外 你将使用Atomic对象而不是Lock对象来防止互斥开销(当然 一开始 你使用synchronized关键字以最简单的方式编写了代码 一旦你运行该程序 发现它太慢了 并开始应用性能调优技术 而此时你也只能写出这样的解决方案) 因为你的模型的特性 使得如果在计算过程中产生冲突 那么发现冲突的任务将直接忽略它 并不会更新它的值 下面是这个示例的代码
Java编程思想 并发总结_第160张图片
Java编程思想 并发总结_第161张图片

ReadWriteLock
ReadWriteLock对向数据结构相对不频繁地写入 但是有多个任务要经常读取这个数据结构的这类情况进行了优化 ReadWriteLock使得你可以同时有多个读取者 只要它们都不试图写入即可 如果写锁已经被其他任务持有 那么任何读取者都不能访问 直至这个写锁被释放为止
ReadWriteLock是否能够提高程序的性能是完全不确定的 它取决于诸如数据被读取的频率与被修改的频率相比较的结果 读取和写入操作的时间(锁将更复杂 因此短操作并不能带来好处) 有多少线程竞争以及是否在多处理机器上运行等因素 最终 唯一可以了解ReadWriteLock是否能够给你的程序带来好处的方式就是用试验来证明
下面是只展示了ReadWriteLock的最基本用法的示例
在这里插入图片描述
Java编程思想 并发总结_第162张图片
在这里插入图片描述
Java编程思想 并发总结_第163张图片

活动对象
Java中的线程机制看起来非常复杂并难以正确使用 另外 它好像还有点达不到预期效果的味道 尽管多个任务可以并行工作 但是你必须花很大的气力去实现防止这些任务彼此互相干涉的技术

有一种可替换的方式被称为活动对象或行动者 之所以称这些对象是 活动的 是因为每个对象都维护着它自己的工作器线程和消息队列 并且所有对这种对象的请求都将进入队列排队 任何时刻都只能运行其中的一个 因此 有了活动对象 我们就可以串行化消息而不是方法 这意味着不再需要防备一个任务在其循环的中间被中断这种问题了
当你向一个活动对象发送消息时 这条消息会转变为一个任务 该任务会被插入到这个对象的队列中 等待在以后的某个时刻运行 Java SE5的Future在实现这种模式时将派上用场 下面是一个简单的示例 它有两个方法 可以将方法调用排进队列
Java编程思想 并发总结_第164张图片
Java编程思想 并发总结_第165张图片
Java编程思想 并发总结_第166张图片

为了能够在不经意间就可以防止线程之间的耦合 任何传递给活动对象方法调用的参数都必须是只读的其他活动对象 或者是不连接对象 即没有连接任何其他任务的对象(这一点很难强制保障 因为没有任何语言支持它) 有了活动对象

  1. 每个对象都可以拥有自己的工作器线程
  2. 每个对象都将维护对它自己的域的全部控制权(这比普通的类要更严苛一些 普通的类只是拥有防护它们的域的选择权)
  3. 所有在活动对象之间的通信都将以在这些对象之间的消息形式发生
  4. 活动对象之间的所有消息都要排队

你可能感兴趣的:(Java编程思想)