Effective Java读书笔记一:并发(66-73)

来源:互联网 发布:java sock5 编辑:程序博客网 时间:2024/05/21 19:32

第66条:同步访问共享的可变数据

关键字synchronized可以保证在同一时刻,只有一个线程可以执行某一个方法,或者某一个代码块。

同步不仅可以阻止一个线程看到对象处于不一致的状态中,它还可以保证进入同步方法或者同步代码块的每个线程,都看到同一个锁保护的之前的修改效果。

Volatile 变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。对变量的写操作不依赖于当前值,该变量没有包含在具有其他变量的不变式中。不具有原子性。

多个线程共享可变数据的时候,每个读或写数据的线程都必须执行同步。如果没有同步,就无法保证一个线程所做的修改可以被另一个线程获知。如果需要线程之间的交互通信,而不需要互斥,volatile修饰符就是一种可以接受的形式,但需要正确的使用。

第67条:避免过度同步

依据情况不同,过度同步可能会导致性能降低,死锁,甚至不确定的行为。

为了避免活性失败和安全性失败,在一个被同步的方法或代码块中,永远不要放弃对客户端的控制。换句话说,在一个被同步的区域内部,不要调用设计成要被覆盖的方法,或者由客户端以函数对象的形式提供的方法。

通常,你应该在同步区域内做尽量少的工作。

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

如果一个可变类要并发使用,应该使这个类编程线程安全的,通过内部同步,你还可以获得,明显比外部锁定整个对象更高的并发性。否则,就不要在内部同步。让客户在必要的时候从外部同步。

反例:
StringBuffer实例几乎总是被用于单个线程中,而它们执行的却是内部同步。为此,StringBuffer基本都由StringBuilder代替。

第68条:executor和task优先于线程

  • 如果编写的是小程序,或者轻载的服务器,使用Executor.newCachedThreadPool通常是个不错的选择。
  • 在大负载的服务器中,最好使用Executor.newFixedThreadPool,它为你提供了一个包含固定线程数目的线程池,或者为了最大限度的控制它,就直接使用ThreadPoolExecutor类。

你不仅应该尽量不要编写自己的工作队列,而且还应该尽量不直接使用线程。现在的关键抽象不再是Thread了,它以前既充当工作单位,又是执行机制。工作单位和工作单位是分开的,现在的关键抽象是工作单元,称作任务(task)。

任务有两种:Runnable及其近亲Callable(它与Runnable类似,但它会返回值)。执行任务的通用机制是executor service。

Executor FrameWork也有一个可以代替java.util.Timer的东西,即ScheduledThreadPoolExecutor。

Timer只有一个线程来执行任务,如果timer唯一的线程抛出未被捕捉的异常,timer就会停止工作。而线程池executor支持多个线程,并且优雅的从抛出未受检异常的任务中恢复。

第69条:并发工具优先于wait和notify

java.util.concurrent中更高级的工具分三类:Executor Framework,并发集合(Concurrent Collection)以及同步器(Synchronizer)。

并发集合为标准的集合接口提供了高性能的并发实现,这些实现在内部自己管理同步。因此,并发集合中不可能排除并发活动;将它锁定没有什么作用,只会使程序的速度变慢。

concurrent collections提供了标准容器的高性能并发实现.内部同步和互斥,外部使用,无需加锁.

优先使用ConcurrentHashMap,而不是Collections.synchronizedMap或者Hashtable,且无需做同步操作.

有的concurrent collections提供了block操作接口,例如BlockingQueue,从中取数据的时候,如果队列为空,线程将等待,新的数据加入后,将自动唤醒等待的线程;大部分的ExecutorService都是采用这种方式实现的

简而言之,我们应该,优先使用java.util.concurrent包中提供的更高级的语言来代替wait,notify.

如果非要用wait和notify,注意以下几点:

  • wait前的条件检查,当条件成立时,就跳过等待,可以保证不会死锁,
  • wait后的检查,条件不成立继续等待,可以保证安全
  • 通常情况下都应该使用notifyAll,虽然从优化角度看,这样不好.

同步器是使一个线程能够等待另一个线程的对象。

最常用的同步器是CountDownLatch和Semaphore,不常用的是Barrier 和Exchanger。

倒计数器 锁存器是一次性障碍,允许一个或者多个线程等待一个或者多个其它线程来做某些事情。

CountDownLatch的唯一构造器带一个int类型的参数,这个int参数是指允许所有在等待线程被处理之前,必须在锁存器上调用countDown方法的次数。

对于间歇式定时,应该始终使用System.nanoTime而不是System.cucurrentTimeMills。

应该始终使用wait循环模式来调用wait方法.不要在循环外调用wait方法.

小结
直接使用 wait和notify,就像 用并发汇编语言进行编程一样.而concurrent则提供了更高级的语言。
没有理由在新代码中使用 wait和notify ,即使有,也很少。
如果正在维护使用 wait和notify的代码,则尽量在 while循环内部调用wait。
应该优先使用notifyAll,而不是notify.

第70条:线程安全性的文档化

如果你没有在一个类的文档中描述其行为的并发情况,使用这个类的程序员将不得不做出某些假设。如果这些假设是错误的,这样得到的程序就可能缺少足够的同步,或者过度同步。无论属于哪种情况,都可能会发生严重的错误。

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

  • 不可变的——这个类的实例是不可变的。这样的例子包括String,Long,BigInteger。
  • 无条件的线程安全——这个类的实例是可变的,但是这个类有足够的内部同步。例子包括Random,ConconcurrentHashMap。
  • 有条件的线程安全——除了有些方法为进行安全的并发使用而需要外部同步之外,这种线程安全级别与无条件安全相同。例子包括:Collections.synhronized包装返回的集合,它们的迭代器要求外部同步。
  • 非线程安全——这个类的实例是可变的。为了并发使用它们,客户必须利用自己选择的外部同步包围每个方法调用。例子包括ArrayList
  • 线程对立的——这个类不能安全地被多个线程并发使用,即使所有的方法调用都被外围同步包围。

类的线程安全说明通常放在它的文档中,但带有特殊线程安全属性的方法则应该在它们自己的文档注释中说明它们的属性。

私有锁只能用在无条件的线程安全类上。私有锁对象模式特别适用于那些专门为继承而设计的类。如果这种类适用它的实例作为锁的对象,子类可能很容易在无意中妨碍基类的操作,反之亦然。

第71条:慎用延迟初始化

延迟初始化是延迟到需要域的值时才将它初始化的这种行为。

对于延迟初始化,最好建议“除非绝对必要,否则就不要那么做”。延迟化降低了初始化类或者创建实例的开销,却增加了访问被延迟初始化的域的开销。

如果域只是在类的实例部分被访问,并且初始化这个域的开销很高,可能就值得进行延迟初始化。

如果出于性能的考虑而需要对静态域使用延迟初始化,就使用lazy initialization holder class 模式。保证在被用时初始化。

private static class FieldHolder {      static final FieldType field = computeFieldValue();  }  public static FieldType getField() {      return FieldHolder.field;  }  

如果出于性能的考虑而需要对实例域使用延迟初始化,就使用双重检查模式。这种模式避免了在域被初始化之后访问这个域时的锁定开销。

private volatile FieldType field;  public FieldType getField() {      FieldType result = field;      if (result == null) {          synchronized (this) {              result = field;              if (result == null) {                  field = result = computeFieldValue();              }          }      }      return result;  }  

第一次检查时没有锁定,看看这个域是否被初始化;第二次检查时又锁定。只有当第二次检查时标明这个域没有被初始化,才进行初始化。如果域已经被初始化就不会有锁定,域被声明为volatile很重要。

result局部变量的使用,是为了保证在已经被初始化的情况下,原来的变量只被读取一次到局部变量result中,否则在比较的时候需要读取一次,返回的时候还需要读取一次。虽然这不是严格要求,但是可以提升性能。

简而言之,大多数的域应该正常的进行初始化,否则,可以参考上面的规则,进行延迟初始化

第72条:不要依赖于线程调度器

  • 当有多个线程可以运行时,由线程调度器决定哪些线程将会执行.以及运行多长时间。
  • 任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的。
  • 要确保可运行线程的平均数量不明显多于处理器的数量。
  • 要编写健壮,响应良好的,可移植的多线程应用程序,最好的办法是确保可运行线程的平均数量不明显多于处理器的数量。
  • 线程优先级是Java平台中移植性最差的部分,所以也不要用。

对于大多数程序员来说,Thread.yield的唯一用途,就是在测试期间人为的增加程序的并发性。
在Java语言规范中,Thread.yield根本不做实质性工作,只是将控制权返回给它的调用者。

小结
不要让应用程序的并发性依赖于线程调度器
不要依赖Thread.yield和线程优先级

第73条:避免使用线程组

线程组并没有提供太多有用的功能,而且他们提供的许多功能还都有缺陷的。

如果你正在设计的一个类需要处理线程的逻辑组,或许就应该使用线程池executor。

《Effective Java中文版 第2版》PDF版下载:
http://download.csdn.net/detail/xunzaosiyecao/9745699

作者:jiankunking 出处:http://blog.csdn.net/jiankunking

0 1