EffectiveJava第十章:并发

版权声明:本文为博主原创文章,转载请注明出处 https://github.com/baiiu https://blog.csdn.net/u014099894/article/details/68948142

本章的建议可以帮助写出清晰、正确的并发程序。

66. 同步访问共享的可变数据

对数据操作的原子性和可见性要区分清楚,才能明白什么时候使用synchronized、volatile来保持数据被多个线程共享。

  • 同步的全部意义
    如果没有同步,一个线程的变化就不能被其他线程看到。
    同步不仅可以阻止一个线程看到对象处于不一致的状态之中,还可以保证进入同步方法或同步代码块的每个线程,都看到由同一个锁保护的之前所有的修改效果。
    即同步的意义有两个:互斥和可见。

  • 原子性和可见性
    Java语言规范保证读或写一个变量是原子的(atomic),除了long和double。即读取一个非long或double类型的变量,可以保证返回的值是某个线程保存在该变量的。
    但并不保证一个线程写入的值对于另一个线程将是可见的。此时可以使用同步、或volatile关键字。

  • volatile关键字
    volatile关键字不执行互斥访问,仅保证可见性。
    所以当所使用的同步代码块中仅仅是为了保证变量的修改是可见的,而不是为了互斥访问,可以使用该关键字来提高性能。
    即没多条语句操作变量,不需要互斥。

67. 避免过度同步

过度同步可能会导致 性能降低、死锁、甚至不确定的行为。

  • 一个被同步的方法或代码块中,永远不要放弃对客户端的控制。
    即在一个被同步的区域内部,不要调用设计成要被覆盖的方法。从同步区域中,外部提供的方法不知道会做什么事情,也无法控制它,可能会造成异常、死锁或者数据损坏。

  • Java提供的锁是可重入的。
    同一个线程,可以进到同一个锁所保护的正在操作的资源中。可再入锁简化了多线程的面向对象程序的构造,当可能会将活性失败变成安全性失败。

  • 可以通过将外来方法的调用移出同步代码块来解决这个问题。
    也可以使用CopyOnWriteArrayList并发集合,通过重新拷贝整个底层数组,在这里实现所有的写操作。由于内部数组永远不改动,因此迭代不需要锁定,速度也非常快。

  • 在同步区域之外被调用的外来方法被称作开放调用。这种调用方式可以避免死锁,还可以增加并发性。外来方法运行时间可能会很长,如果进行同步访问,其他线程对受保护资源的访问就会遭到不必要的拒绝。

  • 通常,应该在同步区域内做尽可能少的工作。
    获得锁,检查共享数据,根据需要转换数据,然后释放锁。

  • 过度同步的性能损失
    在这个多核的时代,过度同步的实际成本并不是获取锁所花费的CPU时间,而是指失去了并行的机会、以及因为需要确保每个核都有一个一致的内存视图而导致的延迟。
    另一个潜在的开销在于,它会限制VM优化代码执行的能力。

  • 如果一个可变的类要并发使用,应该使这个类变成线程安全的。通过内部同步,你还可以获得明显比从外部锁定整个对象更高的并发性。
    否则,就不要在内部同步,让客户端在必要的时候从外部同步。
    当你不确定时,就不要同步你的类,提供文档,并清楚的说明它不是线程安全的。

总之,为了避免死锁和数据破坏,千万不要从不同区域内部调用外来方法。更一般的,要尽量限制同步区域内的工作量。
当设计一个可变类时,考虑一下它们是否应该自己完成同步操作。当有足够理由一定要在内部同步类的时候,才应该这么做,并写进文档里。

68. executor和task优先于线程

  • java.util.concurrent.Executors类包含了静态工厂,能为你提供所需的大多数executor,还可以直接使用ThreadPoolExecutor类来创建自己的线程池,可以线程池操作的各个方面。

  • 线程池的选择
    如果是小程序,轻载的服务器,使用Executors.newCachedThreadPool
    如果是负载比较大,使用Executors.newFixed
    或者自定义以满足需求。

  • 尽量避免使用线程。
    现在工作单元和执行机制是分开的。Thread担当了这两个角色。
    现在关键是抽象是工作单元(task,任务),任务有两种:Runnable和Callable。
    执行任务的通用机制是executor service。

69. 并发工具优先于wait和notify

java.util.concurrent中工具分成三类:Executor Framework、并发集合(Concurrent Collection)、同步器(Synchronizer)。

  • 并发集合
    并发集合为标准的集合接口提供了高性能的并发实现。为了提高并发性,这些实现在内部自己管理同步。
    这意味着客户端无法原子的对并发集合进行方法调用。
    因此有些集合接口已经通过依赖状态的修改操作进行了扩展,它将几个基本操作合并到了单个原子操作中,比如ConcurrentHashMap;
    有些集合接口已经通过阻塞操作进行了扩展,它们会一直等待(阻塞)到可以成功执行为止。比如BlockingQueue。

  • 除非不得已使用同步集合,则应该优先使用ConcurrentHashMap,而不是使用Collections.synchronizedMap或Hashtable。

  • 同步器(Synchronizer)是一些使线程能够等待另一个线程的对象,允许它们协调动作。
    最常用的同步器是CountDownLatch和Semaphore,较不常用的是CyclicBarrier和Exchanger。

  • 使用wait的标准模式

