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

《Java并发编程实战》读书笔记-第5章 基础构建模块

2017-06-12 11:57 357 查看
第五章,基础构建模块

1,同步容器类。

Vector、HashTable此类的容器是同步容器。但也有一些问题,例如,一个线程在使用Vector的size()方法进行循环每一个元素的时候,而另一个线程对Vector的元素进行删除时,可能会发生ArrayIndexOutOfBoundsException。

如果要避免这个问题,可以在调用Vector进行循环的地方,对Vector实例加锁,但效率非常差。

synchronized (vector) {
for(int i=0; i < vector.size(); i++)
doSomethng(vector.get(i));
}


2,迭代器与ConcurrentModificationException

Vector中有的同步问题,在许多现代的容器类中也有有类似的问题。当发现容器在迭代过程中被修改时,就会抛出一个ConcurrentModificationException。无论直接迭代还是使用for-each循环,对容器类进行迭代的标准方式都是使用Iterator,这种机制的实现方式是将计数器的变化和容器关联起来,如果迭代期间计数器被修改,那么hasNext或next方法就会抛出异常。

  如果不想抛出这种异常,就要在所有使用容器的地方对容器加锁,或是在迭代一个克隆的容器。但迭代克隆容器的方法需要考虑性能问题。

3,隐藏的迭代器

public void printSet() {
System.out.pringln(mySet);
}


上面的代码中打印
mySet
的内容,其中
System.out.pringln(mySet)
这条语句是对
mySet
的一个隐藏的迭代,如果在这个迭代过程,有其它方法对
mySet
进行删除的话,就会抛出ConcurrentModificationException。如果使用的是同步的
Set
的话,就会避免这个问题。隐藏的迭代的方法还有hashCode, equals, containAll, removeAll, retainAll。

4,并发容器

ConcurrentHashMap:

提供的迭代器不会抛出ConcurrentModificationException,因此不需要在迭代过程对容器加锁。它有“弱一致性”,弱一致性的迭代器可以容忍并发的修改,当创建迭代器时会遍历已有的元素,并可以(但不保证)在迭代器被构造后将修改操作反映给容器。

isSize和isEmpty的语义被减弱了,以反映容器的并发特性。事实上,isSize和isEmpty在并发环境下用处很小。

没有实现对Map加锁,以及提供独占访问。在HashTable和synchronizedMap中,获得Map的锁能防止其它线程访问这个Map。在一些不常见的情况中需要使用这种功能,例如:通过原子方式添加一些映射,或者对Map迭代并在此期间保持元素顺序相同。只有当应用程序需要
加锁Map
独占访问
时,才应该放弃使用ConcurrentHashMap。

如果需要“若没有则添加”、“若相等则移除”、“若相等则替换”等原子操作时,就要考虑使用ConcurrentHashMap了。

CopyOnWriteArrayList:

这个容器的线程安全性在于,只要正确地发布一个事实不可变的对象,那么在访问该对象时就不再需要进一步同步。

这个容器不会抛出ConcurrentModificationException,并且返回的元素与迭代器创建时元素完全一致,而不必考虑之后修改带的影响。

BlockingQueue:适合生产者和消费者模式

Deque和BlockingDeque:

他们分别对Queue和BlockingQueue进行扩展。Deque是一个双端队列,实现了在队列头和队列尾的高效插入和移除。

双端队列适用于另一种相关模式:工作密取(Work Stealing)。在工作密取设计中,每个消费者都有各自的双端队列。如果一个消费者完成了自己双端队列中的全部工作,那以它可以从其它消费者双端队列末尾秘密地获取工作。密取工作模式比传统的生产者-消费者模式具有更高的可伸缩性,这是因为工作者线程不会在单个共享的任务队列上发生竞争。大多数时候,它们只是访问自己的双端队列,从而极大地减少了竞争。当工作者线程需要访问另一个队列时,它会从队列的尾部而不是从头部获取工作,因此进一步降低了队列上的竞争程序。

5,同步工具类

同步工具类可以是任何一个对象,只要它根据其自身的状态来协调线程的控制流。阻塞队列可以作为同步工具类,其它类型的同步工具类还包括信号量,栅栏以及闭锁。

6,闭锁。

闭锁是一个同步工具类,可以延迟线程的进度直到其到达终止状态。简单地说,闭锁可以用来确保某些活动直到其它活动都完成后才继续执行。例如:

确保某个计算在其需要的所有资源都被初始化之后才继续执行。

确保某个服务在其依赖的所有其他服务都已经启动之后才启动。

等待直到某个操作的所有参与者都就绪再继续执行。

CountDownLatch是一种灵活的闭锁实现。可以在以上的情况中使用,它可以使一个或多个线程等待一组事件发生。

7,FutureTask

FutureTask也可用作闭锁。FutureTask.get的行为取决于任务的状态。如果已经完成,那么get会立即返回结果,否则将阻塞直到任务进入完成状态。

