您的位置:首页 > 编程语言 > Java开发

第三章 使用 JDK 并发包构建程序

2014-03-12 16:06 176 查看
3.1 java.util.concurrent 概述

主要包含原子量、并发集合、同步器、可重入锁,并对线程池的构造提供了强力的支持。

原子量是定义了支持对单一变量执行原子操作的类。 所有类都有 get 和 set 方法,工作方法和对 volatile 变量的读取和写入一样。

并发集合是原有集合框架的补充,为多线程并发程序提供了支持。主要有:BlockingQueue,ConcurrentMap,ConcurrentNavigableMap。

 同步器提供了一些帮助在线程间协调的类,包括 semaphores,barriers,latches,exchangers 等。

 一般同步代码依靠内部锁(隐式锁),这种锁易于使用,但是有很多局限性。新的 Lock对象支持更加复杂的锁定语法。 和隐式锁类似, 每一时刻只有一个线程能够拥有 Lock 对象,通过与其相关联的 Condition 对象,Lock 对象也支持 wait 和 notify 机制。

 线程完成的任务(Runnable 对象)和线程对象(Thread)之间紧密相连。适用于小型程序,在大型应用程序中,把线程管理和创建工作与应用程序的其余部分分离开更有意义。线程池封装线程管理和创建线程对象。

3.2 原子量

 无锁算法    (nonblocking algorithms) 这些无锁算法使用低层原子化的机器指令,例如使用compare-and-swap(CAS)代替锁保证并发情况下数据的完整性。

在 JDK5.0 之前,如果不使用本机代码,就不能用 Java 语言编写无等待、无锁定的算法。在 java.util.concurrent 中添加原子变量类之后,这种情况发生了变化。

要获得好的硬件利用率,只是简单地在多个线程中分割工作是不够的,还必须确保线程确实大部分时间都在工作,而不是在等待更多的工作,或等待锁定共享数据结构。

3.2.1 锁同步法

在 Java 语言中,协调对共享字段访问的传统方法是使用同步,确保完成对共享字段的所有访问,同时具有适当的锁定。通过同步,可以确定(假设类编写正确)具有保护一组访问变量的所有线程都将拥有对这些变量的独占访问权,   并且以后其他线程获得该锁定时,  将可以看到对这些变量进行的更改。   弊端是如果锁定竞争太厉害(线程常常在其他线程具有锁定时要求获得该锁定)   ,会损害吞吐量,因为竞争的同步非常昂贵。对于现代 JVM 而言,无竞争的同步现在非常便宜。

基于锁的算法的另一个问题是:如果延迟具有锁的线程(因为页面错误、计划延迟或其他意料之外的延迟),则没有要求获的锁的线程可以继续运行。

还可以使用 volatile 变量来以比同步更低的成本存储共享变量,但它们有局限性。虽然可以保证其他变量可以立即看到对 volatile 变量的写入,但无法呈现原子操作的读-修改-写

顺序,这意味着 volatile 变量无法用来可靠地实现互斥(互斥锁定)或计数器。

使用锁,如果一个线程试图获取其他线程已经具有的锁,那么该线程将被阻塞,直到该锁可用。此方法具有一些明显的缺点,其中包括当线程被阻塞来等待锁时,它无法进行其他任何操作。如果阻塞的线程是高优先级的任务,那么该方案可能造成非常不好的结果(称为优先级倒置的危险)。

 使用锁还有一些其他危险,如死锁(当以不一致的顺序获得多个锁时会发生死锁)。甚至没有这种危险,锁也仅是相对的粗粒度协调机制,同样非常适合管理简单操作,如增加计数器或更新互斥拥有者。如果有更细粒度的机制来可靠管理对单独变量的并发更新,则会更好一些;在大多数现代处理器都有这种机制。

3.2.2 比较并交换

大多数现代处理器都包含对多处理的支持。当然这种支持包括多处理器可以共享外部设备和主内存,同时它通常还包括对指令系统的增加来支持多处理的特殊要求。特别是,几乎

每个现代处理器都有通过可以检测或阻止其他处理器的并发访问的方式来更新共享变量的指令。

