美文网首页effective Java
《Effective Java》读书笔记 —— 并发

《Effective Java》读书笔记 —— 并发

作者: 666真666 | 来源:发表于2017-04-13 14:56 被阅读100次

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

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

    Java 语言保证读或者写一个变量是原子的,除非这个变量的类型是 long 或者 double。虽然读写一个变量是原子的,但不能保证一个线程的写入值对于另一个线程将是可见的,归因于 Java 语言的内存模型(可能暂时存放在内存缓存或者寄存器),所以线程间同步也是必要的。

    实例一

    以下是是个不使用同步的例子,通过一个boolean域实现线程间通信,主线程通过一个静态变量控制后台线程何时停止循环。

    private static boolean stopRequested;  // 共享数据
    Thread backgroundThread = new Thread(new Runnable(){
        public void run() {
            int i = 0;
            while(!stopRequested) {
                i++;
            }
        }
    });
    backgroundThread.start();
    
    sleep(1);    // 1s 后设置stopRequested,让另一个线程执行
    stopRequested = true;
    

    运行结果,循环永远不会结束,由于没有同步,就不能保证后台线程何时“看到”主线程对stopRequested的改变。没有同步,不会考虑其他地方,所以编译器会有提升优化

    while(!stopRequested) {
      i++;
    }
    

    转变为:

    if (!stopRequested) {
       while(true) {
           i++;
       }
    }
    

    方案一:修改为同步方法数据

    private static synchronized void requestStop {
        stopRequested = true;
    }
    
    private static synchronized boolean stopRequested() {
        return stopRequested;
    }
    

    方案二:stopRequested声明为 volatile ,性能更好,不使用锁。

    private static volatile boolean stopRequested;
    
    案例二

    使用 volatile 要小心。volatile修饰符只能保证任何一个线程在读取该域的时候都是最近刚刚被写入的值,不能保证执行互斥方法。

    private static volatile int nextSerialNumber = 0;
    public static int generateSerialNumber() {
        return nextSerialNumber++;
    }
    

    由于增量操作符++,不是原子性的,generateSerialNumber方法内部其实执行了两项操作,首先读取值,然后写回一个新值,如果第二个线程在第一个线程读取旧值的期间读取这个域,第二个线程就会与第一个线程一起看到同一个值,并返回相同的序列号,这就是安全性失败,得到错误的结果。

    方案一:generateSerialNumber 方法声明增加 synchronized 修饰符,可以删除变量的volatile修饰符

    方案二:使用 AtomicLong 类

    private static final AtomicLong nextSerialNum = new AtomicLong();
    
    public static long generateSerialNumber(){
        return nextSerialNum.getAndIncrement();
    }
    
    总结

    当多个线程共享可变数据时,每个读或者写数据的线程都必须执行同步。如果只需要线程间的交互通信,而不需要互斥,则可以使用volatile修饰符。如果线程间只读取数据,不会修改,那么事实上是不可变的,这种不需要同步机制。

    2.避免过度同步

    上一条是缺少同步的危险性,本条关注点相反,过度同步可能会导致性能降低、死锁、甚至不确定的行为。

    在一个被同步的方法或者代码块中,永远不要放弃对客户端的控制。换句话说,在一个被同步的区域内部,不要调用设计成被覆盖的方法,或者是由客户端函数对象的形式提供的方法,这样的方法是外来的。因为外来的无法控制,在同步域中调用可能会导致异常、死锁或者数据损坏。

    3.executor和task优先于线程

    工作队列:允许客户端将后台异步处理的工作项目加入队列。

    Executors 提供了静态工厂,可以创建任何你想要的大多数 executor

    不应该编写自己的工作队列,而且尽量不要直接使用线程。

        * Executors.newSingleThreadExecutor();
            * 单个线程执行任务
        * Executors.newCachedThreadExecutor();
            * 缓冲池
            * 编写的是小程序,或者轻量级的服务器
        * Executors.newFixedThreadExecutor();
            * 大负载服务器,可以控制线程数据的线程池
        * Executors.newPoolExecutor();
            * 可以最大限度的控制线程池
        * ScheduledThreadPoolExecuror
            * timer只用一个线程执行任务,对于长期运行的任务,会影响都定时的准确性。
            * ScheduledThreadPoolExecuror支持多线程,更加灵活
    

    使用demo

    ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.execute(runnable)   // 开始执行
    executor.shutdown();            // 优雅地终止
    

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

    没有理由再使用,1.5以后Java提供更高级的并发工具

    更高级的并发工具:

    • Executor Framework
    • 并发集合(Concurrent Collection)
    • 同步器(Synchronizer)
    并发集合(Concurrent Collection)

    并发工具为标准的集合接口(List、Queue、Map)提供了高性能的并发实现。

    同步器(Synchronizer)

    同步器是一些使线程能够等待另一个线程的对象,允许它们协调动作。

    同步器包含:(后两种不常用)

    • CountDownLatch
      • 倒计数锁存器,允许一个或者多个线程等待一个或者多个其他线程来做某些事情
      • 构造器带有一个int,指允许所有在等待的线程被处理之前,必须在锁存器上调用 countDown 方法的次数
    • Semaphore
    • CyclicBarrier
    • Exchanger

    5.线程安全性的文档化

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

    几种线程安全性级别:

    • 不可变的:这个类的实例是不变的,所以,不需要外部的同步,包括String、Long、BitInteger
    • 无条件的线程安全:这个类的实例可变,但是这个类有足够的内部同步,可以被并发使用,包括Random、ConcurrentHashMap
    • 有条件的线程安全:一部分方法线程安全
    • 非线程安全:类可变,为了并发使用,客户端必须利用自己选择的外部同步包围每一个方法(或者调用序列),包括ArrayList和HashMap
    • 线程队里的:不能安全地被多个线程并发使用,即使有外部同步包围。线程对立的根源通常在于没有同步的修改静态数据,基本没有线程对立的

    6.慎用延迟初始化

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

    优点:降低了初始化类或者创建实例的开销。

    缺点:增加了访问被延迟初始化的域的开销。

    多线程使用同一延迟初始化域,需要采用某种同步方法。

    简单的同步get方法

    大多数情况下,正常初始化要优先于延迟初始化,下面是正常初始化

    private final FieldType field = computerFieldValue();
    

    下面是支持多线程的延迟初始化

    private FieldType field;
    synchronized FieldType getField() {
        if (field == null) {
            field = computerFieldValue();
        }
        return field;
    }
    
    利用静态内部类实现懒加载

    处于性能考虑,也可以使用一下方法

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

    第一次调用getField,读取FieldHolder.field,导致FieldHolder类初始化。这种模式的好处在于,不需要对getField使用同步。VM在初始化类时,是同步访问的

    双重检查模式

    避免了在域被初始化之后访问这个域时的锁定开销。

    private volatile Field field;
    FieldType getField() {
        FieldType result = field;
        if (result == null) {
            synchronized(this) {
                result = field;
                if(result == null) {
                    field = result = computerFieldValue();
                }
            }
        }
    }
    
    单重检查模式
    private volatile Field field;
    FieldType getField() {
        FieldType result = field;
        if(result == null) {
            field = result = computerFieldValue();
          }
        }
    }
    
    总结

    大多数情况,都应该进行正常初始化,而不是延迟初始化。如果要使用,对于实例域,使用双重检查模式,对于静态域,使用功能静态内部类懒加载,对于可以接受重复初始化的实力域,可以考虑使用单重检查模式。

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

    线程调度器:决定哪些线程将会运行以及运行多长时间。

    任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能是不可移植的。不要依赖 Thread.yield 或者线程优先级

    8.避免使用线程组

    线程组:允许你同时把Thread的某些基本功能应用到一组线程上。

    线程组并没有提供太多有用的功能,而且它们提供的许多功能还是有缺陷的。或者应该使用线程池。

    相关文章

      网友评论

        本文标题:《Effective Java》读书笔记 —— 并发

        本文链接:https://www.haomeiwen.com/subject/tdmbattx.html