读《java并发编程实战》

一. 这本书在谈什么? 《java并发编程实战》是一本讲解多线程并发变成实践的书,书中详细阐述了线程安全出现的原因,结合场景分析了什么情况下能够避免安全问题,以及讲解了通过java的concurrent包提供的多种解决线程安全问题的工具,包括如synchronized,Executor,Atomic,ConcurrentHashMap,CountDownLatch。书中还提供了很多高级的线程安全的用法,没有再继续读下去,后面有空再继续研读把。
二. 这本书细节部分都讲了什么? 1. 线程简介

  1. 线程可以理解为一种轻量级的进程
  2. 线程带来性能的提升以外,也带来了风险
  3. 多线程并发执行,因为线程执行的先后顺序不一定,会产生竞态条件,尤其是代码编译后语句执行顺序和代码编写顺序不一致。
  4. 当某个操作无法继续执行下去时,就会产生活跃性问题。比如死锁。
  5. 线程使用时间片和退出时间片都会出现上下文切换,这样也会带来开销。
2. 线程安全性
  1. 编写线程安全的代码,核心是对共享的和可变的状态的访问。
  2. 一个对象是否是线程安全的,要看它是否被多个线程访问。
  3. 多个线程访问某个状态变量并且有一个线程执行写入操作时,必须采用同步机制来协同这些线程对变量的访问。
对多线程访问一个可变的状态变量时,可以使用以下三种方法修复:
  • 不在线程之间共享该状态变量
  • 将状态变量修改为不可变的变量
  • 在访问状态变量时使用同步
  1. 对于多线程的使用,正确的方法是:首先使代码正确运行,然后再提高代码的速度。
  2. 无状态对象是线程安全的,比如servlet。
  3. java.util.concurrent.atomic包中包含了一些原子变量类。
  4. 要保持状态一致性,就需要在单个原子操作中更新所有相关的状态变量。
  5. 同步代码块Synchronized Block,线程会在进入同步代码块时自动获得锁,并且在退出同步代码块时自动释放锁。锁是互斥的,最多只有一个线程能持有这种锁。
  6. 每个共享的和可变的变量都应该只有一个锁来保护。
  7. 一种常见的加锁约定为,将所有的可变状态都封装在对象内部。
  8. 通过缩小同步代码块的作用范围,我们可以做到既确保servlet的并发性,同时又维护线程安全性。
  9. 当执行时间较长的计算或者可能无法快速完成的操作时,一定不要持有锁。
3. 对象的共享
  1. “重排序”: 无法确保线程中的操作将按照程序中指定的顺序执行。
  2. 对线程中使用共享的long和double等类型的变量是不安全的。
  3. 为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程必须都在同一个锁上同步。
  4. volatile类型的变量总会返回最新写入的值。volatile是一种比sychronized关键字更轻量级的同步机制。volatile的典型用法是检查某个状态标记以判断是否退出循环。volatile主要是用在一个线程做写入,其他线程只读的时候。
  5. 线程封闭是实现线程安全性最简单的方式之一。
  6. ThreadLocal主要用在:当某个频繁执行的操作需要一个临时对象,同时又希望避免在每次执行时都重新分配该临时对象,就可以使用这项技术。
  7. 不可变对象一定是线程安全的,使用final标记。
  8. 线程安全库中的容器类提供了以下的安全发布保证:
Hashtable、synchronizedMap、ConcurrentMap
vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList、synchronizedSet
BlockingQueue、ConcurrentLinkedQueue
4. 对象的组合
  1. 实例封闭
  2. Collections.unmodifyiableMap(result); 创建一个不可变list
  3. 虽然AtomicInteger是线程安全的,但是经过组合得到的类却并不是。
  4. @GuardedBy注解用来标记对象被持有的锁