现在的处理器(包括 Intel 和 Sparc 处理器)使用的最通用的方法是实现名为“比较并交换(Compare And Swap)”或 CAS 的原语。(在 Intel 处理器中,比较并交换通过cmpxchg 系列指令实现。PowerPC 处理器有一对名为“加载并保留”和“条件存储”的指令,它们实现相同的目地;MIPS 与 PowerPC 处理器相似,除了第一个指令称为“加载链接”)。

 CAS 操作包含三个操作数 —— 内存位置(V)   、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。 (在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。 )CAS 有效地说明了“我认为位置 V应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。   ”

通常将 CAS 用于同步的方式是从地址 V 读取值 A,    执行多步计算来获得新值 B,   然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。

 类似于 CAS 的指令允许算法执行读-修改-写操作,而无需害怕其他线程同时修改变量,因为如果其他线程修改变量,那么 CAS 会检测它(并失败) ,算法可以对该操作重新计算。CAS 的价值是它可以在硬件中实现,并且是极轻量级的(在大多数处理器中) 。

 基于 CAS 的并发算法称为“无锁定算法”,因为线程不必再等待锁定(有时称为互斥或关键部分,这取决于线程平台的术语)  。无论 CAS 操作成功还是失败,在任何一种情况中,它都在可预知的时间内完成。如果 CAS 失败,调用者可以重试 CAS 操作或采取其他适合的操作。

如果每个线程在其他线程任意延迟(或甚至失败)时都将持续进行操作,就可以说该算法是 “无等待”的。 “无锁定算法”要求某个线程总是执行操作。 (无等待的另一种定义是保证每个线程在其有限的步骤中正确计算自己的操作,而不管其他线程的操作、计时、交叉或速度。这一限制可以是系统中线程数的函数;例如,如果有 10 个线程,每个线程都执行一次 CasCounter.increment() 操作,最坏的情况下,每个线程将必须重试最多九次,才能完成增加。

在过去的 15 年里,人们已经对无等待且无锁算法(也称为无阻塞算法)进行了大量研究,许多人通用数据结构已经发现了无阻塞算法。无阻塞算法被广泛用于操作系统和 JVM级别,进行诸如线程和进程调度等任务。虽然它们的实现比较复杂,但相对于基于锁的备选算法,它们有许多优点:可以避免优先级倒置和死锁等危险,竞争比较便宜,协调发生在更细的粒度级别,允许更高程度的并行机制等等。

3.2.3 原子变量类

java.util.concurrent.atomic 包中添加原子变量类。所有原子变量类都公开“比较并设置”原语(与比较并交换类似)  ,这些原语都是使用平台上可用的最快本机结构(比较并交换、

加载链接/条件存储,最坏的情况下是旋转锁)来实现的。 java.util.concurrent.atomic 包中提供了原子变量的 9 种风格(AtomicInteger、AtomicLong、AtomicReference、AtomicBoolean、原子整型、长型、引用、及原子标记引用和戳记引用类的数组形式,其原子地更新一对值)。

原子变量类可以认为是 volatile 变量的泛化, 它扩展了 volatile 变量的概念,来支持原子条件的比较并设置更新。读取和写入原子变量与读取和写入对 volatile 变量的访问具有相同

的存取语义。

原子变量的操作会变为平台提供的用于并发访问的硬件原语,比如比较并交换。

 调整具有竞争的并发应用程序的可伸缩性的通用技术是降低使用的锁对象的粒度,希望更多的锁请求从竞争变为不竞争。从锁转换为原子变量可以获得相同的结果,通过切换为更

细粒度的协调机制,竞争的操作就更少,从而提高了吞吐量。

3.2.4 使用原子量实现银行取款

 大多数用户都不太可能自己使用原子变量开发无阻塞算法,他们更可能使用java.util.concurrent 中提供的版本,如 ConcurrentLinkedQueue。但是万一您想知道对比以前JDK 中的相类似的功能,这些类的性能是如何改进的,可以使用通过原子变量类公开的细粒度、硬件级别的并发原语。

 开发人员可以直接将原子变量用作共享计数器、序号生成器和其他独立共享变量的高性能替代,否则必须通过同步保护这些变量。

 通过内部公开新的低级协调原语,和提供一组公共原子变量类,现在用 Java 语言开发无等待、无锁定算法首次变为可行。然后, java.util.concurrent 中的类基于这些低级原子变

量工具构建, 为它们提供比以前执行相似功能的类更显著的可伸缩性优点。  虽然您可能永远不会直接使用原子变量,还是应该为它们的存在而欢呼。

3.3 并发集合

3.3.1 队列 Queue 与 BlockingQueue

java.util 包为集合提供了一个新的基本接口:java.util.Queue。虽然肯定可以在相对应的两端进行添加和删除而将 java.util.List 作为队列对待,但是这个新的 Queue 接口提供了支

持添加、删除和检查集合的更多方法。

   1) boolean add(Object e):将指定的元素插入此队列(如果立即可行且不会违反容量限制) 在成功时返回 true,否则抛出 IllegalStateException。

   2) public boolean offer(Object element):将指定的元素插入此队列(如果立即可行且不会违反容量限制)  ,当使用有容量限制的队列时,此方法通常要优于 add(E),后者可能无法插

