EffectiveJava--并发

编程技术  /  houtizong 发布于 3年前   66
本章内容:
1. 同步访问共享的可变数据
2. 避免过度同步
3. executor和task优先干线程
4. 并发工具优先于wait和notify
5. 线程安全性的文档化
6. 慎用延迟初始化
7. 不要依赖于线程调度器
8. 避免使用线程组

1. 同步访问共享的可变数据
    关键字synchronized可以保证在同一时刻,只有一个线程可以执行某一个方法,或者某一个代码块。对象同步并不仅限于当多个线程操作同一可变对象时,仍然能够保证该共享对象的状态始终保持一致。与此同时, 他还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护的之前所有的修改效果。 
    Java语言规范保证了读写一个变量是原子的,除非这个变量的类型为long 或double。换句话说, 读取一个非long 或double 类型的变量,可以保证返回的值是某个线程保存在该变量中的,即时多个线程在没有同步的情况下并发地修改这个变量也是如此。你可能听说过,为了提高性能,在读或写原子数据的时候,应该避免使用同步,这个建议是非常危险而错误的。虽然语言规范保证了线程在读取原子数据的时候,不会看到任意的数值,但是它并不保证一个线程写入的值对于另一个线程将是可见的。为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的。
    即便这样做不会带来数据同步修改的问题,但是他会导致另外一个更为隐匿的错误发生。见如下代码:
        public class StopThread {
            private static boolean stopRequested = false;
            public static void main(String[] args) throw InterruptedException
            {
                Thread bgThread = new Thread(new Runnable() {
                    public void run() {
                        int i = 0;
                        while (!stopRequested)
                            i++;
                    }
                });
                bgThread.start();
                TimeUnit.SECONDS.sleep(1);
                stopRequested = true;
            }
        }
    对于上面的代码片段,有些人会认为在主函数sleep 一秒后,工作者线程的循环状态标志(stopRequested)就会被修改,从而致使工作者线程正常退出。然而事实却并非如此,因为Java 的规范中并没有保证在非同步状态下,一个线程修改的变量,在另一个线程中就会立即可见。事实上,这也是Java针对内存模型进行优化的一个技巧。没有同步,虚拟机将这个代码:
        while (!stopRequested)
            i++;
    转换成这样:
        if (!stopRequested) {
            while (true)
                i++;
        }
    这是可以接收的,这种优化被称为提升,正是HotSpot Server VM的工作。结果是个活性失败:这个程序无法前进。修正这个问题的一种方式是同步访问stopRequested域。见如下代码:
        public class StopThread {
            private static boolean stopRequested = false;
            private static synchronized void requestStop() {
                stopRequested = true;
            }
            private static synchronized boolean stopRequested() {
                return stopRequested;
            }
            public static void main(String[] args) throw InterruptedException
            {
                Thread bgThread = new Thread(new Runnable() {
                    public void run() {
                        int i = 0;
                        while (!stopRequested())
                            i++;
                    }
                });
                bgThread.start();
                TimeUnit.SECONDS.sleep(1);
                requestStop();
            }
        }
    在上面的修改代码中,读写该变量的函数均被加以同步。如果读和写方法没有都被同步,同步就不会起作用。
    StopThread中被同步方法的动作即使没有同步也是原子的。换句话说,这些方法的同步只是为了它的通信效果,而不是为了互访访问。事实上,Java 中还提供了另外一种方式用于处理该类问题,即volatile 关键字。该单词的直译为“易变的”,引申到这里就是告诉cpu 该变量是容易被改变的变量,不能每次都从当前线程的内存模型中获取该变量的值,而是必须从主存中获取,这种做法所带来的唯一负面影响就是效率的折损,但是相比于
