并发Concurrent

来源:互联网 发布:java nanotime 唯一 编辑:程序博客网 时间:2024/06/05 16:49

Thread机制允许同时进行的多个活动,并发程序设计比单线程程序设计要困难得多。

第六十六条、同步访问共享的可变数据

  1. 关键字synchronized可以保证在同一时刻,只有一个线程可以执行某一个方法,或者某一代码块。同步为一种互斥的方式:当一个对象被一个线程修改的时候,可以阻止另一个线程观察到对象内部不一致的状态。对象被创建的时候处于一致的状态,当有方法访问它的时候,它就被锁定。这些方法观察到对象的状态,并且可能会引起状态转变;正确地使用同步可以保证没有任何方法会看到对象处于不一致的状态。如果没有同步,一个线程的变化就不能被其他线程看到。同步不仅可以阻止一个线程看到对象处于不一致的状态之中,它还可以保证进入同步方法或者同步代码块的每个进程,都看到由同一个锁保护的之前所有的修改结果。

  2. 为了在线程之间进行通信,也为了互斥访问,同步是必要的。归因于Java语言规范中的内存模型,它规定了一个线程所做的变化何时以及如何变成对其他线程可见。

  3. 如果对共享的可变数据的访问不能同步,后果十分可怕,即使这个变量是原子可读写的。(不要使用Thread.stop方法,本质上是不安全的。)要阻止一个线程妨碍另一个线程,建议的做法是让第一个线程轮询poll一个boolean域,这个域一开始为false,但是可以通过第二个线程设置为true,以表示第一个线程将终止自己

        import java.util.Timer;    import java.util.concurrent.TimeUnit;    /**     * Created by laneruan on 2017/7/30.     */    public class StopThread {    //    private static boolean stopRequest;    //    //    public static void main(String[] args) throws InterruptedException{    //        Thread backgroundThread = new Thread(new Runnable() {    //            @Override    //            public void run() {    //                int i = 0;    //                while(!stopRequest){    //                    i++;    //                }    //            }    //        });    //        backgroundThread.start();    //        //问题在于,由于没有同步,就不能保证后台线程何时能够"看到"主线程    //        // 对stopRequested值所做的改变;修正方法是同步访问stopRequested域    //        TimeUnit.SECONDS.sleep(1);    //        stopRequest = true;    //    }        private static boolean stopRequested;        //注意,读写方法都被同步了,如果读写没有都被同步,同步就不会起作用        // 但是这里存在的问题是循环的每次迭代中的同步开销,虽然很小,但还是        // 有更简洁且性能更好的替换法    //    private static synchronized void requestStop(){    //        stopRequested = true;    //    }    //    private static synchronized boolean stopRequested(){    //        return stopRequested;    //    }    //    public static void main(String[] args) throws InterruptedException{    //        Thread backgroundThread = new Thread(new Runnable() {    //            @Override    //            public void run() {    //                int i = 0;    //                while(!stopRequested()){    //                    i++;    //                }    //            }    //        });    //        backgroundThread.start();    //        TimeUnit.SECONDS.sleep(1);    //        requestStop();    //    }        //volatile修饰符不执行互斥访问,但是它可以保证任何一个线程在读取该域的        //时候都将看到最近刚刚被写入的值。        private static volatile boolean stopRequest;        public static void main(String[] args) throws InterruptedException{            Thread backgroundThread = new Thread(new Runnable() {                @Override                public void run() {                    int i = 0;                    while(!stopRequest){                        i++;                    }                }            });            backgroundThread.start();            //问题在于,由于没有同步,就不能保证后台线程何时能够"看到"主线程            // 对stopRequested值所做的改变;修正方法是同步访问stopRequested域            TimeUnit.SECONDS.sleep(1);            stopRequest = true;        }    }
  4. 避免这条遇到的问题最佳办法是不共享可变的数据:

    换句话说是将可变数据限制在单个线程中。让一个线程在短时间内修改一个数据对象,然后与其他线程共享,这是可以接受的,只同步共享对象引用的动作,然后其他线程没有进一步的同步也可以读取对象,只要它没有再被修改。这种对象被称作事实上不可变的。

    将这种对象引用从一个线程传递到其他的线程被称作安全发布,安全发布对象引用有许多种方法:可以将它保存在静态域中,作为类初始化的一部分;可以将它保存在volatile域、final域或者通过正常锁定访问的域中,或者可以将它放到并发的集合中。

  5. 总结:当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步。未能同步共享可变数据会造成程序的活性失败和安全性失败。如果只需要线程之间的交互通信而不要求互斥,volatile修饰符就是一种可以接受的同步形式,但是使用它需要一些技巧。


第六十七条、避免过度同步

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

  2. 为了避免活性失败和安全性失败,在一个被同步的方法或者代码块中,永远不要放弃对客户端的控制。即在一个被同步的区域内部,不要调用设计成要被覆盖的方法,或者是由客户端以函数对象的形式提供的方法。从包含该同步区域的类的角度来看,这样的方法是外来的,这个类不知道该方法会做什么,也无法控制它。

  3. 总结:为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法,更为一般地讲:尽量限制同步区域内部的工作量。只有当你有足够的理由一定要在内部同步类的时候,才应该这么做,同时要写在文档中。


第六十八条、executor和task优先于线程

  1. Java1.5发行版本中,java.util.concurrent包含了一个Executor Framework,这是一个很灵活的基于接口的任务执行工具。它创建了一个很好地工作队列:
    ExecutorService executor = Executors.newSingleThreadExecutor();
    下面是为执行提交一个runnable的方法:
    executor.execute(runnable);
    下面是告诉executor如何优雅地终止:
    executor.shutdown();
    可以利用executor service完成更多的事情。比如,可以等待完成一项特殊任务,可以等待一个任务集合中的任何任务或者所有任务完成(invokeAny和invokeAll方法)等等。

  2. 如果想让不止一个线程来处理来自这个队列的请求,只要调用一个不同的静态工厂,这个工厂创建了一种不同的executor service,称作线程池(thread pool)。你可以用固定或者可变数目的线程创建一个线程池,java.util.concurrent.Executors类包含了静态工厂,能为你提供所需的大多数executor,当然也可以使用更强大的ThreadPoolExecutor类。

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

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

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

  4. 你不仅应该尽量不要编写自己的工作队列,而且还应该尽量不直接使用线程。现在关键的抽象不再是Thread,它以前既充当工作单元,又是执行机制,现在工作单元和执行机制是分开的,抽象的是工作单元:task(任务)。任务有两种:Runnable及其近亲Callable;执行任务的通用机制是Executor Service。如果你从任务的角度看,并让一个executor service替你执行任务,在选择适当的执行策略方面就获得了极大的灵活性。


第六十九条、并发工具优先于wait和notify

  1. 既然正确地使用wait和notify比较困难,就应该使用更高级的并发工具来代替。java.util.concurrent中更高级的工具分为三类:Executor Framework并发集合(Concurrent Collection)以及同步器(Synchronizer)。

  2. 并发集合为标准的集合接口(如List、Queue、Map)提供了高性能的并发实现。为了提高并发性,这些实现在内部自己同步管理。因此,并发集合中不可能排除并发活动,客户无法原子地对并发集合进行方法调用。因此有些集合接口已经通过依赖状态的修改状态进行了扩展,它将几个基本操作合并到了单个原子操作中。例如:ConcurrentMap扩展了Map接口,并添加了几个方法,一般地说,优先使用并发集合,而不是使用外部同步的集合。有些集合接口已经通过阻塞操作进行了扩展,它们会一直等待到可以执行成功为止。

  3. 同步器(Synchronizer)是一些使线程能够等待另一个线程的对象。允许它们协调动作,最常用的同步器是CountDownLatchSemaphore

    • 倒计数锁存器CountDown Latch是一次性的障碍,允许一个或者多个线程等待一个或者多个其他线程来做某些事情。CountDownLatch的唯一构造器是带有一个int类型的参数,这个int类型的参数表示允许所有在等待的线程被处理之前,必须在锁存器上调用countDown方法的次数。

          import java.util.concurrent.CountDownLatch;    import java.util.concurrent.Executor;    /**     * Created by laneruan on 2017/8/2.     */    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;        }    }