入元素,而只是抛出一个异常。

   3) public Object remove():获取并移除此队列的头。

   4) public Object poll();获取并移除此队列的头,如果此队列为空,则返回 null。

   5) public Object element();获取但是不移除此队列的头。       此队列为空时将抛出一个异常。

   6) public Object peek();获取但不移除此队列的头;如果此队列为空,则返回 null。

 基本上,一个队列就是一个先入先出(FIFO)的数据结构。一些队列有大小限制,因此如果想在一个满的队列中加入一个新项,多出的项就会被拒绝。这时新的 offer 方法就可以起作用了。它不是对调用 add()方法抛出一个 unchecked 异常,而只是得到由 offer()方法返回的 false。 remove() 和 poll() 方法都是从队列中删除第一个元素(head) 。remove() 的 行为与 Collection 接口的版本相似,但是新的 poll()方法在用空集合调用时不是抛出异常,只是返回
null。因此新的方法更适合容易出现异常条件的情况。后两个方法 element() 和

peek()用于在队列的头部查询元素。与 remove() 方法类似,在队列为空时, element() 抛出一个异常,而 peek() 返回 null。

 在 JDK 中有两组 Queue 实现:实现了新 BlockingQueue 接口的和没有实现这个接口的。

 原 来 有 的 java.util.LinkedList 实 现 已 经 改 造 成 不 仅 实 现 java.util.List而且还实现 java.util.Queue 接口。可以将 LinkedList 集合看成这两者中的t接口任何一种。

 PriorityQueue 和 ConcurrentLinkedQueue 类在 Collection Framework 中加入两个具体集合实现。PriorityQueue 类实质上维护了一个有序列表。 加入到 Queue 中的元素根据它们的天然排序  (通过其 java.util.Comparable 实现)   或者根据传递给构造函数的 java.util.Comparator实现来定位。

ConcurrentLinkedQueue 是基于链接节点的、线程安全的队列。并发访问不需要同步。因为它在队列的尾部添加元素并从头部删除它们,所以只要不需要知道队列的大小, ConcurrentLinkedQueue 对公共集合的共享访问就可以工作得很好。收集关于队列大小的信息会很慢,需要遍历队列。

新的 java.util.concurrent 包可用的具体集合类中加入了 BlockingQueue 接口和五个阻塞队列类。阻塞队列实质上就是一种带有一点扭曲的 FIFO 数据结构,不是立即从队列中添

加或者删除元素,线程执行操作被阻塞,直到有空间或者元素可用。 BlockingQueue 接口的Javadoc 给出了阻塞队列的基本用法,生产者中的 put() 操作会在没有空间可用时阻塞,而