synchronized 关键字,其效率优势还是非常明显的。见如下代码:
        public class StopThread {
            private static volatile boolean stopRequested = false;
            public static void main(String[] args) throw InterruptedException
            {
                Thread bgThread = new Thread(new Runnable() {
                    public void run() {
                        int i = 0;
                        while (!stopRequested)
                            i++;
                    }
                });
                bgThread.start();
                TimeUnit.SECONDS.sleep(1);
                stopRequested = true;
            }
        }
    和第一个代码片段相比,这里只是在stopRequested 域变量声明之前加上volatile 关键字,从而保证该变量为易变变量。然而需要说明的是,该关键字并不能完全取代synchronized 同步方式,见如下代码:
        public class Test {
            private static volatile int nextID = 0;
            public static int generateNextID() {
                return nextID++;
            }
        }
    generateNextID方法的用意为每次都给调用者生成不同的ID 值,遗憾的是,最终结果并不是我们期望的那样,当多个线程调用该方法时,极有可能出现重复的ID 值。问题在于增量操作符(++)不是原子的,而是由两个指令构成,首先是读取一个值,然后写回一个新值。由此可见,这两个指令之间的时间窗口极有可能造成数据的不一致。
    修正generateNextID方法的一种方法是在它的声明中增加synchronized修饰符,并删除volatile修饰符。另一种方法是使用JDK(1.5 later)中java.util.concurrent.atomic包提供的AtomicLong类,使用该类性能要明显好于synchronized 的同步方式,见如下修复后的代码:
        public class Test {
            private static final AtomicLong nextID = new AtomicLong();
            public static long generateNextID() {
                return nextID.getAndIncrement();
            }
        }
    避免本条目中所讨论的问题的最佳办法是不共享可变的数据,要么共享不可变的数据,要么压根不共享。
    简而言之,当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步。如果没有同步,就无法保证一个线程所做的修改可以被另一个线程获知。如果只需要线程之间的交互通信,而需要互斥,volatile修饰符就是一种可以接受的同步形式。