4、总结:直接使用wait和notify就像是用“并发汇编语言”进行编程一样,而java.util.concurrent则提供了更高级的语言,没有理由在新代码中使用wait和notify。


第七十条、线程安全性的文档化

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

    • 不可变的 Immutable:这个类的实例是不可变的,不需要外部的同步,如:String、Long和BigInteger

    • 无条件的线程安全 unconditionally thread-safe:这个类的实例是可变的,但是这个类有着足够的内部同步,所以它的实例可以被并发使用,无需任何外部同步。如:Random和ConcurrentHashMap.

    • 有条件的线程安全 conditionally thread-safe:除了有些方法进行安全的并发使用而需要外部同步之外,这种线程的安全级别与无条件的线程安全相同。包括Collections.synchronized包装返回的集合。

    • 非线程安全 not thread-safe:这个类的实例是可变的,为了并发地使用它们,客户必须利用自己选择的外部同步包围每个方法调用。例子包括通用的集合实现(ArrayList和HashMap)

    • 线程对立的 thread-hostie:这个类不能安全地被多个线程并发使用。(很少)

  2. 有条件的线程安全类必须在文档中指明“哪个方法调用序列需要外部同步,以及在执行这些序列的时候需要获得哪把锁。”如果你编写的是无条件的线程安全类,就应该考虑使用私有锁对象来代替同步的方法。