消费者的 take() 操作会在队列中没有任何东西时阻塞。

 五个队列所提供的各有不同:

  ArrayBlockingQueue :一个由数组支持的有界队列。

 LinkedBlockingQueue :一个由链接节点支持的可选有界队列。

  PriorityBlockingQueue :一个由优先级堆支持的无界优先级队列。

 DelayQueue :一个由优先级堆支持的、基于时间的调度队列。

 SynchronousQueue :一个利用 BlockingQueue 接口的简单聚集(rendezvous)机制。

  ArrayBlockingQueue 和 LinkedBlockingQueue 几乎相同,只是在后备存储器方 面 有 所 不 同 , LinkedBlockingQueue 并 不 总 是 有 容 量 界 限 。 无 大 小 界 限 的LinkedBlockingQueue 类 在 添 加 元 素 时 永 远 不 会 有 阻 塞 队 列 的 等 待 ( 至 少 在 其 中 有Integer.MAX_VALUE 元素之前不会) 。

 PriorityBlockingQueue 是具有无界限容量的队列,它利用所包含元素的 Comparable 排序顺序来以逻辑顺序维护元素。可以将它看作 TreeSet 的可能替代物。例如,在队列中加

入字符串 One、Two、Three 和 Four 会导致 Four 被第一个取出来。对于没有天然顺序的元素,可以为构造函数提供一个 Comparator 。不过对 PriorityBlockingQueue 有一个技巧。从 iterator() 返回的 Iterator 实例不需要以优先级顺序返回元素。如果必须以优先级顺序遍历所有元素,那么让它们都通过 toArray() 方 法 并 自 己 对 它 们 排 序 , 像Arrays.sort(pq.toArray()) 。

 新的 DelayQueue 实现可能是其中最有意思(也是最复杂)的一个。加入到队列中的元素必须实现新的 Delayed 接口(只有一个方法 long getDelay(java.util.concurrent.TimeUnit

unit) )。因为队列的大小没有界限,使得添加可以立即返回,但是在延迟时间过去之前,不能从队列中取出元素。如果多个元素完成了延迟,那么最早失效/失效时间最长的元素将第

一个取出。

SynchronousQueue 类是最简单的。它没有内部容量。它就像线程之间的手递手机制。在队列中加入一个元素的生产者会等待另一个线程的消费者。当这个消费者出现时,这个元素就直接在消费者和生产者之间传递,永远不会加入到阻塞队列中。

3.3.2 使用 ConcurrentMap 实现类

java.util.concurrent.ConcurrentMap 接口和 ConcurrentHashMap 实现类只能在键不存在时将元素加入到 map 中,只有在键存在并映射到特定值时才能从 map 中删除一个元素。主要定义了下面几个方法(K 表示键的类型,V 表示值的类型)                          :

   V putIfAbsent(K key,V value):如果指定键已经不再与某个值相关联,则将它与给定值关联。

   boolean remove(Object key,Object value):只有目前将键的条目映射到给定值时,才移除该键的条目。

   boolean replace(K key,V oldValue,V newValue):只有目前将键的条目映射到给定值时,才替换该键的条目。

   V replace(K key,V value):只有目前将键的条目映射到某一值时,才替换该键的条目。

   putIfAbsent() 方法用于在 map 中进行添加。这个方法以要添加到 ConcurrentMap 中的键的值为参数,就像普通的 put() 方法,但是只有在 map 不包含这个键时,才能将键加入

到 map 中。如果 map 已经包含这个键,那么这个键的现有值就会保留。 putIfAbsent() 方法是原子的。等价于下面的代码(除了原子地执行此操作之外)                             :

      if (!map.containsKey(key))

                return map.put(key, value);

          else

             return map.get(key);

像 putIfAbsent() 方法一样,重载后的 remove() 方法有两个参数:键和值。在调用时,只有当键映射到指定的值时才从 map 中删除这个键。如果不匹配,那么就不删除这个键,

并返回 false。如果值匹配键的当前映射内容,那么就删除这个键,这个方法是原子性的。

这种操作的等价源代码(除了原子地执行此操作之外)                         :

      if (map.containsKey(key) && map.get(key).equals(value)) {

                 map.remove(key);

                 return true;

       } else return false;

   总之,ConcurrentMap 中定义的方法是原子性的。

3.3.3 CopyOnWriteArrayList 和 CopyOnWriteArraySet

