|
67 | 67 | public synchronized void push(int elt) { ... }
|
68 | 68 | ```
|
69 | 69 |
|
| 70 | +这称为同步代码的关键部分,在这种情况下是整个推送方法。在一个线程可以执行同步代码之前,它必须获得某个对象的锁定 - 默认情况下,就像在这种情况下一样,当前对象。当锁由一个线程持有时,另一个线程尝试进入在该锁上同步的任何关键部分将会阻塞 - 即,将被暂停 - 直到它可以获得锁。这种同步版本的推送是线程安全的;在多线程环境中,每个线程的行为与其在单线程环境中的行为保持一致。为了保证不变,并使 `ArrayStack` 作为一个整体线程安全,方法pop和isEmpty也必须在同一个对象上同步。方法 `isEmpty` 不写入共享数据,因此不需要同步它来防止竞争条件,但出于不同的原因。每个线程都可以使用单独的内存缓存,这意味着一个线程的写入操作可能不会被另一个线程看到,除非它们都发生在同一个锁上同步的块内,或者除非变量标记了 `volatile` 关键字。 |
| 71 | + |
| 72 | +实际上,完全方法同步是 `JDK1.0` 中提供的集合类的策略:`Vector`,`Hashtable` 及其子类;访问其实例数据的所有方法都是同步的。这些现在被视为遗留类,因为这种政策对这些类的所有客户提出高昂的价格是要避免的,无论它们是否需要线程安全。同步可能非常昂贵:强制线程逐个排队进入关键部分,会减慢程序的整体执行速度,如果经常发生争用,管理锁的开销可能非常高。 |
| 73 | + |
| 74 | +#### `JDK 1.2`:同步集合和失败快速迭代器 |
| 75 | + |
| 76 | +在 `JDK 1.2` 中首次引入集合框架时,`JDK 1.0` 集合中内部同步的性能成本使得设计人员避免了这种情况。相反,接口 `List`,`Set` 和 `Map` 的平台实现扩大了程序员对并发策略的选择范围。为了为单线程执行提供最高性能,新的集合根本没有提供并发控制。 (最近,对同步类 `StringBuffer` 进行了相同的策略更改,在 `Java 5` 中通过其未同步的等效 `StringBuilder` 对其进行了补充。) |
| 77 | + |
| 78 | +随着这一变化,为集合迭代器提供了一个新的并发策略。在多线程环境中,获取迭代器的线程通常会继续使用它,而其他线程修改原始集合。因此,迭代器行为必须被视为集合并发策略的组成部分。如第 `11.1` 节所述,`Java 2` 集合迭代器的策略是快速失败:每次访问后备集合时,都会检查它是否进行结构修改(通常意味着元素已添加或从中删除集合)。如果他们检测到结构修改,则立即失败,抛出 `ConcurrentModificationException` 而不是继续尝试迭代修改后的集合,结果不可预知。请注意,此故障快速行为用于帮助查找和诊断错误;它不作为收集合同的一部分得到保证。 |
| 79 | + |
| 80 | +没有强制同步的 `Java` 集合的出现是一个值得欢迎的发展。 然而,在很多情况下仍然需要线程安全的集合,所以框架通过同步包装提供了一个选择,使用新的集合和旧的并发策略(请参阅第 `17` 章)。 这些是通过调用 `Collections` 类中的一个工厂方法创建的,它提供了一个将被封装的非同步集合。 例如,要创建一个同步列表,您可以提供一个要包装的 `ArrayList` 实例。 包装器通过将方法调用委托给您提供的集合来实现接口,但调用在包装器对象本身上同步。 例 `11-3` 显示了例 `11-2` 的接口 `Stack` 的一个同步封装器。 要得到一个线程安全的堆栈,你可以这样写: |
| 81 | + |
| 82 | +```java |
| 83 | + Stack threadSafe = new SynchronizedArrayStack(new ArrayStack()); |
| 84 | +``` |
| 85 | + |
| 86 | +这是使用同步包装的首选方式; 对包装对象的唯一引用是由包装器保存的,因此包装对象上的所有调用都将在属于包装器对象本身的同一个锁上进行同步。 让同步的包装器可用是非常重要的,但是你不会使用它们,因为它们与传统集合具有相同的性能缺点。 |
| 87 | + |
| 88 | +例 `11-3`。 一个 `ArrayStack` 的同步包装器 |
| 89 | + |
| 90 | +```java |
| 91 | + public class SynchronizedArrayStack implements Stack { |
| 92 | + private final Stack stack; |
| 93 | + public SynchronizedArrayStack(Stack stack) { |
| 94 | + this.stack = stack; |
| 95 | + } |
| 96 | + public synchronized void push(int elt) { stack.push(elt); } |
| 97 | + public synchronized int pop() { return stack.pop(); } |
| 98 | + public synchronized boolean isEmpty() { return stack.isEmpty(); } |
| 99 | + } |
| 100 | +``` |
| 101 | + |
| 102 | +安全地使用同步集合即使像 `SynchronizedArrayStack` 这样拥有完全同步方法并且本身是线程安全的类仍然必须在并发环境中小心使用。 例如,这个客户端代码不是线程安全的: |
| 103 | + |
| 104 | +```java |
| 105 | + Stack stack = new SynchronizedArrayStack(new ArrayStack()); |
| 106 | + ... |
| 107 | + // 不要在多线程环境中执行此操作 |
| 108 | + if (!stack.isEmpty()) { |
| 109 | + stack.pop(); // 可以抛出IllegalStateException |
| 110 | + } |
| 111 | +``` |
| 112 | + |
| 113 | +如果在评估 `isEmpty` 和执行 `pop` 之间的时间内堆栈中的最后一个元素被另一个线程删除,则会引发异常。 这是一个常见的并发程序错误的例子,有时称为 `test-then-act`,其中程序行为由在某些情况下会过时的信息指导。 为了避免它,测试和操作必须以原子方式执行。 对于同步集合(与旧集合一样),必须通过客户端锁定实施: |
| 114 | + |
| 115 | +```java |
| 116 | + synchronized(stack) { |
| 117 | + if (!stack.isEmpty()) { |
| 118 | + stack.pop(); |
| 119 | + } |
| 120 | + } |
| 121 | +``` |
| 122 | + |
| 123 | +为了使这种技术能够可靠地工作,客户端用来保护原子动作的锁定应该与同步包装器的方法所使用的相同。在本例中,与同步集合中一样,包装器的方法是 在包装器对象本身上同步。(另一种方法是在单个客户端中限制对集合的引用,从而强化自己的同步规则。但是这种策略的适用性有限。) |
| 124 | + |
| 125 | +客户端锁定确保线程安全,但代价是:由于其他线程在执行操作时不能使用任何集合的方法,因此守护长时间操作(例如遍历整个阵列)将影响吞吐量 如果同步方法大量使用,这种影响可能非常大; 除非您的应用程序需要同步集合的功能,例如独占锁定,否则 `Java 5` 并发集合几乎总是更好的选择。 |
| 126 | + |
| 127 | +#### 并发集合:`Java 5` 和其他 |
| 128 | + |
| 129 | +`Java 5` 引入了线程安全的并发集合,作为一组更大的并发实用程序的一部分,其中包括原语 - 原子变量和锁 - 它们使 `Java` 程序员能够访问管理并发线程的相对最新的硬件创新,特别是比较和交换操作,下面解释。并发集合删除了前面部分描述的客户端锁定的必要性 - 事实上,这些集合甚至不可能实现外部同步,因为没有一个对象在被锁定时会阻止所有方法。操作需要是原子操作的,例如,只有当元素当前不存在时才将元素插入到Map中 - 并发集合提供了一个指定为自动执行的方法 - 在本例中为 `ConcurrentMap.putIfAbsent`。 |
| 130 | + |
| 131 | +如果您需要线程安全性,并发集合通常提供比同步集合更好的性能。这主要是因为它们的吞吐量并未因需要序列化访问而降低,正如同步集合的情况一样。同步的集合也遭受管理锁的开销,如果争用太多,则锁可能很高。这些差异会导致超过几个线程的并发访问的两个数量级的效率差异。 |
| 132 | + |
| 133 | +机制并发集合通过几种不同的机制实现线程安全。其中第一个是唯一不使用新基元的是写时复制。使用 `copy-on-write` 的类将它们的值存储在内部数组中,该数组实际上是不可变的;对集合值的任何更改都会导致创建一个新数组来表示新值。同步被这些类使用,尽管只是简单地在创建新数组期间使用;由于读取操作不需要同步,因此写入时复制集合在设计它们的情况下表现良好,其中读取在写入方面占据主导地位。`Copy-on-write` 由集合类 `CopyOnWriteArrayList` 和 `CopyOnWriteArraySet` 使用。 |
| 134 | + |
| 135 | +第二组线程安全集合依赖于比较和交换(`CAS`),这是对传统同步的根本改进。为了看它是如何工作的,考虑一种计算,其中将单个变量的值用作长期运行计算的输入,该计算的最终结果用于更新变量。传统的同步使得整个计算为原子操作,排除其他任何线程同时访问变量。这减少了并行执行的机会并且损害了吞吐量。基于 `CAS` 的算法表现方式不同:它会生成变量的本地副本并执行计算而不会获得排他访问权限。只有当准备好更新变量时,它才会调用 `CAS`,`CAS` 在一次原子操作中将变量值与开始时的值进行比较,如果它们相同,则用新值更新它。如果它们不相同,则该变量必须由另一个线程修改;在这种情况下,`CAS` 线程可以使用新值再次尝试整个计算,或者放弃,或者在某些算法中继续执行,因为干扰实际上已经完成了它的工作!使用 `CAS` 的集合包括 `ConcurrentLinkedQueue` 和 `ConcurrentSkipListMap`。 |
| 136 | + |
| 137 | +第三组使用 `java.util.concurrent.locks.Lock` 的实现,`Java 5` 中引入的接口作为经典同步的更灵活的替代方案。锁具有与传统同步相同的基本行为,但线程也可以在特殊情况下获得它:只有当锁没有被保持,或者超时,或者线程没有中断时。与代码块或方法执行时保持对象锁的同步代码不同,锁被保持,直到它的解锁方法被调用。该组中的一些集合类使用这些工具将集合划分为可以单独锁定的部分,从而提高了并发性。例如,`LinkedBlockingQueue` 对队列的头部和尾部分别具有锁定,以便可以并行添加和删除元素。其他使用这些锁的集合包括 `ConcurrentHashMap` 和 `BlockingQueue` 的大部分实现 |
| 138 | + |
| 139 | +迭代器上面描述的机制导致迭代器策略更适合并发使用,而不是快速失败,这隐含地将并发修改视为要消除的问题。写时复制集合具有快照迭代器。这些集合由数组支持,这些数组一旦创建,就永远不会改变;如果集合中的值需要更改,则会创建一个新数组。所以迭代器可以读取其中一个数组中的值(但从不修改它们),而没有被另一个线程更改的危险。快照迭代器不会抛出 `ConcurrentModificationException`。 |
| 140 | + |
| 141 | +上述第三组也具有弱一致的迭代器。 在 `Java 6` 中,这包括 `DelayQueue` 和 `PriorityBlockingQueue`,它们在 `Java 5` 中具有快速迭代器。这意味着,除非这些队列是静态的,否则当不添加或插入元素时,您不能迭代这些队列的 `Java 5` 版本; 在其他时候,你必须使用 `toArray` 将它们的元素复制到一个数组中,然后遍历它。 |
| 142 | + |
| 143 | + |
| 144 | + |
0 commit comments