2. 避免过度同步
    过度同步所导致的最明显问题就是性能下降,特别是在如今的多核时代,再有就是可能引发的死锁和 一系列不确定性的问题。
    当同步函数或同步代码块内调用了外来方法,如可被子类覆盖的方法,或外部类的接口方法等。由于这些方法的行为存在一定的未知性,如果在同步块内调用了类似的方法,将极有可能给当前的同步带来未知的破坏性。见如下代码: 
        public class ObservableSet<E> extends ForwardingSet<E> {
            public ObservableSet(Set<E> set) {
                super(set);
            }
            private final List<SetObserver<E>> observers = new ArrayList<SetObserver<E>>(); 
            public void addObserver(SetObserver<E> observer) {
                synchronized(observers) {
                    observers.add(observer);
                }
            }
            public boolean removeObserver(SetObserver<E> observer) {
                synchronized(observers) {
                    return observers.remover(observer);
                }
            }
            private void notifyElementAdded(E element) {
                synchronized(observers) {
                    for (SetObserver<E> observer : observers)
                        observer.added(this,element);
                }
            }
            @Override public boolean add(E element) {
                boolean added = super.add(element);
                if (added)
                    notifyElementAdded(element);
                return added;
            }
            @Override public boolean addAll(Collection<? extends E> c) {
                boolean result = false;
                for (E element : c)
                    result |= add(element);
                return result;
            }
        }
    Observer通过调用addObserver方法预定通知,通过调用removeObserver方法取消预定。在这两种情况下,这个回调接口的实例都会被传递给方法:
        public interface SetObserver<E> {
            void added(ObservableSet<E> set,E element);
        }
    如果只是粗略地检测一下,ObservableSet会显得很正常。如下:
        public static void main(String[] args) {
            ObservableSet<Integer> set = new ObservableSet<Integer>(new HashSet<Integer>());
            set.addObserver(new SetObserver<Integer>() {
                public void added(ObservableSet<Integer> s, Integer e) {
                    System.out.println(e);
                }
            });
            for (int i = 0; i < 100; i++)
                set.add(i);
        }
    对于这个测试用例,他完全没有问题,可以保证得到正确的输出,即打印出0-99 的数字。现在我们换一个观察者接口的实现方式,见如下代码片段:
        set.addObserver(new SetObserver<Integer>() {
            public void added(ObservableSet<Integer> s,Integer e) {
                System.out.println(e);
                if (e == 23)
                    s.removeObserver(this);
            }
        });
    对于以上代码,你可能以为这个程序会打印出0~23的数字,之后观察者会取消预定,程序会悄悄地完成它的工作。实际上却是打印出0~23的数字,然后抛出ConcurrentModificationException 异常。问题在于,当notifyElementAdded调用观察者的added方法时,它正处于遍历observers列表的过程中,added方法调用可观察集合的removeObserver方法,从而调用observers.remove。现在有麻烦了,我们正在企图在遍历列表的过程中,将一个元素从列表中删除,这是非法的。notyfyElementAdded方法中的迭代是在一个同步的块中,可以防止并发的修改,但是无法防止迭代线程本身回调到可观察的集合中,也无法防止修改它的observers列表。
   
    下面看另一个例子,编写一个试图取消预定的观察者,但是不直接调用removeObserver,它用另一个线程的服务来完成。这个观察者使用了一个executor service:
        set.addObserver(new SetObserver<Integer>() {
            public void added(final ObservableSet<Integer> s,Integer e) {
                System.out.println(e);
                if (e == 23)
                    ExecutorService executor = Executors.newSingleThreadExecutor();
                    final SetObserver<Integer> observer = this;
                    try{
                        executor.submit(new Runnable(){
                            public void run(){
                                s.removeObserver(observer);
                            }
                        }).get();
                    }catch(ExecutionException ex){
                        throw new AssertionError(ex.getCause());
                    }catch(InterruptedException ex){
                        throw new AssertionError(ex.getCause());
                    }finally{
                        executor.shutdown();
                    }
                 }  
            }
        });
    这一次我们没有遇到异常,而是遭遇了死锁。后台线程调用s.removeObserver,它企图锁定observers,但它无法获得该锁,因为主线程已经有锁了。在这期间,主线程一直等待后台线程来完成对观察者的删除,这正是造成死锁的原因。
    由于Java中synchronized 关键字构成的锁是可重入的,或者说是可递归的,即在同一个线程内可多次调用且不会被阻塞,这种调用不会死锁,就像第一个例子,它会产生一个异常。如果恰恰相反,我们的冲突调用来自于多个线程,那么将会形成死锁。在多线程的应用程序中,死锁是一种比较难以重现和定位的错误。

    为了解决上述问题,我们需要做的一是将调用外部代码的部分移出同步代码块,再有就是针对该遍历,我们需要提前copy 出来一份,并基于该对象进行遍历,从而避免了上面的并发访问冲突,如下:
        private void notifyElementAdded(E element) {
            List<SetObserver<E>> snapshot = null;
            synchronized(observers) {
                snapshot = new ArrayList<SetObserver<E>>(observers);
            }
            for (SetObserver<E> Observer : snapshot)
                Observer.added(this,element);
        }
    事实上,还有一种更好的方法,Java1.5以来,Java类库就提供了一个并发集合,叫做CopyOnWriteArrayList,这是专门为此定制的。它通过重新拷贝整个底层数组,在这里实现所有的写操作。由于内部数组永远不改动,因此迭代不需要锁定,速度也非常快。修改如下:
        public class ObservableSet<E> extends ForwardingSet<E> {
            public ObservableSet(Set<E> set) {
                super(set);
            }
            private final List<SetObserver<E>> observers = new CopyOnWriteArrayList<SetObserver<E>>(); 
            ... ...
    上面的两个修改都避免了出现异常和死锁。

    通常,你应该在同步区域内做尽可能少的工作。获得锁,检查共享数据,根据需要转换数据,然后放掉锁。如果你必须要执行某个耗时的动作,则应设法把这个动作移到同步区域的外面。
    在这个多核的时代,过度同步的实际成本并不是指获取锁所花费的CPU时间,而是指失去了并行的机会,以及因为需要确保每个核都有一个一致的内存视图而导致的延迟。过度同步的另一项潜在开销在于,它会限制VM优化代码执行的能力。
    如果一个可变的类要并发使用,应该使这个类变成是线程安全的,通过内部同步,你还可以获得明显比从外部锁定整个对象更高的并发性。否则,就不要在内部同步,让客户在必要的时候从外部同步。减少不必要的代码同步还可以大大提高程序的并发执行效率,一个非常明显的例子就是StringBuffer,该类在JDK 的早期版本中即以出现,是数据操作同步类,即时我们是以单线程方式调用该类的方法,也不得不承受块同步带来的额外开销。Java 在1.5 中提供了非同步版本的StringBuilder 类,这样在单线程应用中可以消除因同步而带来的额外开销,对于多线程程序,可以继续选择StringBuffer,或者在自己认为需要同步的代码部分加同步块。 所以,当你不确定的时候,就不要同步你的类,而是应该建立文档,注明它不是线程安全的。
    简而言之,为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法。更为一般的讲,要尽量的限制同步区域内部的工作量。当你在设计一个可变类的时候,要考虑一下它们是否应该自己完成同步操作。在现在这个多核时代,这比永远不要过度同步来得更重要。只有当你有足够的理由一定要在内部同步类的时候,才应该这么做,同时还应该将这个决定清楚地写到文档中。

3. executor和task优先于线程
    在Java 1.5 中提供了java.util.concurrent,在这个包中包含了Executor Framework 框架, 这是一个很灵活的基于接口的任务执行工具。该框架提供了非常方便的调用方式和强大的功能,如: 
        ExecutorService executor = Executors.newSingleThreadExecutor(); //创建一个单线程执行器对象。 
        executor.execute(runnable); //提交一个待执行的任务。
        executor.shutdown(); //使执行器优雅的终止。
    你可以利用executor service完成更多的事情,如:可以等待完成一项目特殊的任务,可以等待executor service优雅地完成终止,可以在任务完成时逐个获取这些任务的结果,等等。

    如果想让不止一个线程来处理来自这个队列的请求,只要调用一个不同的静态工厂,这个工厂创建了一种不同的executor service,称作线程池。你可以用固定或者可变数目的线程创建一个线程池。java.util.concurrent.Executors类包含了静态工厂,能为你提供所需的大多数executor。然而,如果你想要来点特别的,可以直接使用ThreadPoolExecutor类。这个类允许你控制线程池操作的几乎每个方面。
    为特殊的应用程序选择executor service是很有技巧的。如果编写的是小程序,或者是轻载的服务器,使用Executors.newCachedThreadPool()通常是个不错的选择,因为它不需要配置,并且在一般情况下能够正确地完成工作。但是对于大负载的服务器来说,缓存的线程池就不是很好的选择了,在缓存的线程池中,被提交的任务没有排成队列,而是直接交给线程执行。如果没有线程可用,就创建一个新的线程。如果服务器负载得太重,以致它所有的CPU都全被占用了,当有更多的任务时,就会创建更多的线程,这样只会使情况变得更糟。对于该种情况,Executors 提供了另外一个工厂方法Executors.newFixedThreadPool(),它为你提供了一个包含固定线程数目的线程池,或者为了最大限度地控制它,就直接使用ThreadPoolExecutor类。

    Executor Framework也有一个可以代替java.util.Timer的东西,即ScheduledThreadPoolExecutor,通过工厂方法Executors.ScheduledThreadPool()可以创建该类。虽然timer使用起来更加容易,但是被调用的线程池executor更加灵活。timer只用一个线程来执行任务,这在面对长期运行的任务时,会影响到定时的准确性。如果timer唯一的线程抛出未被捕获的异常,timer就会停止执行。被调度的线程池executor支持多个线程,并且优雅地从抛出未受检异常的任务中恢复。

4. 并发工具优先于wait和notify
    自从Java1.5发行版本开始,Java平台就提供了更高级的并发工具,它们可以完成以前必须在wait和notify上手写代码来完成的各项工作。既然正确地使用wait和notify比较困难,就应该用更高级的并发工具来代替。
    java.util.concurrent 中更高级的工具分成三类:Executor Framework(在3中简单说明)、并发集合(Concurrent Collection)以及同步器(Synchronizer)。

    并发集合为标准的集合接口提供了高性能的并发实现。为了提高并发性,这些实现在内部管理同步,并发集合不可能排除并发活动,将它锁定没有什么作用,只会使程序的速度变慢。
    有些集合接口已经通过依赖状态的修改操作进行了扩展,将几个基本操作合并到了单个原子操作中。如java.util.concurrent 中提供的并发集合就有更好的并发性,其性能通常数倍于普通集合。如ConcurrentHashMap,它扩展了Map接口,并添加了几个方法,包括putIfAbsent(key, value),当键没有映射时会替它插入一个映射,并返回与键关联的前一个值,如果没有这样的值,则返回null,ConcurrentHashMap除了提供卓越的并发性之外,速度也非常快。换句话说,除非有极其特殊的原因存在,否则在并发的情况下,一定要优先选择ConcurrentHashMap,而不是Collections.syschronizedmap 或者Hashtable。
    java.util.concurrent 包中还提供了阻塞队列,它们会一直等待到可以成功执行为止。如BlockingQueue扩展了Queue接口,并添加了包括take在内的几个方法,它从队列中删除并返回了头元素,如果队列为空就等待。这样就允许将阻塞队列用于工作队列,也称作生产者-消费者队列,大多数ExecutorService实现都使用BlockingQueue,该队列极大的简化了生产者线程和消费者线程模型的编码工作。

    同步器是一些使线程能够等待另一个线程的对象,允许它们协调动作。包括常用的CountDownLatch(倒计数锁存器)和Semaphore,和不常用的CyclicBarrier 和Exchanger。
    CountDownLatch是一次性的障碍,允许一个或者多个线程等待一个或者多个线程来做某些事情。CountDownLatch 的唯一构造函数带有一个int 类型的参数,这个int 参数是指允许所有在等待的线程被处理之前,必须在锁存器上调用countDown 方法的次数。
    现在我们给出一个简单应用场景,然后再给出用CountDownLatch 实现该场景的实际代码。场景描述如下:假设想要构建一个简单的框架,用来给一个动作的并发执行定时。这个框架中包含单个方法,这个方法带有一个执行该动作的executor,一个并发级别(表示要并发执行该动作的次数),以及表示该动作的runnable。所有的工作线程自身都准备好,要在timer 线程启动时钟之前运行该动作。当最后一个工作线程准备好运行该动作时,timer 线程就开始执行,同时允许工作线程执行该动作。一旦最后一个工作线程执行完该动作,timer 线程就立即停止计时。直接在wait 和notify 之上实现这个逻辑至少来说会很混乱,而在CountDownLatch 之上实现则相当简单。见如下示例代码:
        public static long time(Executor executor,int concurrency,final Runnable action) {
            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() {
                    public void run() {
                        ready.countDown();
                        try {
                            start.await();
                            action.run();
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                        } finally {
                            done.countDown();
                        }
                    }
                });
            }
            //等待工作者线程准备可以执行,即所有的工作线程均调用ready.countDown()方法。
            ready.await();
            //这里使用nanoTime,是因为其精确度高于System.currentTimeMills(),且不受系统的实时时钟的调整所影响。
            long startNanos = System.nanoTime();
            //该语句执行后,工作者线程中的start.await()均将被唤醒。
            start.countDown();
            //下面的等待,只有在所有的工作者线程均调用done.countDown()之后才会被唤醒。
            done.await();
            return System.nanoTime() - startNanos;
        }
    注意这个方法使用了三个倒计数锁存器,第一个是ready,工作线程用它来告诉timer线程它们已经准备好了。然后工作线程在第二个锁存上等待,也就是start。当最后一个工作线程调用ready.countDown时,timer线程记录下起始时间,并调用start.countDown,允许所有的工作线程继续进行。然后timer线程在第三个锁存器上等待,直到最后一个工作线程运行完该动作,并调用done.countDown。一旦调用这个,timer线程就会苏醒过来,并记录下结束的时间。

    虽然你始终应该优先使用并发工具,而不是使用wait和notify,但可能必须维护使用了wait和notify的遗留代码。wait方法被用来使线程等待某个条件,它必须在同步区域内部被调用,这个同步区域将对象锁定在了调用wait方法的对象上。下面是使用wait方法的标准模式:
        synchronized(obj){
            while(<condition does not hold>)
                obj.wait()
                ... ...
        }
    始终应该使用wait循环模式调用wait方法:永远不要在循环之外调用wait方法。循环会在等待之前和之后测试条件。

    一个相关的话题是,为了唤醒等待的线程,你应该使用notify还是notifyAll,一个是唤醒单个正在等待的线程,另一个是唤醒所有正在等待的线程。一种常见的说法是,你总是应该使用notifyAll,这是合理而保守的建议。它总会产生正确的结果,因为它可以保证你将会唤醒所有需要被唤醒的线程,你可能也会唤醒其它一些线程,但是这不会影响程序的正确性,这些线程醒来之后,会检查它们正在等待的条件,如果发现条件并不满足,就会继续等待。同时还可以避免来自不相关线程的意外或恶意的等待,否则,这样的等待会吞掉一个关键的通知,使真正的接收线程无限的等待下去。