第七十一条、慎用延迟初始化Lazy initialization

  1. 延迟初始化是延迟到需要域的值时才将它初始化的这种行为。如果永远不需要这个值,这个域就永远不会被初始化。这种方法既适用于静态域,也适用于实例域。主要是一种优化,除非绝对必要,否则就不要这么做。延迟初始化是一把双刃剑:降低了初始化类或者创建实例的开销,却增加了访问被延迟初始化的域的开销

  2. 当有多个线程的时候,延迟初始化是需要技巧的,如果两个或者多个线程共享一个延迟初始化的域,采用某种形式的同步是很重要的,否则就会造成很严重的bug。大多数情况下,正常的初始化要优先于延时初始化
    private final FieldType field = computerFieldValue();
    同步访问方法来进行延迟初始化:

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

        private static class FieldHolder{         static final FieldType field = computerFieldValue();    }    static FieldType getField(){ return FieldHolder.field;}
  4. 如果出于性能的考虑需要对实例域进行延迟初始化,就使用**双重检查模式**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;    }
  5. 总结:大多数的域都应该正常地进行初始化,而不是延迟初始化。如果为了达到性能目的或者为了破坏有害的初始化循环,而必须延迟初始化一个域,就可以使用相应的初始化方法。


第七十二条、不要依赖线程调度器

  1. 任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的。要编写健壮的,响应良好的、可移植的多线程应用程序,最好的办法是确保可运行线程的平均数量不明显多于处理器的数量。这使得线程调度器没有更多的选择。

  2. 保持可运行线程数量尽可能少的主要方法是:让每个线程做些有意义的工作,然后等待更多有意义的工作。根据Executor Framework这意味着适当地规定了线程池的大小,并且使任务保持适当地小,彼此独立。

  3. 线程不应该一直处于busy-wait状态,即反复地检查一个共享对象,以等待某些事情的发生。除了使程序易受到调度器的影响,这种做法也极大地增加了处理器的负担。

  4. 不要依赖Thread.yield或者线程优先级,这些设施仅仅对调度器作出暗示。线程优先级可以用来提高一个已经能够正常工作的程序的服务质量,但永远不应该拿来修正一个原本并不能工作的程序。


第七十三条、避免使用线程组

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

原创粉丝点击