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

第五章,基础构建模块

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值