5. 线程安全性的文档化
    如下的列表概括了线程安全性的几种级别:
(1)不可变的——这个类的实例是不变的,所以,不需要外部的同步。如String、Long、BigInteger等。
(2)无条件的线程安全——这个类的实例是可变的,但是这个类有着足够的内部同步,所以,它的实例可以被并发的使用,无需任何外部同步。如Random、ConcurrentHashMap等。
(3)有条件的线程安全——除了有些方法为进行安全的并发使用而需要外部同步之外,这种线程安全级别与无条件的线程安全相同。如Collections.synchronized包装返回的集合,它们的迭代器要求外部同步。
(4)非线程安全——这个类的实例是可变的。为了并发地使用它们,客户必须利用自己选择的外部同步包围每个方法调用。如ArrayList、HashMap等。
(5)线程对立的——这个类不能安全地被多个线程并发使用,即使所有的方法调用都被外部同步包围。线程对立的根源通常在于、没有同步地修改静态数据。这种类是因为没有考虑到并发性而产生的后果。如System.runFinalzersOnExit(已删除)
    在文档中描述一个有条件的线程安全类要特别小心。你必须指明哪个调用序列需要外部同步,还要指明为了执行这些序列,必须获得哪一把锁。

    当一个类承诺了“使用一个公有可访问的锁对象”时,就意味着允许客户端以原子的方式执行一个方法调用序列,但是,这种灵活性是要付出代价的。首先并发集合使用的那种并发控制,并不能与高性能的内部并发控制相兼容。然后客户客户端还可以发起拒绝服务攻击,他只需超时地保持公有可访问锁即可,这有可能是无意的,也可能是有意的。为了避免这种拒绝服务攻击,应该使用一个私有锁对象来代替同步的方法:
        private final Object lock = new Object();
        public void foo(){
            synchronized(lock){... ... }
        }
    因为这个私有锁对象不能被这个类的客户端程序所访问,所以它们不可能妨碍对象的同步。注意lock域被声明为final的,这样可以防止不小心改变它的内容,而导致不同步访问包含对象的悲惨后果。
    私有锁对象模式只能用在无条件的安全类上。有条件的线程安全类不能使用这种模式,因为在执行某些方法调用序列时,它们的客户端程序必须获得哪把锁。私有锁对象模式特别选用于那些专门为继承设计的类。总之,如果你编写的是无条件的线程安全类,就应该考虑使用私有锁对象来代替同步的方法。这样可以防止客户端程序和子类的不同步干扰,让你能够在后续的版本中灵活地对并发控制采用更加复杂的方法。

