阿里云自助建站教程,自适应网站如何做移动适配,网站策划书格式及范文,广东智能网站建设配件目录
1、同步容器类
1.1 - 同步容器类的问题
1.2 - 迭代和容器加锁
2、并发容器类
2.1 - ConcurrentHashMap 类
2.2 - CopyOnWriteArrayList 类
3、阻塞队列和生产者-消费者模式
3.1 - 串行线程封闭
4、阻塞方法与中断方法
5、同步工具类
5.1 - 闭锁 - CountDow…目录
1、同步容器类
1.1 - 同步容器类的问题
1.2 - 迭代和容器加锁
2、并发容器类
2.1 - ConcurrentHashMap 类
2.2 - CopyOnWriteArrayList 类
3、阻塞队列和生产者-消费者模式
3.1 - 串行线程封闭
4、阻塞方法与中断方法
5、同步工具类
5.1 - 闭锁 - CountDownLatch
5.2 - 使用 FutureTask 做闭锁
5.3 - 信号量 - Semaphore
5.4 - 栅栏 - Barrier
5.6 - 构建高效且可伸缩的结果缓存 委托是创建线程安全类的一个最有效的策略只需让现有的线程安全类管理所有的状态即可。Java 平台类库包含了丰富的并发基础构建模块例如线程安全的容器类以及各种用于协调多个相互协作的线程控制流的同步工具类(Synchronizer)等。 Java 并发类的 API 文档点击这里。
1、同步容器类 同步容器类包括 Vector 和 Hashtable二者是早期 JDK 的一部分这些同步的封装器类是由 Collections.synchronizedXxx 等工厂方法创建的。这些类实现线程安全的方式是将它们的状态封装起来并对每个公有方法都进行同步使得每次只有一个线程能访问容器的状态
1.1 - 同步容器类的问题 同步容器类都是线程安全的但在某些情况下的复合操作也需要额外的客户端加锁来保护。容器上常见的复合操作包括迭代(反复访问元素直到遍历完容器中所有元素)、跳转(根据指定顺序找到当前元素的下一个元素)以及条件运算例如 “若没有则添加” (检查在 Map 中是否存在键值 K如果没有就加入二元组(KV))。//目前的很多并发容器也没有完全解决复合操作的问题此外同步容器的加锁粒度比较大
1.2 - 迭代和容器加锁 在迭代期间对容器进行加锁。如果容器的规模很大或者在每个元素上执行操作的时间很长那么这些线程将长时间等待。即使不存在饥饿或者死锁等风险长时间地对容器加锁也会降低程序的可伸缩性。持有锁的时间越长那么在锁上的竞争就可能越激烈如果许多线程都在等待锁被释放那么将极大地降低吞吐量和 CPU 的利用率。//对整个容器加锁的弊端 如果不希望在迭代期间对容器加锁那么一种替代方法就是“克隆”容器并在副本上进行迭代。由于副本被封闭在线程内因此其他线程不会在选代期间对其进行修改这样就避免抛出 ConcurrentModificationException (在克隆过程中仍然需要对容器加锁)。在克隆容器时存在显著的性能开销。这种方式的好坏取决于多个因素包括容器的大小在每个元素上执行的工作迭代操作相对于容器其他操作的调用频率以及在响应时间和吞吐量等方面的需求。//创建副本/快照对容器进行迭代是一种不错的思路
2、并发容器类 Java 5.0 提供了多种并发容器类来改进同步容器的性能。同步容器将所有对容器状态的访向都串行化以实现它们的线程安全性。这种方法的代价是严重降低并发性当多个线程竞争 容器的锁时吞叶量将严重减低。//同步容器类性能低 另一方面并发容器是针对多个线程并发访问设计的。在 Java 5.0 中增加了 ConcurrentHashMap用来替代同步且基于散列的 Map以及 CopyOnWriteArrayList用于在遍历操作为主要操作的情况下代替同步的 List。在新的 ConcurrentMap 接口中增加了对一些常见复合操作的支持例如“若没有则添加”、替换以及有条件删除等。//学习并发容器类重要的是操作和使用 通过并发容器来代替同步容器可以极大地提高伸缩性并降低风险。 2.1 - ConcurrentHashMap 类 与 HashMap 一样ConcurrentHashMap 也是一个基于散列的 Map但它使用了一种完全不同的加锁策略来提供更高的并发性和伸缩性。ConcurrentHashMap 并不是将每个方法都在同一个锁上同步并使得每次只能有一个线程访向容器而是使用一种粒度更细的加锁机制来实现更大程度的共享这种机制称为分段锁 (Lock Striping)。在这种机制中任意数量的读取线程可以并发地访问 Map执行读取操作的线程和执行写入操作的线程可以并发地访问 Map并且一定数量的写入线程可以并发地修改 Map。ConcurrentHashMap 带来的结果是在并发访问环境下将实现更高的吞吐量而在单线程环境中只损失非常小的性能。//与HashTable相比具有更好的并发性能 ConcurrentHashMap 与其他并发容器一起增强了同步容器类它们提供的选代器不会抛出ConcurrentModificationException因此不需要在选代过程中对容器加锁。ConcurrentHashMap 返回的选代器具有弱一致性 (Weakly Consistent)而并非“及时失败”。弱一致性的选代器可以容忍并发的修改当创建迭代器时会遍历已有的元素并可以(但是不保证)在选代器被构 造后将修改操作反映给容器。//只能反映暂时状态
2.2 - CopyOnWriteArrayList 类 CopyOnWriteArrayList 用于替代同步 List在某些情况下它提供了更好的并发性能并且在迭代期间不需要对容器进行加锁或复制。(类似地CopyOnWriteArraySet 的作用是替代同步 Set) “写入时复制(Copy-0n-Write)”容器的线程安全性在于只要正确地发布一个事实不可变的对象那么在访问该对象时就不再需要进一步的同步。在每次修改时都会创建并重新发布一个新的容器副本从而实现可变性。“写入时复制”容器的迭代器保留一个指向底层基础数组的引用这个数组当前位于迭代器的起始位置由于它不会被修改因此在对其进行同步时只需确保数组内容的可见性。因此多个线程可以同时对这个容器进行迭代而不会彼此干扰或者与修改容器的线程相互干扰。“写人时复制”容器返回的迭代器不会抛出ConcurrentModificationException并且返回的元素与迭代器创建时的元素完全一致而不必考虑之后修改操作所带来的影响。
3、阻塞队列和生产者-消费者模式 阻塞队列支持生产者 - 消费者这种设计模式。该模式将“找出需要完成的工作”与“执行工作”这两个过程分离开来并把工作项放人一个“待完成”列表中以便在随后处理而不是找出后立即处理。生产者 - 消费者模式能简化开发过程因为它消除了生产者类和消费者类之间的代码依赖性此外该模式还将生产数据的过程与使用数据的过程解耦开来以简化工作负载的管理因为这两个过程在处理数据的速率上有所不同。//BlockingQueue 在构建高可靠的应用程序时有界队列是一种强大的资源管理工具它们能抑制并防止产生过多的工作项使应用程序在负荷过载的情况下变得更加健壮。 Java 中的阻塞队列 3.1 - 串行线程封闭 在 java.utilconcurrent 中实现的各种阻塞队列都包含了足够的内部同步机制从而安全地将对象从生产者线程发布到消费者线程。 对于可变对象生产者 -消费者这种设计与阻塞队列一起促进了串行线程封闭从而将对象所有权从生产者交付给消费者。线程封闭对象只能由单个线程拥有但可以通过安全地发布该对象来“转移”所有权。在转移所有权后也只有另一个线程能获得这个对象的访问权限并且发布对象的线程不会再访问它。这种安全的发布确保了对象状态对于新的所有者来说是可见的并且由于最初的所有者不会再访问它因此对象将被封闭在新的线程中。新的所有者线程可以对该对象做任意修改因为它具有独占的访问权。 对象池利用了串行线程封闭将对象“借给”一个请求线程。只要对象池包含足够的内部同步来安全地发布池中的对象并且只要客户代码本身不会发布池中的对象或者在将对象返回给对象池后就不再使用它那么就可以安全地在线程之间传递所有权。//线程封闭就是在封闭对象上始终只有一个线程进行操作串行化
4、阻塞方法与中断方法 Thread 提供了 interrupt 方法用于中断线程或者查询线程是否已经被中断。每个线程都有一个布尔类型的属性表示线程的中断状态当中断线程时将设置这个状态。 中断是一种协作机制。一个线程不能强制其他线程停止正在执行的操作而去执行其他的操作。当线程 A 中断 B 时A 仅仅是要求 B 在执行到某个可以暂停的地方停止正在执行的操作一一前提是如果线程 B 愿意停止下来。虽然在 API 或者语言规范中并没有为中断定义任何特定应用级别的语义但最常使用中断的情况就是取消某个操作。方法对中断请求的响应度越高就越容易及时取消那些执行时间很长的操作。 当在代码中调用了一个将抛出 InterruptedException 异常的方法时你自己的方法也就变成了一个阻塞方法并且必须要处理对中断的响应。对于库代码来说有两种基本选择//处理中断的策略
传递 InterruptedException。避开这个异常通常是最明智的策略只需把 InterruptedException 传递给方法的调用者。传递 IterruptedException 的方法包括根本不捕获该异常或者捕获该异常然后在执行某种简单的清理工作后再次抛出这个异常。恢复中断。有时候不能抛出InterruptedException例如当代码是 Runnable 的一部分时在这些情况下必须捕获 InterruptedException并通过调用当前线程上的 interrupt 方法恢复中断状态这样在调用栈中更高层的代码将看到引发了一个中断。
5、同步工具类 所有的同步工具类都包含一些特定的结构化属性它们封装了一些状态这些状态将决定执行同步工具类的线程是继续执行还是等待此外还提供了一些方法对状态进行操作以及另一些方法用于高效地等待同步工具类进入到预期状态。 Java 中提供的一些同步工具类 5.1 - 闭锁 - CountDownLatch 闭锁是一种同步工具类可以延迟线程的进度直到其到达终止状态。闭锁的作用相当于一扇门在闭锁到达结束状态之前这扇门一直是关闭的并且没有任何线程能通过当到达结束状态时这扇门会打开并允许所有的线程通过。当闭锁到达结束状态后将不会再改变状态因此这扇门将永远保持打开状态。闭锁可以用来确保某些活动直到其他活动都完成后才继续执行。//闭锁一旦打开就不再关闭- CountDownLatch CountDownLatch 是一种灵活的闭锁实现可以在上述各种情况中使用它可以使一个或多个线程等待一组事件发生。闭锁状态包括一个计数器该计数器被初始化为一个正数表示需要等待的事件数量。countDown 方法递减计数器表示有一个事件已经发生了而 await 方法等待计数器达到零这表示所有需要等待的事件都已经发生。如果计数器的值非零那么 await 会一直阻塞直到计数器为零或者等待中的线程中断或者等待超时。//闭锁的实现原理内置事件计数器
5.2 - 使用 FutureTask 做闭锁 FutureTask 也可以用做闭锁。(FutureTask 实现了 Future 语义表示一种抽象的可生成结果的计算)。FutureTask 表示的计算是通过 Callable 来实现的相当于一种可生成结果的 Runnable并且可以处于以下 3 种状态等待运行 (Waiting to run)正在运行(Running)和运行完成(Completed)。“执行完成”表示计算的所有可能结束方式包括正常结束、由于取消而结束和由于异常而结束等。当FutureTask 进入完成状态后它会永远停止在这个状态上。//FutureTask表示可生成结果的计算 Future.get 的行为取决于任务的状态。如果任务已经完成那么 get 会立即返回结果否则get 将阻塞直到任务进入完成状态然后返回结果或者抛出异常。FutureTask 将计算结果从执行计算的线程传递到获取这个结果的线程而 FutureTask 的规范确保了这种传递过程能实现结果的安全发布。//将计算和获取结果进行分离获取结果的过程优点类似于从阻塞队列中获取数据 下列代码中Preloader 就使用了 FutureTask 来执行一个高开销的计算并且计算结果将在稍后使用。通过提前启动计算可以减少在等待结果时需要的时间。
public class Preloader {ProductInfo loadProductInfo() throws DataLoadException {return null;}private final FutureTaskProductInfo future new FutureTask(() - loadProductInfo());private final Thread thread new Thread(future);public void start() {//执行任务thread.start();}public ProductInfo get() throws DataLoadException, InterruptedException {try {//获取结果return future.get();} catch (ExecutionException e) {Throwable cause e.getCause();if (cause instanceof DataLoadException) {throw (DataLoadException) cause;} else {throw LaunderThrowable.launderThrowable(cause);}}}interface ProductInfo {}
}class DataLoadException extends Exception {}
5.3 - 信号量 - Semaphore 计数信号量(Counting Semaphore) 用来控制同时访问某个特定资源的操作数量或者同时执行某个指定操作的数量。计数信号量还可以用来实现某种资源池或者对容器施加边界。 Semaphore 中管理着一组虚拟的许可 (permit)许可的初始数量可通过构造函数来指定。在执行操作时可以首先获得许可(只要还有剩余的许可)并在使用以后释放许可。如果没有许可那么 acquire 将阻塞直到有许可(或者直到被中断或者操作超时)。release 方法将返回一个许可给信号量。计算信号量的一种简化形式是二值信号量即初始值为 1 的 Semaphore。二值信号量可以用做互斥体(mutex)并具备不可重人的加锁语义谁拥有这个唯一的许可谁就拥有了互斥锁。//信号量不可重入所以信号量不能用来替换可重入锁
5.4 - 栅栏 - Barrier 我们已经看到通过闭锁来启动一组相关的操作或者等待一组相关的操作结束。闭锁是一次性对象一旦进入终止状态就不能被重置。 栅栏(Barrier)类似于闭锁它能阻塞一组线程直到某个事件发生。栅栏与闭锁的关键区别在于所有线程必须同时到达栅栏位置才能继续执行。闭锁用于等待事件而栅栏用于等待其他线程。//栅栏等待线程闭锁等待事件事件具有一次性 CyclicBarrier 可以使一定数量的参与方反复地在栅栏位置汇集它在并行选代算法中非常有用这种算法通常将一个问题拆分成一系列相互独立的子问题。当线程到达栅栏位置时将调用 await 方法这个方法将阻塞直到所有线程都到达栅栏位置。如果所有线程都到达了栅栏位置那么栅栏将打开此时所有线程都被释放而栅栏将被重置以便下次使用。//栅栏可重用 另一种形式的栅栏是 Exchanger它是一种两方 (Two-Party栅栏各方在栅栏位置上交换数据。当两方执行不对称的操作时Exchanger 会非常有用例如当一个线程向缓冲区写入数据而另一个线程从缓冲区中读取数据。这些线程可以使用 Exchanger 来汇合并将满的缓冲区与空的缓冲区交换。当两个线程通过 Exchanger 交换对象时这种交换就把这两个对象安全地发布给另一方。//用来处理数据交换
5.6 - 构建高效且可伸缩的结果缓存 几乎所有的服务器应用程序都会使用某种形式的缓存。重用之前的计算结果能降低延迟提高吞吐量但却需要消耗更多的内存。//用空间换时间 像许多“重复发明的轮子”一样缓存看上去都非常简单。然而简单的缓存可能会将性能瓶颈转变成可伸缩性瓶颈即使缓存是用于提升单线程的性能。 下边是一个高效且可伸缩的缓存的开发示例代码用于改进一个高计算开销的函数。该示例具有很强的参考价值
public class Memoizer A, V implements ComputableA, V {//使用ConcurrentHashMap缓存计算结果提升性能//使用ConcurrentMapA, FutureV而不是ConcurrentMapA, V保存计算结果避免重复计算private final ConcurrentMapA, FutureV cache new ConcurrentHashMap();private final ComputableA, V c;public Memoizer(ComputableA, V c) {this.c c;}public V compute(final A arg) throws InterruptedException {while (true) {//1-计算时先获取如果已经有线程计算那么就阻塞等待获取前一个线程的计算结果FutureV f cache.get(arg);if (f null) {//2-封装计算任务若没有则添加CallableV eval () - c.compute(arg);FutureTaskV ft new FutureTaskV(eval);f cache.putIfAbsent(arg, ft); //原子性-若没有则添加if (f null) {f ft;ft.run(); //3-执行任务}}try {//4-获取计算结果可能会抛出异常需对异常进行处理return f.get();} catch (CancellationException e) {//5-如果执行被取消需要移除计算任务给其他线程计算机会cache.remove(arg, f);} catch (ExecutionException e) {//6-执行异常直接抛出throw LaunderThrowable.launderThrowable(e.getCause());}}}
}interface Computable A, V {V compute(A arg) throws InterruptedException;
}class ExpensiveFunction implements ComputableString, BigInteger {public BigInteger compute(String arg) {return new BigInteger(arg);}
} 注意Memoizer 没有解决缓存逾期的问题但它可以通过使用 FutureTask 的子类来解决在子类中为每个结果指定个逾期时间并定期扫描缓存中逾期的元素。同样它也没有解决缓存清理的问题即移除旧的计算结果以便为新的计算结果腾出空间从而使缓存不会消耗过多的内存。//使用缓存应该考虑缓存的失效问题(存储时间) 至此全文结束。