《Java并发编程实战》读书笔记

2016-08-22
读书笔记
  • Java中的主要同步机制:synchronizedLockvolatile原子变量
  • java.util.concurrent.atomic
  • 内存可见性:读线程可立即看到写线程的写入结果。

volatile

  • 禁止指令重排序:不会将该变量上的操作与其他内存操作一起重排序。
  • 可见性:volatile变量不会缓存在寄存器或其他处理器不可见的地方。
  • volatile不足以确保递增操作(++)的原子性。
  • 加锁机制可以确保可见性及原子性,volatile只能确保可见性。

ThreadLocal(防止共享)

  • ThreadLocal有get、set方法。为每个使用ThreadLocal变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。
  • ThreadLocal常用于防止对可变的单实例变量全局变量进行共享。
    • 如全局数据库链接Connection对象,将链接保存到ThreadLocal中,每个线程都会拥有自己的连接。
  • 将单线程程序移植到多线程,将共享的全局变量转为ThreadLocal,可维持线程安全性。然而,禁止的这些变量的共享,在业务上可能就不满足需求了。
  • ThreadLocal可视为Map,其中保存了特定于线程的值。但真实的实现并非如此,这些特定于线程的值保存在Thread对象中,当线程结束,这些值会被gc。
  • 在实现应用程序框架时大量使用了ThreadLocal。如在EJB调用期间,J2EE容器需将一个事务上下文(Transaction Context)与某执行线程关联。将事务上下文保存在ThreadLocal。
  • ThreadLocal类似于全局变量,降低代码可重用性,并在类间引入隐含的耦合性,因此在使用时要格外小心。

不变性

  • 不可变对象一定是线程安全的。
  • 对象中所有域均final,对象也可能是可变的。因为final域中可以保存对可变对象的引用。

基础构建模块

同步容器类

  • Vector、HashTable、Collections.synchronizedXxx
  • 将类中的状态封装起来,并对每个public方法进行同步,每次仅有一个线程可访问。
同步容器类的问题
  • 同步容器类均线程安全,但某些情况需额外的客户端加锁来保护复合操作。如Vector迭代
迭代器与ConcurrentModificationException
  • 对容器类进行迭代的标准方式都是使用Iterator,但若有其他线程并发修改容器,使用迭代器也无法避免在迭代期间对容器加锁。
  • 若不想在迭代期间对容器加锁,可“克隆”一个副本进行迭代。但克隆过程本身需加锁。克隆存在显著的性能开销。
隐藏迭代器
  • containsAll、removeAll、retainAll

并发容器

  • 同步容器为串行访问,吞吐量低。使用并发容器可极大提高伸缩性并降低风险。
  • ConcurrentHashMap增加了对一些常见复合操作的支持,如“若没有则添加”、替换、有条件删除等。
  • CopyOnWriteArrayList用于在遍历操作为主要操作的情况下代替同步List。
  • ConcurrentSkipListMap代替同步的SortedMap(如synchronizedMap包装的TreeMap)
  • ConcurrentSkipListSet代替同步的SortedSet(如synchronizedMap包装的TreeSet)
  • Queue用来临时保存一组待处理元素,ConcurrentLinkedQueue先进先出,PriorityQueue为非并发的优先队列。
  • Queue是通过LinkedList实现的,去掉了List的随机访问需求,实现高效并发。
  • BlockingQueue
ConcurrentHashMap
  • ConcurrentHashMap使用了分段锁。
  • ConcurrentHashMap的迭代器不会抛出CoucurrentModificationException,不需在迭代过程中对容器加锁
  • HashTable、synchronizedMap,对Map加锁并独占访问。
  • “若没有则添加”、“若相等则删除”、“若相等则替换”均为原子操作。
CopyOnWriteArrayList
  • CopyOnWriteArrayList迭代期间不需对容器加锁、复制(修改时需复制)。
  • 当修改容器时会复制底层数组,这需要一定开销。
  • 仅当迭代操作远多于修改操作时使用。

同步工具类