这两个集合对对 copy-on-write 模式作了比较好的支持。这个模式说明了,为了维护对象的一致性快照,要依靠不可变性(immutability)来消除在协调读取不同的但是相关的属

性时需要的同步。

对于集合,这意味着如果有大量的读(即 get() )和迭代,不必进行同步操作以照顾偶尔的写(即 add() )调用。对于新的 CopyOnWriteArrayList 和 CopyOnWriteArraySet 类,所有可变的(mutable)操作都首先取得后台数组的副本,对副本进行更改,然后替换副本。这 种 做 法 保 证 了 在 遍 历 自 身 可 更 改 的 集 合 时 , 永 远 不 会 抛 出ConcurrentModificationException。遍历集合会用原来的集合完成,而在以后的操作中使用更新后的集合。

  这些新的集合最适合于读操作通常大大超过写操作的情况。集合的使用与它们的非copy-on-write 替代物完全一样。只是创建集合并在其中加入或者删除元素。即使对象加入到

了集合中,原来的 Iterator 也可以进行,继续遍历原来集合中的项。

3.4 同步器

3.4.1 Semaphore

 类 java.util.concurrent.Semaphore 提供了一个计数信号量,从概念上讲,信号量维护了一个许可集。  如有必要,  在许可可用前会阻塞每一个 acquire(),  然后再获取该许可。每个 release()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。

 Semaphore 通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。一般操作系统的进程调度中使用了 PV 原语,需要设置一个信号量表示可用资源的数量,P 原语就相当

于 acquire(),V 原语就相当于 release()。

 注意,调用 acquire()时无法保持同步锁,因为这会阻止将数据项返回到池中。信号量封装所需的同步,以限制对池的访问,这同维持该池本身一致性所需的同步是分开的。将信号量初始化为 1,使得它在使用时最多只有一个可用的许可,从而可用作一个相互排斥的锁。这通常也称为二进制信号量,因为它只能有两种状态:一个可用的许可,或零个可用的许可。按此方式使用时,二进制信号量具有某种属性(与很多 Lock 实现不同)  ,即可以由线程释放“锁”  ,而不是由所有者(因为信号量没有所有权的概念)  。在某些专门的上下文(如死锁恢复)中这会很有用。

3.4.2 Barrier

java.util.concurrent.CyclicBarrier 一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。

 因为该 barrier 在释放等待线程后可以重用,所以称它为循环的 barrier。CyclicBarrier 最重要的属性就是参与者个数,另外最要方法是 await()。当所有线程都调用了 await()后,就

表示这些线程都可以继续执行,否则就会等待。

CyclicBarrier 支持一个可选的 Runnable 命令,在一组线程中的最后一个线程到达之后,该命令只在每个屏障点运行一次。若在继续所有参与线程之前(但在释放所有线程之前)

更新共享状态,此屏障操作有用。

3.4.3 CountDownLatch

 类 java.util.concurrent.CountDownLatch 是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。

用给定的数字作为计数器初始化 CountDownLatch。一个线程调用 await()方法后,在当前计数到达零之前,会一直受阻塞。其他线程调用 countDown() 方法,会使计数器递减,

所以,计数器的值为 0 后,会释放所有等待的线程。其他后续的 await 调用都将立即返回。

这种现象只出现一次,  因为计数无法被重置。 如果需要重置计数,请考虑使用 CyclicBarrier。

 CountDownLatch 作 为 一 个 通 用 同 步 工 具 , 有 很 多 用 途 。 使 用 “ 1 ” 初 始 化 的CountDownLatch 用作一个简单的开/关锁存器,或入口:在通过调用 countDown() 的线程

打 开 入 口 前 , 所 有 调 用 await 的 线 程 都 一 直 在 入 口 处 等 待 。 用 N 初 始 化 的CountDownLatch 可以使一个线程在 N 个线程完成某项操作之前一直等待,或者使其在某

项操作完成 N 次之前一直等待。
另一种典型用法是,将一个问题分成 N 个部分,执行每个部分并让 CountDownLatch倒计数的 Runnable 来描述每个部分,然后将所有 Runnable 加入到 Executor 队列。当所有