6. 慎用延迟初始化
    延迟初始化是延迟到需要域的值时才将它初始化的这种行为。如果永远不需要这个值,这个域就永远不会被初始化。这种方法既选用于静态域,也选用于实例域。虽然延迟初始化主要是一种优化,但它也可以用来打破类和实例初始化的有富循环。和大多数优化一样,对于延迟初始化,最好的建议"除非绝对必要,否则就不要这么做"。延迟初始化如同一把双刃剑,它确实降低了实例对象创建的开销,却增加了访问被延迟初始化的域的开销。
    如果域只在类的实例部分被访问,并且初始化这个域的开销很高,可能就值得进行延迟初始化。当有多个线程时,延迟初始化是需要技巧的,如果两个或者多个线程共享一个延迟初始化的域,采用某种形式的同步是很重要的,否则就可能造成严重的Bug。
    如果利用延迟优化来破坏初始化的循环,就要使用同步访问方法,因为它是最简单、最清楚的替代方法:如下:
        public class TestClass {
            private FieldType field;
            synchronized FieldType getField() {
                if (field == null)
                    field = computeFieldValue();
            return field;
            }
        }
    如果出于性能的考虑而需要对静态域使用延迟初始化,可以考虑使用延迟初始化Holder class 模式:
        public class TestClass {
            private static class FieldHolder {
                static final FieldType field = computeFieldValue();
            }
            static FieldType getField() {
                return FieldHolder.field;
            }
        }
    当getField()方法第一次被调用时,它第一次读取FieldHolder.field,导致FieldHolder 类得到初始化。这种模式的魅力在于,getField 方法没有被同步,并且只执行一个域访问,因此延迟初始化实际上并没有增加任何访问成本。现在的VM 将在初始化该类的时候,同步域的访问。一旦这个类被初始化,VM将修补代码,以便后续对该域的访问不会导致任何测试或者同步。

    如果出于性能的考虑而需要对实例域使用延迟初始化,可使用双重检查模式:
        public class TestClass {
            private volatile FieldType f;
            FieldType getField() {
                FieldType result = f;
                if (result == null) { //如果是数值型的基本类型域时,需用0来检查
                    synchronized(this) {
                        result = f;
                        if (result == null)
                            f = result = computeFieldValue();
                    }
                }
                return result;
            }
        }
    注意在上面的代码中,首先将域字段f 声明为volatile 变量,其语义在之前的条目中已经给出解释,这里将不再赘述。再者就是在进入同步块之前,先针对该字段进行验证,如果不是null,即已经初始化,就直接返回该域字段,从而避免了不必要的同步开销。然而需要明确的是,在同步块内部的判断极其重要,因为在第一次判断之后和进入同步代码块之前存在一个时间窗口,而这一窗口则很有可能造成不同步的错误发生,因此第二次验证才是决定性的。
    在该示例代码中,使用局部变量result 代替volatile 的域字段,可以避免在后面的访问中每次都从主存中获取数据,从而提高函数的运行性能。事实上,这只是一种代码优化的技巧而已。

    如果需要对一个可以接受重复初始化实例域延迟初始化,可使用单重检查模式:
        public class TestClass {
            private volatile FieldType f;
            FieldType getField() {
                FieldType result = f;
                if (result == null) //如果是数值型的基本类型域时,需用0来检查
                    f = result = computeFieldValue();
                return result;
            }
        }

    简而言之,大多数的域应该正常地进行初始化,而不是延迟初始化。如果为了达到性能目标,或者为了破坏有害的初始化循环,而必须延迟初始化一个域,就可以使用相应的延迟初始化方法。