闭锁
  • CountDownLatch为一种闭锁实现,使一个或多个线程等待一组事件发生。countDown方法递减计数器,await方法等待计数器达到0。
  • 闭锁为一次性对象,一旦进入终止状态,就不能被重置。
  • 闭锁拦截所有线程,直到某个状态满足条件。
栅栏Barrier
  • 闭锁用于等待事件(满足某结束状态),栅栏用于等待其他线程。
信号量Semaphore
  • 计数信号量用于控制同时访问某特定资源的操作数,或同时执行某指定操作的数量。
  • Semaphore管理一组虚拟许可(permit),执行操作时首先获得许可,使用后释放许可。
  • acquire获得许可,release释放许可。
FutureTask
  • FutureTask表示一种可生成结果的计算,计算是通过Callable实现(相当于可生成结果的Runnable)。
  • Future.get获取结果,若任务未完成,则一直阻塞直到完成后返回结果。

阻塞队列和生产者-消费者

  • 阻塞队列的put、take可阻塞。无界队列永不会满,无界队列上的put永远不会阻塞。
  • BlockingQueue简化了生产者-消费者的实现,支持任意数量的生产者和消费者。
  • 如果阻塞队列不能完全符合设计需求,还可以通过信号量(Semaphore)来创建其他的阻塞数据结构。
  • ArrayBlockingQueue、LinkedBlockingQueue为FIFO队列,比同步List拥有更好并发性。
  • PriorityBlockingQueue既可根据元素的自然序比较元素(实现了Comparable方法),也可用Comparator来比较。
  • io密集型与cpu密集型的任务,使用生产者-消费者,提高吞吐率。
  • Java增加了Deque和BlockingDeque对Queue、BlockingQueue进行扩展。Deque为双端队列,实现了在队列头、尾的高效插入、移除。具体实现包括ArrayDeque、LinkedBlockingDeque。
  • 阻塞队列用于生产者-消费者,双端队列用于工作密取模式。生产者-消费者,所有消费者共享一个工作队列,而工作密取的每个消费者有自己的双端队列。

阻塞方法和中断方法

  • 线程A中断B时,A仅仅是要求B在执行到某个可以暂停的地方停止正在执行的操作-前提是B愿意停下来。

构建高效且可伸缩的结果缓存

  • ConcurrentHashMap存储缓存数据
  • Future
  • FutureTask
  • Callable

任务执行

在线程中执行任务

  • 大多数服务器应用程序都提供了一种自然的任务边界选择方式:以独立的客户请求为边界。如Web服务器、EJB容器、邮件服务器、文件服务器、数据库服务器。
  • 串行地执行任务:性能太差,每次只处理1个请求。
  • 显式地为任务创建线程:为每个请求创建一个新线程。
  • 无限制创建线程的不足
    • 线程生命周期的开销非常高
      • 线程的创建与销毁是有代价的。
      • 若请求到达率非常高且请求处理过程是轻量级的,如大多数服务器应用,为每个请求创建一个线程将消耗大量计算资源。
    • 资源消耗
      • 活跃的线程会消耗资源,特别是内存。若可运行线程多于cpu核数,有些线程将闲置。闲置线程占用内存较多。
      • 若已有足够多的线程使cpu保持忙碌,创建更多线程将降低性能。
    • 稳定性
      • 可创建线程数有上限。与平台、jvm启动参数、Thread构造函数中请求的栈大小、底层操作系统对线程的限制等有关。

Executor框架

  • 在Java类库中,任务执行的主要抽象不是Thread,而是Executor。
  • Executor基于生产者-消费者,提前任务相当于生产者,执行任务相当于消费者。

    public interface Executor {
        void execute(Runnable command);
    }
    
示例:基于Executor的Web服务器
  • private static final Executor exec = Executors.newFixedThreadPool(THREADS_NUM);
  • ServerSocket socket = new ServerSocket(80);
  • final Socket connection = socket.accept();Runnable task = new Runnable(){connection};
  • exec.execute(task);
执行策略
  • 将任务的提交与执行解耦。