的子部分完成后,协调线程就能够通过 await。  (当线程必须用这种方法反复倒计数时,可改为使用 CyclicBarrier。)

3.4.4 Exchanger

 类 java.util.concurrent.Exchanger 提供了一个同步点,在这个同步点,一对线程可以交换数据。每个线程通过 exchange()方法的入口提供数据给他的伙伴线程,并接收他的伙伴线程提供的数据,并返回。

 当在运行不对成的活动时很有用,比如当一个线程填充了 buffer,另一个线程从 buffer中消费数据;这些线程可以用 Exchanger 来交换数据。当两个线程通过 Exchanger 交互了对

象,这个交换对于两个线程来说都是安全的。

3.4.5 Future 和 FutureTask

  接口 public interface Future<V> 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。计算完成后只能使用 get() 方法来获取结果,

如有必要,计算完成前可以阻塞此方法。取消则由 cancel 方法来执行。还提供了其他方法,以确定任务是正常完成还是被取消了。一旦计算完成,就不能再取消计算。如果为了可取消性而使用 Future 但又不提供可用的结果,  则可以声明 Future<?> 形式类型、并返回 null 作为底层任务的结果。

Future 主要定义了 5 个方法:

1) boolean cancel(boolean mayInterruptIfRunning):试图取消对此任务的执行。   如果任务已完成、或已取消, 或者由于某些其他原因而无法取消, 则此尝试将失败。

当调用 cancel 时,如果调用成功,而此任务尚未启动,则此任务将永不运行。如果任务已经启动,则 mayInterruptIfRunning 参数确定是否应该以试图停止任务的方式来中断执行此

任务的线程。

2) boolean isCancelled():如果在任务正常完成前将其取消,则返回 true。

3) boolean isDone():如果任务已完成,则返回 true。 可能由于正常终止、异常或取消而完成,在所有这些情况中,此方法都将返回 true。

4) V get() throws InterruptedException,ExecutionException :如有必要,等待计算完成,然后获取其结果。

5) V get(long timeout, TimeUnit unit) throws InterruptedException,ExecutionException, TimeoutException:如有必要,最多等待为使计算完成所给定的时间之后,获取其结果(如果结果可用)。

FutureTask 类 是Future 的 一 个 实 现 , 并 实 现 了 Runnable , 所 以 可 通 过Executor(线程池) 来执行。也可传递给Thread对象执行。

如果在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,  可以把这些作业交给 Future 对象在后台完成,当主线程将来需要时,就可以通过 Future 对象获得后台作业

的计算结果或者执行状态。

3.5 显示锁

3.5.1 ReentrantLock

java.util.concurrent.lock 中的类 ReentrantLock 被作为 Java 语言中 synchronized 功能的替代,它具有相同的内存语义、相同的锁定,但在争用条件下却有更好的性能,此外,它

还有 synchronized 没有提供的其他特性。

Java 是第一个直接把跨平台线程模型和正规的内存模型集成到语言中的主流语言。 核心类库包含一个 Thread 类,可以用它来构建、启动和操纵线程,Java 语言包括了跨线程传

达并发性约束的构造 —— synchronized 和 volatile 。在简化与平台无关的并发类的开发的同时,它决没有使并发类的编写工作变得更繁琐,只是使它变得更容易了。

 把代码 块声明为synchronized , 有 两 个 重 要 后 果 , 通 常 是 指 该 代 码 具 有 原 子 性(atomicity)和可见性(visibility)。原子性意味着一次只能有一个线程执行一个指定监控对

象(lock)保护的代码,从而防止多个线程在更新共享状态时相互冲突。可见性则更为微妙;它要对付内存缓存和编译器优化的各种反常行为。 一般来说,   线程以某种不必让其他线程立即可以看到的方式(不管这些线程在寄存器中、在处理器特定的缓存中,还是通过指令重排或者其他编译器优化)不受缓存变量值的约束,但是如果开发人员使用了同步,那么运行库将确保某一线程对变量所做的更新先于对现有 synchronized 块所进行的更新,当进入由同一监控器(lock)保护的另一个
synchronized 块时,将立刻可以看到这些对变量所做的更新。类似的规则也存在于 volatile 变量上。