7. 不要依赖于线程调度器
    当有多个线程可以运行时,由线程调度器决定哪些线程将会运行,以及运行多长时间。任何一个合理的操作系统在做出这样的决定时,都会努力做到公正,但是所采用的策略却大相径庭。因此,编写良好的程序不应该依赖于这种策略的细节,任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的。
    要编写健壮的、响应良好的、可移植的多线程应用程序,最好的办法是确保可运行线程(等待的线程并不是可运行的)的平均数量不明显多于处理器的数量。这使得线程调度器没有更多的选择:它只需要运行这些可运行的线程,直到它们不再可运行为止。即使在根本不同的线程调度算法下,这些程序的行为也不会有很大的变化。
    如果线程没有在做有意义的工作,就不应该运行。应适当地规定线程池的大小,并且使任务保持行当地小,彼此独立,任务也不应该大小,否则分配的开销也会影响性能。
    线程不应该一直处于忙——等的状态,即反复地检查一个共享对象,以等待某些事情发生。除了使程序易受到调度器的变化影响之外,忙——等这种做法也会极大地增加处理器的负担,降低了同一机器上其他进程可以完成的有用的工作量。
    如果某一程序不能工作,是因为某些线程无法像其他线程那样获得足够的CPU时间,那么,不要企图通过Thread.yield(它不做实质性的工作,只是将控制权返回给它的调用者)来修正该程序,这样得到的程序仍然是不可移植的,更好的解决办法是重新构造应用程序,以减少可并发运行的线程数量。
    有一种相关的方法是调整线程的优先级,同样,线程优先级是Java平台上最不可移植的特征了。

8. 避免使用线程组
    线程组的初衷是作为一种隔离applet(小程序)的机制,当然是出于安全的考虑,但是它们从来没有真正履行这个承诺。它们很少的用处是同时把Thread的某些基本功能应用到一组线程上,但很少使用。如果你正在设计的一个类需要处理线程的逻辑组,或许就应该使用线程池executor。

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!

留言需要登陆哦

技术博客集 - 网站简介:
前后端技术:
后端基于Hyperf2.1框架开发,前端使用Bootstrap可视化布局系统生成

网站主要作用:
1.编程技术分享及讨论交流,内置聊天系统;
2.测试交流框架问题,比如:Hyperf、Laravel、TP、beego;
3.本站数据是基于大数据采集等爬虫技术为基础助力分享知识,如有侵权请发邮件到站长邮箱,站长会尽快处理;
4.站长邮箱:[email protected];

      订阅博客周刊 去订阅

文章归档

文章标签

友情链接

Auther ·HouTiZong
侯体宗的博客
© 2020 zongscan.com
版权所有ICP证 : 粤ICP备20027696号
PHP交流群 也可以扫右边的二维码
侯体宗的博客