Java并发编程实战笔记(二)

第四章

  1. 在设计线程安全类的过程中,需要包含以下三个基本要素:找出够成对象状态的所有变量,找出约束状态变量的不变性条件和建立对象状态的并发访问管理策略。
  2. 包含多个变量的不变性条件将带来原子性需求:这些相关的变量必须在单个原子操作中进行读取或更新。
  3. 如果不了解对象的不变性条件和后验条件,那么就不能保证线程安全性,要满足在状态变量的有效值或状态转换上的各种约束条件,就需要借助原子性和封装性。
  4. 类的不变性条件与后验条件约束了在对象上有哪些状态和状态转换是有效的。在某些对象的方法中还包含一些基于状态的先验条件。
  5. 要想实现某个等待先验条件为真时才执行的操作,一种更简单的方法是通过现有的类库(例如阻塞队列BlockingQueue或信号量Semaphore来实现依赖状态的行为)。
  6. 封装简化了线程安全类的实现过程,它提供了一种实例封闭机制(Instance Confinement),通常也简称为“封闭”。
  7. 通过将封闭机制与合适的加锁策略结合起来,可以确保以线程安全的方式来使用非线程安全的对象。
  8. 将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。
  9. 钝化操作(Passivation):指的是将状态保存到持久性存储。
  10. 一些基本的容器类并非是线程安全的,例如ArrayList和HashMap,但类库提供了包装器工厂方法(例如Collections.synchronizedList及其类似方法),使得这些非线程安全的类可以在多线程环境中安全的使用。这些工厂方法通过“装饰器(Decorator)”模式将容器类封装在一个同步的包装器对象中,而包装器能将接口中的每个方法都实现为同步方法,并将调用请求转发到底层的容器对象上。只要包装器对象拥有对底层容器对象的唯一引用(即把底层容器对象封闭在包装器中),那么它就是线程安全的。在这些方法的Javadoc指出,对底层容器对象的所有访问必须通过包装器来进行。
  11. 封闭机制更易于构造线程安全的类,因为当封闭类的状态时,在分析累的线程安全性时就无须检查整个程序。
  12. 遵循Java监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护。
  13. 在许多类中都使用了Java监视器模式,例如Vector和Hashtable。Java监视器模式的主要优势就在于它的简单性。
  14. 使用私有的锁对象而不是对象的内置锁(或任何其他可通过公有方式访问的锁),有许多有点。私有的锁对象可将锁封装起来,使客户代码无法得到锁,但客户代码可以通过公有方法来访问锁,以便参与到它的同步策略中。
  15. 如果一个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全的发布这个变量。
  16. 要添加一个新的原子操作,最安全的方式是修改原始的类,但这通常无法做到。另一个方法是扩展这个类,即继承这个类,并添加一个新方法,但是并非所有类都像Vector那样将状态向子类公开,因此这个方法也不合适。第三种方式是扩展类的功能,但并不是扩展类本身,而是将扩展代码放入一个辅助类中,即封装类。一种更好的方法是组合(Composition)。例如以下程序中ImprovedList通过将List对象的操作委托给底层的List实例来实现List的操作,同时还添加了一个原子的putIfAbsent方法。
  Public class ImprovedList<T> implements List<T>{
       private final List<T> list;
       public ImprovedList(List<T> list){this.list = list;}
       public synchronized Boolean putIfAbsent(T x){
             boolean contains = list.contains(x);
             if(!contains){
                list.add(x);
             } 
             return !contains;
       }
}
  1. 将同步策略文档化:在文档中说明客户代码需要了解的线程安全性保证,以及代码维护人员需要了解的同步策略。这是尤其重要的!!!!!!
  2. 18.