使用 synchronized 进行同步的典型方法如下:

synchronized (lockObject) {

  //更新对象状态

}

 实现同步操作需要考虑安全更新多个共享变量所需的一切,  不能有争用条件,不能破坏数据(假设同步的边界位置正确) ,而且要保证正确同步的其他线程可以看到这些变量的最

新值。通过定义一个清晰的、跨平台的内存模型,通过遵守下面这个简单规则,构建“一次编写,随处运行”的并发类是有可能的:不论什么时候,只要您将编写的变量接下来可能被

另一个线程读取,或者您将读取的变量最后是被另一个线程写入的,那么您必须进行同步。

  Synchronized 虽然能够实现同步,但是他有一些限制,比如:它无法中断一个正在等候获得锁的线程,也无法通过投票得到锁,如果不想等下去,也就没法得到锁。

3.5.1.1ReentrantLock 的特性

java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。 ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等等的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。
)

ReentrantLock(可重入锁)有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加 1,然后锁需要被释放两次才能获得真正释放。这模仿了synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续)synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。

ReentrantLock 锁的使用方法如下:
Lock lock = new ReentrantLock();
lock.lock();
try {
// 更新对象状态
}
finally {
lock.unlock();
}Lock 和 synchronized 有一点明显的区别 —— lock 必须在 finally 块中释放。否则,如果受保护的代码将抛出异常, 锁就有可能永远得不到释放。这一点区别看起来可能没什么,但是实际上,它极为重要。忘记在 finally 块中释放锁,可能会在程序中留下一个定时炸弹,当有一天炸弹爆炸时,您要花费很大力气才有找到源头在哪。而使用 synchronized 同步,JVM

将确保锁会获得自动释放。

除此之外,与目前的 synchronized 实现相比,ReentrantLock 实现更具可伸缩性。

根类 Object 包含某些特殊的方法,如:wait() 、 notify() 和 notifyAll() 在线程之间进行通信。这些是高级的并发性特性,   许多开发人员从来没有用过它们 —— 这可能是件好事,因为它们相当微妙,    很容易使用不当。幸运的是, 随着 JDK 5.0 中引入 java.util.concurrent ,开发人员几乎更加没有什么地方需要使用这些方法了。

 通知与锁定之间有一个交互 —— 为了在对象上 wait 或 notify ,您必须持有该对象的锁。就像 Lock 是同步的概括一样, Lock 框架包含了对 wait 和 notify 的概括,这个概括叫做条件(Condition)。Lock 对象则充当绑定到这个锁的条件变量的工厂对象,与标准的 wait 和 notify 方法不同,对于指定的 Lock ,可以有不止一个条件变量与它关联。这样就简化了许多并发算法的开发。例如, 条件(Condition) 的 Javadoc 显示了一个有界缓冲区实现的示例,
该示例使用了两个条件变量,  “not full” “not empty” 它比每个 lock只用一个 wait 设置的实现方式可读性要好一些。 Condition 的方法与 wait 、notify 和 notifyAll 方法类似,分别命名为 await 、 signal 和 signalAll ,因为它们不能覆盖 Object 上的对应方法。

ReentrantLock 构造器的一个参数是 boolean 值,它允许选择想要一个公平(fair)锁,还是一个不公平(unfair)锁。公平锁使线程按照请求锁的顺序依次获得锁;而不公平锁则允许讨价还价,在这种情况下,线程有时可以比先请求锁的其他线程先得到锁。

为什么我们不让所有的锁都公平呢?毕竟,公平是好事,不公平是不好的,不是吗?在现实中,公平保证了锁是非常健壮的锁,有很大的性能成本。要确保公平所需要的记帐(bookkeeping)和同步,就意味着被争夺的公平锁要比不公平锁的吞吐率更低。作为默认设置,应当把公平设置为 false ,除非公平对您的算法至关重要,需要严格按照线程排队的

顺序对其进行服务。
那么同步又如何呢?内置的监控器锁是公平的吗?答案令许多人感到大吃一惊, 它们是不公平的,而且永远都是不公平的。但是没有人抱怨过线程饥渴,因为 JVM 保证了所有线程最终都会得到它们所等候的锁。确保统计上的公平性, 对多数情况来说,这就已经足够了,而这花费的成本则要比绝对的公平保证的低得多。所以,默认情况下 ReentrantLock 是“不