8,信号量

  计数信号量用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量,当达到指定数量呀资源后,就阻塞住,直到有资源被释放掉并可用后,才能继续执行。计数信号量还可以用来实现某种资源池,或者对容器加边界。

  Semaphore是信号量的一个实现。你可以使用Semaphore将任何一种容器变成有界阻塞容器。

  

注意:

只有一个资源的Semaphore和Lock非常像,但有一点区别。锁有“可重入锁”,但Semaphore没有这个概念。例子如下:

当你一个类的所有方法都是
Synchronized
的话,在一个
Synchronized
方法里面,可以进入到另一个
Synchronized
方法里面。他们都使用的是同一把锁,可以进入使用这把锁的地方。

如果是使用“只有一个资源”的Semaphore来实现的话,每一个方法开始都要取得一个资源的话,在一个方法进入到另一个方法后,就执行不下去了。因为两个方法的调用,使用了两个资源。

9,栅栏

栅栏类似于闭锁,它能阻塞一组线程直到某个事件发生。栅栏与闭锁的关键区别在于,所有的线程必须同时到达栅栏才能继续执行。闭锁用于等待事件,而栅栏用于等待其它线程。

10,构建高效且可伸缩的结果缓存(例子)

当缓存里已经存在结果,就从缓存里取。如果缓存里的结果还没有计算完成,就等待计算完成(计算可能花费时间比较长,所以做成异步)。

这个例子有几个问题:

没有控制启动线程的数量。如果想要控制的话,可以使用信号量控制,或者使用线程池体系(Executors)

当缓存的是 Future 而不是值时,将导致缓存污染的问题:如果某个计算取消或者失败,那么在计算这个结果时将指明计算过程被取消或者失败。为了避免这种情况,如果Memoizer发现计算被取消,那么将把Future从缓存中移除。如果检测到RuntimeException,那么也会移除Future,这样将来的计算才可能成功。

Memorizer同样没有解决缓存逾期的问题,但它可以通过使用FutureTask的子类来解决,在子类中为每个结果指定一个逾期时间,并定期扫描缓存中逾期的元素。(同样,它也没有解决缓存清理的问题,即移除旧的计算结果以便为新的计算结果腾出空间,从而使缓存不会消耗过多的内存)

public class Memoizer <A, V> implements Computable<A, V> {
private final ConcurrentMap<A, Future<V>> cache
= new ConcurrentHashMap<A, Future<V>>();
private final Computable<A, V> c;

public Memoizer(Computable<A, V> c) {
this.c = c;
}

public V compute(final A arg) throws InterruptedException {
while (true) {
Future<V> f = cache.get(arg);
if (f == null) {
Callable<V> eval = new Callable<V>() {
public V call() throws InterruptedException {
return c.compute(arg);
}
};
FutureTask<V> ft = new FutureTask<V>(eval);
f = cache.putIfAbsent(arg, ft);
if (f == null) {
f = ft;
ft.run();
}
}
try {
return f.get();
} catch (CancellationException e) {
cache.remove(arg, f);
} catch (ExecutionException e) {
throw LaunderThrowable.launderThrowable(e.getCause());
}
}
}
}


11,小结

到目前为止,我们已经介绍了许多基础知识。下面这个“并发技巧清单”列举了在第一部分中介绍的主要概念和规则。

可变状态是至关重要的(It’s the mutable state,stupid)。

所有的并发问题都可以归结为如何协调对并发状态的访问。可变状态越少,就越容易确保线程安全性。

尽量将域声明为final类型,除非需要它们是可变的。

不可变对象一定是线程安全的。

不可变对象能极大地降低并发编程的复杂性。它们更为简单而且安全,可以任意共享而无须使用加锁或保护性复制等机制。

封装有助于管理复杂性。

在编写线程安全的程序时,虽然可以将所有的数据都保存在全局变量中,但为什么要这样做?将数据封装在对象中,更易于维持不变性条件:将同步机制封装在对象中,更易于遵循同步策略。

用锁来保护每个可变变量。

当保护同一个不变性条件中的所有变量时,要使用同一个锁。

在执行复合操作期间,要持有锁。

如果从多个线程中访问同一个可变变量时没有同步机制,那么程序会出现问题。

不要故作聪明地推断出不需要使用同步。

在设计过程中考虑线程安全,或者在文档中明确地指出它不是线程安全的。

将同步策略文档化。

12,什么是弱一致性

集合操作是并发操作

不会抛出ConcurrentModificationException异常

they are guaranteed to traverse elements as they existed upon construction exactly once, and may (but are not guaranteed to) reflect any modifications subsequent to construction.(感觉翻译不好)

13,什么是并发(Concurrent),什么是同步(Synchronized)

并发:是线程安全的,但不是通过一个独占锁(single exclusion lock)进行管理控制的.

同步:通过单独锁(single lock)来管理控制各种访问(读写),可扩展性(scalability)低。

14,happens-before

todo
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  读书笔记 java并发