第五章

  1. 委托是创建线程安全类的一个最有效的策略:只需让现有的线程安全类管理所有的状态即可。
  2. 同步容器类包括Vector和Hashtable,二者是早期JDK的一部分,此外还包括在JDK1.2中添加的一些功能相似的类,这些同步的封装器类是由Collections.synchronizedXxx等工厂方法创建的。这些类实现线程安全的方式是:将它们的状态封装起来,并对每个公有方法都进行同步,使得每次只有一个线程能访问容器的状态。
  3. 尽管同步容器类是线程安全的,但在某些情况下需要额外的客户端加锁来保护复合操作:迭代(反复访问元素,知道遍历完容器中所有元素)、跳转(根据指定顺序找到当前元素的下一个元素)以及条件运算。
    如果一个线程正对共享容器进行迭代,另一线程也正对这个共享容器进行写操作,就很可能发生运行时异常,抛出ConcurrentModificationException。这是个“及时失败”的预警异常
  4. 为保证在复合操作中保证同步容器类的线程安全性,一种方法是在迭代期间加锁保护,另一种方法是“克隆”,并在副本上进行迭代。
  5. 并发容器:ConcurrentHashMap替代同步且基于散列的Map。
    CopyOnWriteArrayList代替同步的List(遍历操作为主的情况下)。
    JDK 5.0新增:Queue和BlockingQueue。
    Queue(非阻塞)提供了几种实现:ConcurrentLinkedQueue,PriorityQueue.
    BlockingQueue(阻塞),在“生产者-消费者”设计模式中,非常有用。
    JDK 6.0引入ConcurrentSkipListMap和ConcurrentSkipListSet,分别作为同步的SortedMap和SortedSet的并发替代品(例如用synchronizedMap包装的TreeMap或TreeSet)。
  6. ConcurrentHashMap:加锁策略(分段锁 Lock Striping),更高的并发性和伸缩性,并非将锁加在每个方法上,而是一种粒度更细的加锁机制,从而实现更大程度的共享。在这种机制下,任意数量的读取线程可以并发地访问Map,执行读取操作的线程和执行写入操作的线程可以并发的访问Map,并且一定数量的写入线程可以并发的修改Map。ConcurrentHashMap带来的结果是,在并发访问环境下将实现更高的吞吐量,而在单线程中只损失非常小的性能。
  7. 并发容器增强了同步容器:它们提供的迭代器不会抛出ConcurrentModificationException,因此不需要在迭代过程中对容器加锁。ConcurrentHashMap返回的迭代器具有弱一致性(Weakly Consistent),而并非“及时失败”。弱一致性的迭代器可以容忍并发的修改,当创建迭代器时会遍历已有的元素,并可以(但是不保证)在迭代器被构造后将修改操作反映给容器。
  8. 与Hashtable和synchronizedMap相比,ConcurrentHashMap有着更多的优势以及更少的劣势,因此在大多数情况下,用ConcurrentHashMap来代替同步Map能进一步提高代码的可伸缩性,只有当应用程序需要加锁Map以进行独占访问时,才应该放弃使用ConcurrentHashMap。
  9. 虽然ConcurrentHashMap不能被加锁来执行独占访问,因此无法使用客户端加锁来创建新的原子操作。但ConcurrentMap的接口中已经声明了例如“若没有则添加(putIfAbsent)”、“若相等则移除(remove)”和“若相等则替换(replace)”等原子操作方法。
  10. CopyOnWriteArrayList:替代同步List,迭代期间不需要对容器加锁或复制。此对象在每次修改时,都会创建并重新发布一个新的容器副本,从而实现可变性。“写入时复制”容器的迭代器保留一个指向底层基础数组的引用,这个数组当前位于迭代器的起始位置,由于它不会被修改,因此在对其进行同步时只需确保数组内容的可见性。因此多个线程可以同时对这个容器进行迭代,而不会彼此干扰或者与修改容器的线程相互干扰。
  11. 因为每当修改此容器都会复制底层数组,需要一定开销,特别是当容器规模较大。所以仅当迭代操作远多于修改操作时,才应该使用“写入时复制”容器。
  12. 阻塞队列和生产者-消费者模式:一种最常见的生产者-消费者设计模式就是线程池与工作队列的组合,在Executor任务执行框架中就体现了这种模式。
  13. 在构建高可靠的应用程序时,有界队列是一种强大的资源管理工具:它们能抑制并防止产生过多的工作项,使应用程序在负荷过载的情况下变得更加健壮。
  14. Java 6增加了两种容器类型,Deque(发音为“deck”)和BlockingDeque,它们分别对Queue和BlockingQueue进行了扩展。Deque是一个双端队列,实现了在队列头和队列尾的高效插入和移除。具体实现包括ArrayDeque和LinkedBlockingDeque。
  15. 双端队列适用于一种相关模式:工作密取(Work Stealing)。在工作密取设计中,每个消费者都有各自的双端队列。如果一个消费者完成了自己双端队列中的全部工作,那么它可以从其他消费者的双端队列末尾秘密地获取工作。
  16. 中断是一种协作机制。一个线程不能强制其他线程停止正在运行的操作而去执行其他操作。当线程A中断线程B时,A仅仅是要求B在执行到某个可以暂停的地方停止正在执行的操作—-前提是线程B愿意停下来。
  17. 当在代码中调用了一个将抛出InterruptedException异常的方法时,你自己的方法也就变成了一个阻塞方法,并且必须处理对中断的响应。对于库代码来说,有两种方式:传递InterruptedException,即不捕获这个异常或捕获之后,执行某种简单的清理工作后再次抛出这个异常。二是恢复中断,捕获异常,调用当前线程的interrupt方法恢复中断状态,这样在调用栈上更高层的代码将看到引发了一个中断。
  18. 不应该在出现中断异常时,捕获它却不做出任何反应。
  19. 阻塞队列(BlockingQueue)可以作为一种作为同步工具类,其他类型的同步工具类还有信号量(Semaphore)、栅栏(Barrier)以及闭锁(Latch)。
  20. 闭锁是一种同步工具类,可以延迟线程的进度直到其到达终止状态。闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程都能通过,当到达结束状态时,这扇门就会打开并允许所有的线程通过。当闭锁到达结束状态后,将不会在改变其状态,因此这扇门将永远保持打开状态。闭锁可以用来确保某些活动直到其他活动都完成后才继续执行。具体实现有CountDownLatch。
  21. FutureTask也可以用作闭锁。它表示的计算是通过Callable来实现的,相当于一种可生成结果的Runnable,并且其Future.get方法是一个阻塞方法。FutureTask在Executor框架中表示异步任务,此外还可以用来表示一些时间较长的计算,这些计算可以在使用计算结果之前启动。
  22. 信号量(Semaphore):用来控制同时访问某个特定资源的操作数量,或者同时执行某个特定操作的数量。(Counting Semaphore)计数信号量可以用来实现某种资源池,或者对容器施加边界。Semaphore中管理着一组虚拟的许可(permit),许可的初始数量可通过构造函数来指定,在执行操作时可以首先获得许可(只有还剩有许可),使用完之后释放许可。如果没许可,那么acquire将阻塞直到有许可,release方法将返回一个许可给信号量。其实,这种实现并不包含真正的许可对象,许可也没有和线程相关联。

第一部分小结

  1. 可变状态是至关重要的。所有的并发问题都可以归纳为如何协调对并发状态的访问。可变状态越少,就越容易确保线程安全性。
  2. 尽量将域声明为final类型,除非需要它们是可变的。
  3. 不可变对象一定是线程安全的。它们极大降低并发编程的复杂性。更为简单和安全,可以任意共享而无须加锁或保护性复制等机制。
  4. 封装有助于管理复杂性。
  5. 用锁来保护每个可变变量。
  6. 当保护同一个不变性条件中的所有变量时,要使用同一个锁。
  7. 在执行复合操作期间,要持有锁。
  8. 如果从多个线程中访问同一个可变变量时没有同步机制,那么程序会出现问题。
  9. 不要故作聪明推断出不需要使用同步。
  10. 在设计过程中考虑线程安全,或者在文档中明确指出他不是线程安全的。
  11. 将同步策略文档化。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值