公平”的,这一事实只是把同步中一直不公平的东西表面化而已。如果您在同步的时候并不介意这一点,那么在 ReentrantLock 时也不必为它担心。

除非您对 Lock 的某个高级特性有明确的需要,或者有明确的证据(而不是仅仅是怀疑)表明在特定情况下,同步已经成为可伸缩性的瓶颈,否则还是应当继续使用 synchronized。

 为什么我在一个显然“更好的”实现的使用上主张保守呢?因为对于java.util.concurrent.lock 中的锁定类来说,synchronized 仍然有一些优势。比如,在使用 显示锁的时候,可能忘记用 finally 块释放锁,这对程序非常有害。您的程序能够通过测试,但会在实际工作中出现死锁, 那时会很难指出原因  (这也是为什么根本不让初级开发人员使用 Lock 的一个好理由。)但在退出 synchronized 块时,JVM 会为您做这件事。

另一个原因是因为,当 JVM 用 synchronized 管理锁定请求和释放时,JVM 在生成线程转储时能够包括锁定信息。这些对调试非常有价值,  因为它们能标识死锁或者其他异常行为的来源。 Lock 类只是普通的类,JVM 不知道具体哪个线程拥有 Lock 对象。而且,几乎每个开发人员都熟悉 synchronized,它可以在 JVM 的所有版本中工作。在 JDK 5.0 成为标准(从现在开始可能需要两年)之前,使用 Lock 类将意味着要利用的特性不是每个JVM 都有的,而且不是每个开发人员都熟悉的。

 既然如此,    我们什么时候才应该使用 ReentrantLock 呢?答案非常简单 —— 在确实需要一些 synchronized 所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者锁投票。 ReentrantLock 还具有可伸缩性的好处,应当在高度争用的情况下使用它,但是请记住,大多数 synchronized 块几乎从来没有出现过争用,所以可以把高度争用放在一边。我建议用 synchronized 开发,直到确实证明 synchronized 不合适,而不要仅仅是假设如果使用
ReentrantLock “性能会更好”   。请记住,这些是供高级用户使用的高级工具。 (而且,真正的高级用户喜欢选择能够找到的最简单工具,直到他们认为简单的工具不适用为止。。一如既往,首先要把事情做好,然后再考虑是不是有必要做得更快。)

3.5.1.2ReentrantLock 性能测试

非公平重入锁的性能最好,公平重入锁的性能最差。在线程数比较少的情况下,内部锁和非公平重入锁的性能相当。

ReentrantLock 还有两个比较重要的方法是:tryLock()和 tryLock(long timeout, TimeUnit unit)。tryLock()仅在调用时锁未被另一个线程保持的情况下,才获取该锁。后者如果锁在给定等待时间内没有被另一个线程持有,且当前线程未被中断,则获取该锁。

3.5.2 ReadWriteLock

ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。

所有 ReadWriteLock 实现都必须保证 writeLock 操作的内存同步效果也要保持与相关readLock 的联系。也就是说,成功获取读锁的线程会看到写入锁之前版本所做的所有更新。

 与互斥锁相比,读-写锁允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程),读-写锁利用了这一点。从理论上讲,与互斥锁相比,使用读-写锁式适用于共享数据时,才能完全实现并发性增强。在实践中,只有在多处理器上并且只在访问模所允许的并发性增强将带来更大的性能提高。

与互斥锁相比,使用读-写锁能否提升性能则取决于读写操作期间读取数据相对于修改数据的频率,以及数据的争用——即在同一时间试图对该数据执行读取或写入操作的线程数。

3.6 Fork-Join 框架

在 JDK 7 中, java.util.concurrent 包的新增功能之一是一个 fork-join 风格的并行分解框架。fork-join 概念提供了一种分解多个算法的自然机制, 可以有效地应用硬件并行性。

3.6.1 应用 Fork-Join

语言、库和框架形成了我们编写程序的方式。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息