/*
始终应该使用wait循环模式来调用wait方法,永远不要再循环之外调用wait方法。
循环会在等待之前和之后测试条件。
*/
while(condition is true){
  obj.wait();
}

总之,直接使用wait和notify就像使用并发汇编语言进行编程一样,而concurrent包中提供了更高级的工具。
没有理由使用wait和notify,即使有,也是极少的。

70. 线程安全性的文档化

  • 一个类为了可被多个线程安全的使用,必须在文档中清楚的说明他所支持的线程安全性级别。

  • 线程安全性级别

    • 不可变的(immutable)
      这个类的实例是不可变的,所以不需要外部同步。包括String、Long、BigInteger等。
    • 无条件的线程安全(unconditionally thread-safe)
      这个类的实例是可变的,但是这个类有着足够的内部同步,所以它的实例可以被并发使用,无需任何外部同步。包括Random、ConcurrentHashMap等。
    • 有条件的线程安全(conditionally thread-safe)
      除了有些方法为进行安全的并发使用而需要外部同步之外,这种线程安全级别于无条件的相同。包括Collections.synchronized()包装返回的集合,它们的迭代器总是要求外 部同步。
    • 非线程安全(not thread-safe)
      这个类的实例是可变的。为了并发的使用它们,客户端必须自己使用同步来确保正确性。
    • 线程对立的(thread-hostile)
      该类不能安全的被多线程使用,即使已经是使用同步调用。这种类是因为没有考虑到并发性而产生的结果。Java中线程队里的类或非法很少。
  • 文档中描述一个有条件的线程安全类要特别小心,必须指明哪个调用序列需要外部同步,还要指明这些序列需要的是哪一把锁。

  • 私有锁对象模式只能用在无条件的线程安全类上。

总之,每个类都应该利用字斟句酌的说明或者线程安全注解,清楚的在文档中说明它的线程安全属性。

71. 慎用延迟初始化

延迟初始化是延迟到需要域的值时才将它初始化的这种行为,如果永远不需要这个值,这个域就永远不会初始化。
这种方法既适用于静态域,也适用于实例域。

  • 对于延迟初始化,除非绝对必要,否则就不要这么做。
    降低了初始化类或者创建实例的开销,但增加了访问被延迟初始化的域的开销,实际上可能降低了性能。
    唯一的办法就是测量类在用和不使用延迟初始化时的性能差别。

  • 大多数情况下,正常初始化要优先于延迟初始化。
    如果出于性能的考虑,使用延迟初始化:

    • 对静态域,使用LoDH模式
    • 对实例域,使用DCL模式
      参考 单例模式
  • 如果可以接受重复初始化,可以使用单重检查模式。依然加上volatile关键字。
    如果不在意每个线程都重新计算域的值,并且域的类型为基本类型,且不是long或double,就可以把volatile删去,加快访问,但每个线程都会对它进行初始化。

72. 不要依赖于线程调度器

编写良好的程序不应该依赖于线程调度器,任何依赖于线程调度器来达到正确性或性能要求的程序,很有可能都是不可移植的,比如依赖于ScheduledThreadPoolExecutor等。

  • 要编写健壮的、响应良好的、可移植的多线程应用程序,最好的办法是确保可运行线程的平均数量不明显多于处理器的数量。

  • 保证可运行线程数量尽可能少的主要方法是,让每个线程做些有意义的工作,然后等待更多有意义的工作。
    如果线程没有在做有意义的工作,就不应该运行。

  • 如果一个线程无法像其他线程那样获得足够的CPU时间,那么不要调用Thread.yield来修正程序。不可移植。
    Thread.yield的唯一用途是在测试期间人为的增加程序的并发性,但并不一定可行。可以使用Thread.sleep(1)代替来进行并发测试(Thread.sleep(0)会直接返回)。

  • 线程优先级是Java平台上最不可移植的特征。

不要让程序的正确性依赖线程调度器,否则,这样的程序既不健壮,也不具有移植性。
作为推论,不要依赖Thread.yield或线程优先级。

73. 避免使用线程组

线程组已经过时了

线程组的初衷是作为一种隔离applet的机制(出于安全考虑),但它们从来没有履行这个承诺。
它们的安全价值已经差到根本不在Java安全模型的标准工作中提及的地步。

  • 它们允许你同时把Thread的某些基本功能应用到一组线程上。
    其中一些基本功能已经废弃了,剩下的也很好使用。

总之,线程组并没有提供太多有用的功能,且提供的功能都是由缺陷的。如果需要一个处理线程的逻辑组,可以使用线程池。

猜你喜欢

转载自blog.csdn.net/u014099894/article/details/68948142