5. 基础构建模块
  1. 如果不希望在迭代期间对容器加锁,那么一种替代方法就是“克隆”容器,并在副本上进行迭代。
  2. ConcurrentLinkedQueue是先进先出队列,PriorityQueue是优先级队列,都是线程安全容器。
  3. ConcurrentHashMap并不是将每个方法都在同一个锁上同步并使得每次只能有一个线程访问容器,而是使用一种粒度更细的加锁机制来实现更大程度的共享,这种机制是分段锁。无论在多线程和单线程下都有很好的性能。
  4. 在迭代操作远远多个修改操作时,才应该使用“写入时复制”容器。
  5. 生产者-消费者模式能简化开发过程,因为它消除了生产者类和消费者类之间的代码依赖性。例如BlockingQueue
  6. 有界队列是一种强大的资源管理工具:他们能抑制并防止产生过多的工作项,使应用程序在负荷过载的情况下变成更加健壮。
  7. Deque是一个双端队列,实现包括ArrayDeque和LinkedBlockingDeque。当工作线程需要访问另外一个队列时,它会从队列的尾部而不是从头部获取工作,因此进一步降低了队列上的竞争程度。
  8. 中断是一种协作机制。一个线程不能强制其他线程停止正在执行的操作而去执行其他的操作。当线程A中断B时,A仅仅是要求B在执行到某个可以暂停的地方停止正在执行的操作-前提是如果线程B愿意停止下来。
  9. 闭锁是一种同步工具类,闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何的线程能通过,当到达结束状态时,这扇门会打开并允许所有的线程通过。闭锁可以用来确保某些活动指导其他活动都完成才继续执行。CountDownLatch,FutureTask也可以用来做闭锁。
  10. 计数信号量用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。计数信号量还可以用来实现某种资源池,或者对容器施加边界。Semaphore中管理者一组虚拟的许可。
  11. 栅栏(Barrier)与闭锁的关键区别在于,所有线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待时间,而栅栏用于等待其他线程。CyclicBarrier
6.任务执行
  1. 线程池简化了线程的管理工作。
  2. Executor基于生产者-消费者模式,Executor的创建使用Executors.newFixedThreadPool(number);
  3. Executors中的静态工厂方法之一来创建一个线程池:
newFixedThreadPool:固定长度线程池
newCachedThreadPool:可缓存的线程池,不受任何限制
newSingleThreadExecutor:单线程的线程池,如果线程异常结束也会创建另外一个替代
newScheduledThreadPool:固定长度,延迟或定时执行。代替Timmer
  1. ExecutorService扩展了Executor,添加了一些生命周期管理的方法。
  2. Future表示一个任务的生命周期,并提供了相应的方法来判断是否已经完成或取消。
  3. ExecutorService中的所有submit方法都讲返回一个Future
  4. CompetionService讲Executor和BlockingQueue功能融合在一起,提交后使用completionService.take(); 来获取Future,再用Future的get方法来获取返回。
  5. Future的invokeAll在所有任务执行完毕后,或者调用线程被中断时,又或者超过指定时限时,返回。
7. 取消与关闭
  1. interrupt方法能中断目标线程,而isInterrupted方法能返回目标线程的中断状态。
  2. 调用interrupt并不意味着立刻停止目标线程正在进行的工作,而只是传递了请求中断的消息。通过推迟中断请求的处理,能够制定更灵活的中断策略,从而使应用程序在响应性和健壮性之间实现合理的平衡。
  3. 只有实现了线程中断策略的代码才可以屏蔽中断请求。在常规的任务和库代码中都不应该屏蔽中断请求。
  4. Future可以用来中断线程。
8. 线程池的使用
  1. 线程池的大小为处理器核心数+1,通常能实现最优的利用率。
9. 活跃性危险
  1. 饥饿
  2. 活锁
10. 显式锁
  1. ReentrantLock,它比synchronized危险,因为它需要在finally中关闭,因此优先使用synchronized
三. 这本书跟我有什么关系? 【读《java并发编程实战》】线程的使用属于开发中的高级主题,在平时工作中如果不注意,就会出现线程安全问题。最重要的还是要理解在使用多线程的过程中,应该注意哪些问题,线程安全问题、活跃性问题等,理解出现这些问题的原因,原理,才能避免出现这些问题。另外,要多使用并发编程中提供的一些工具类,synchronized,Executor,Atomic,ConcurrentHashMap,CountDownLatch等,能够进一步减少线程安全问题。

    推荐阅读