线程池
  • 重用现有线程而不是创建新线程,可在处理多个请求时分摊在线程创建与销毁过程中产生的巨大开销。
  • 可通过Executors中静态工厂方法之一来创建一个线程池。
    • newFixedThreadPool
      • 固定大小
      • 每次提交任务创建新线程直到达到线程池最大数量。
    • newCachedThreadPool
      • 可缓存线程池
      • 无大小
      • 若线程数量多于处理需求,则回收
    • newSingleThreadPool
      • 单线程
      • 串行执行任务队列中的任务
    • newScheduledThreadPool
      • 固定大小
      • 以延迟或定时的方式来执行任务
Executor的生命周期
  • Executor通常会创建线程来执行任务。但JVM只有在所有(非守护)线程全部终止后才退出。如无法正确关闭Executor,JVM将无法结束。
  • ExecutorService接口扩展了Executor接口,增加了用于生命周期管理的方法。
    • shutdown:平缓关闭,不再接受新任务,等待正在执行及待执行任务执行完毕。
    • shutdownNow:粗暴关闭,尝试取消正在执行的所有任务,不再启动待执行任务。
延迟任务与周期任务
  • Timer类负责管理延迟任务与周期任务。但Timer有缺陷,使用ScheduledThreadPoolExecutor代替。

找出可利用的并行性

示例:串行的页面渲染器
携带结果的任务Callable与Future
示例:使用Future实现页面渲染器
在异构任务并行化中存在的局限
CompletionService:Executor与BlockingQueue
示例:使用CompletionService实现页面渲染器
为任务设置时限
示例:旅行预订门户网站

取消与关闭


线程池的使用

  • 只有当线程本地值的生命周期受限于任务的生命周期时,在线程池的线程中使用ThreadLocal才有意义,而在线程池的线程中不应该使用ThreadLocal在任务间传递值
  • 只有当任务是同类型且相互独立时,线程池的性能才能达到最佳。若将运行时间较长与较短的任务混在一起,可能“拥塞”,若任务依赖于其他任务,可能死锁。

设置线程池大小

  • 代码中通常不会固定线程池大小,而应通过某动态机制来提供,或Runtime.availableProcessors来动态计算。
  • 线程池大小:cpu数、内存大小、计算密集还是IO密集、是否需JDBC这样的稀缺资源。
  • 计算密集型:线程数=CPU数+1。(当线程偶尔由于页缺失故障或其他原因暂停,额外的+1线程可确保CPU时钟周期不会浪费)
  • IO密集型:线程数比计算密集型更大。
  • 数据库连接池与任务线程池:相互影响。

ThreadPoolExecutor

  • 若默认执行策略(newCachedThreadPool、newFixedThreadPool、newScheduledThreadExecutor)不满足需求,可使用构造方法自定义一个对象。

    public ThreadPoolExecutor(int corePoolSize,
                                 int maximumPoolSize,
                                 long keepAliveTime,
                                 TimeUnit unit,
                                 BlockingQueue<Runnable> workQueue,
                                 ThreadFactory threadFactory,
                                RejectedExecutionHandler handler
                                 ){}
    
    • corePoolSize:线程池最小大小。
    • maximumPoolSize:最大值
    • keepAliveTime:空闲线程存活时间,超出后线程可被回收,但不一定立即回收。
  • newCachedThreadPool线程数量0-Integer.MAX_VALUE,超时时间1分钟,自动扩展与收缩。
  • newFixedThreadPool、newSingleThreadExecutor默认使用无界LinkedBlockingQueue。
  • 使用有界队列ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue有助于避免资源耗尽。

图形用户界面应用程序


避免活跃性危险


性能与可伸缩性


并发程序的测试


显示锁

  • ReentrantLock可重入,必须在finally中释放锁
  • Lock lock = new ReentrantLock();lock.lock();try{}finally{lock.unlock();}
  • 可定时与可轮询的锁获取模式是由tryLock()方法实现的。
  • ReadWriteLock允许多个读操作同时进行,但每次只允许一个写操作。
    • 当写操作时,其他线程无法读、写数据,而当读操作时,其它线程无法写数据,但可读数据

构建自定义的同步工具


原子变量与非阻塞同步机制


Java内存模型

  • 在共享内存的多处理器体系架构中,每个处理器都拥有自己的缓存,并定期与主内存进行协调。


Kommentare: