第十章 并发

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

  • 不要使用Thread.stop。要阻止一个线程妨碍另一个线程,建议做法是让第一个线程轮询一个boolean域,这个域一开始为false,但是可以通过第二个线程设置为true,以表示第一个线程将终止自己。由于boolean域的读写是原子的,所以在访问这个域的时候不在需要同步

67. 避免过度同步

  • 过度同步可能导致性能降低、死锁,甚至不确定的行为
  • 应该在同步区域内做尽量少的工作
  • 为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法

68. executor和task优于线程

java concurrent包中的Executor FrameWork是一个很灵活的基于接口的任务执行工具

ExecutorService executor = Executors.newSingleThreadExecutor();
// 下面是为执行提交一个runable的方法
executor.execute(runable);
// 告诉executor终止
executor.shutdown();

为不同的应用程序选择executor service是很有技巧的

如果编写的是小程序,或者是轻载的服务器,使用Executors.newCachedThreadPool通常是个不错的选择,因为它不需要配置,并且一般情况下能够正确地工作,

但是对于大负载的服务器来说,缓存的线程池不是很好地选择了。在缓存的线程池中,被提交的任务没有排成队列,而是直接交给线程执行。如果没有线程可用,就创建一个新的线程。因此,在大负载的产品服务器中,最好使用Executors.newFixedThreadPool,它为你提供了一个包含固定线程数目的线程池

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

  • java.util.concurrent中更高级的工具分三类:Executor Framework,并发集合(Concurrent Collection)以及同步器(Synchronizer)
  • 并发集合为标准的集合接口(如List、Queue、Map)提供了高性能的并发实现。为了提高并发性,这些实现在内部自己同步管理。因此,并发集合中不可能排除并发活动,客户无法原子地对并发集合进行方法调用。因此有些集合接口已经通过依赖状态的修改状态进行了扩展,它将几个基本操作合并到了单个原子操作中。例如:ConcurrentMap扩展了Map接口,并添加了几个方法,一般地说,优先使用并发集合,而不是使用外部同步的集合
  • 同步器(Synchronizer)是一些使线程能够等待另一个线程的对象。允许它们协调动作,最常用的同步器是CountDownLatch和Semaphore
  • 倒计数锁存器CountDown Latch是一次性的障碍,允许一个或者多个线程等待一个或者多个其他线程来做某些事情。CountDownLatch的唯一构造器是带有一个int类型的参数,这个int类型的参数表示允许所有在等待的线程被处理之前,必须在锁存器上调用countDown方法的次数

    public class ConcurrentCountDown {
        //给一个动作的并发执行定时。
        /**
         * @param executor:执行该动作的executor
         * @param concurrency:并发级别
         * @param action :该动作的Runnable
         *
         * */
        public static long time(Executor executor, int concurrency,final Runnable
                action) throws InterruptedException{
            final CountDownLatch ready = new CountDownLatch(concurrency);
            final CountDownLatch start = new CountDownLatch(1);
            final CountDownLatch done = new CountDownLatch(concurrency);
            for (int i = 0;i < concurrency;i++){
                executor.execute(new Runnable() {
                    @Override
                    public void run() {
                        ready.countDown();  //tell timer we're ready
                        try{
                            start.await();  //wait till peers are ready
                            action.run();
                        } catch (InterruptedException e){
                            Thread.currentThread().interrupt();
                        }finally{
                            done.countDown();   //tell timer we're done
                        }
                    }
                });
            }
            ready.await();             //wait for all workers to be ready
            long startNanos = System.nanoTime(); //对于间歇式的定时,优先使用System.nanoTime
            start.countDown();         //and they're off
            done.await();              //wait for all work to finish
            return System.nanoTime() - startNanos;
        }
    }

70. 线程安全性的文档化

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

    • 不可变的 Immutable:这个类的实例是不可变的,不需要外部的同步,如:String、Long和BigInteger
    • 无条件的线程安全 unconditionally thread-safe:这个类的实例是可变的,但是这个类有着足够的内部同步,所以它的实例可以被并发使用,无需任何外部同步。如:Random和ConcurrentHashMap.
    • 有条件的线程安全 conditionally thread-safe:除了有些方法进行安全的并发使用而需要外部同步之外,这种线程的安全级别与无条件的线程安全相同。包括Collections.synchronized包装返回的集合。
    • 非线程安全 not thread-safe:这个类的实例是可变的,为了并发地使用它们,客户必须利用自己选择的外部同步包围每个方法调用。例子包括通用的集合实现(ArrayList和HashMap)
    • 线程对立的 thread-hostie:这个类不能安全地被多个线程并发使用。(很少)
  2. 有条件的线程安全类必须在文档中指明“哪个方法调用序列需要外部同步,以及在执行这些序列的时候需要获得哪把锁。”如果你编写的是无条件的线程安全类,就应该考虑使用私有锁对象来代替同步的方法

71. 慎用延迟初始化Lazy initialization

  • 延迟初始化是延迟到需要域的值时才将它初始化的这种行为。如果永远不需要这个值,这个域就永远不会被初始化。这种方法既适用于静态域,也适用于实例域。主要是一种优化,除非绝对必要,否则就不要这么做。延迟初始化是一把双刃剑:降低了初始化类或者创建实例的开销,却增加了访问被延迟初始化的域的开销
  • 当有多个线程的时候,延迟初始化是需要技巧的,如果两个或者多个线程共享一个延迟初始化的域,采用某种形式的同步是很重要的,否则就会造成很严重的bug。大多数情况下,正常的初始化要优先于延时初始化:
    private final FieldType field = computerFieldValue();
    同步访问方法来进行延迟初始化:

    private FieldType field;
    synchronized FieldType getField(){
      if (field ==null)
        field = computerFieldValue();
      return field;
    }
  • 如果出于性能的考虑需要对静态域进行延迟初始化,就使用lazy initialization holder class模式,这种模式保证了类要被用到时才会被初始化

    private static class FieldHolder{
    static final FieldType field = computerFieldValue();
    }
    static FieldType getField(){ 
    return FieldHolder.field;
    }
  • 如果出于性能的考虑需要对实例域进行延迟初始化,就使用**双重检查模式**double checked idiom,避免在域被初始化之后访问这个域时的锁定开销。这种模式的思想是两次检查,第一次检查使没有锁定,看看这个域是否初始化了,第二次检查时有锁定。如果域已经初始化则不会有锁定:

    private volatile FieldType field;
    FieldType getField(){
      FieldType result = field;
      if(result == null){
        synchronized (this){
          result = field;
          if(result == null){
            field = result = computerFieldValue();
          }
        }
      }
      return result;
    }
  • 总结:大多数的域都应该正常地进行初始化,而不是延迟初始化。如果为了达到性能目的或者为了破坏有害的初始化循环,而必须延迟初始化一个域,就可以使用相应的初始化方法

72. 不要依赖线程调度器

  • 任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的。要编写健壮的,响应良好的、可移植的多线程应用程序,最好的办法是确保可运行线程的平均数量不明显多于处理器的数量。这使得线程调度器没有更多的选择
  • 保持可运行线程数量尽可能少的主要方法是:让每个线程做些有意义的工作,然后等待更多有意义的工作。根据Executor Framework这意味着适当地规定了线程池的大小,并且使任务保持适当地小,彼此独立
  • 线程不应该一直处于busy-wait状态,即反复地检查一个共享对象,以等待某些事情的发生。除了使程序易受到调度器的影响,这种做法也极大地增加了处理器的负担
  • 不要依赖Thread.yield或者线程优先级,这些设施仅仅对调度器作出暗示。线程优先级可以用来提高一个已经能够正常工作的程序的服务质量,但永远不应该拿来修正一个原本并不能工作的程序

73. 避免使用线程组

  • 线程系统处理线程、锁、监视器之外,还提供了一个基本的抽象:线程组。线程组的初衷是作为一种隔离的applet小程序机制,但是它的安全性不敢恭维。可以忽略

猜你喜欢

转载自blog.csdn.net/u010634897/article/details/81463481