`
ccii
  • 浏览: 54788 次
  • 性别: Icon_minigender_1
  • 来自: 成都
社区版块
存档分类
最新评论

EffectiveJava--并发

阅读更多
本章内容:
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。
1
0
分享到:
评论

相关推荐

    java逻辑思维笔试题-effective-java-3rd-edition:有效的Java第3版注释

    java逻辑思维笔试题Effective Java - 第三版笔记 章节索引 02 - 创建和销毁对象 03 - 所有对象通用的方法 04 - 类和接口 05 - 泛型 06 - 枚举和注释 07 - Lambda 和流 08 - 方法 09 - 通用编程 10 - 例外 11 - 并发 ...

    java并发编程实战中文加英文版加源码

    JAVA并发编程实践中文版 英文版 原书源码 带书签 java_concurrency_in_practice.pdf 英文版还是不错的,但是中文版的译者典型的没有技术功底,介绍上说什么专家, 翻译的非常差劲,有些句子都不通顺,都不知道自己去...

    Effective.Java_Java8_并发_java_effectivejava_

    目录:一、创建和销毁对象 (1 ~ 7)二、对于所有对象都通用的方法 (8 ~ 12)三、类和接口 (13 ~ 22)四、泛型 (23 ~ 29)五、枚举和注解 (30 ~ 37)六、方法 ...65)九、并发 (66 ~ 73)十、序列化 (74 ~ 78)

    JAVA并发编程实践.pdf

    《JAVA并发编程实践》随着多核处理器的普及,使用并发成为构建高性能应用程序的关键。Java 5以及6在开发并发程序中取得了显著的进步,提高了Java虚拟机的性能以及并发类的可伸缩性,并加入了丰富的新并发构建块。在...

    java并发最佳书籍Java.Concurrency.in.Practice

    最近在学习java并发相关,发现这边书还挺不错的,这本书是多人合著的,作者中不乏牛人,有 Joshua Bloch ( Effective Java 作者)和 Doug Lea 等等(中文中惯用的表示牛人多的说法)。

    java源码书籍-ideaLearning:Java学习的经典书籍

    java 源码书籍 JAVA Learning ...EffectiveJava -&gt; EffectiveJava Java 网络编程精解 -&gt; network Java 并发编程的艺术 -&gt; ArtConcurrentBook 快学Scala -&gt; scala-impatient IdeaLearning -&gt; 个人学习记录

    开源bbs源码java-Share:分享

    Java并发编程实战 Effective Java 重构 改善既有代码的设计(推荐)w J2EE相关 基础 轻量级Java EE企业级应用实战(了解 AOP,IOC) Spring 3.x企业应用开发实战 高级 Spring 技术内幕 (推荐) 深入剖析tomcat (没...

    Effective Akka英文原版PDF

    这本书可以帮助你快速掌握非常棒的并发框架Akka和Actor编程模式。采用强大的Scala语言,Akka的Actor模式可以帮助你减少并发程序的工作量。本书由官方发布,内容简洁,适合新手入门,照着书中的例子可以快速学习如何...

    Java学习资料汇集(书籍、文章、总结)

    这本书通过深入浅出的方式,详细讲解了Java并发编程的原理、技术和常见问题。 当然,除了书籍,还有很多其他类型的学习资料可以帮助你学习Java。比如,你可以参加在线课程、观看教学视频,或者参与编程社区的讨论...

    java8源码-Blog:个人博客,知识积累!

    《Java并发编程实战》 《Java多线程编程核心技术》 《Java并发编程的艺术》 《Java8实战》 《HTTP权威指南》 《Spring实战》(第4版) 《看透SpringMVC源代码分析与实践》 《Redis入门指南》(第2版) 《Redis实战》...

    是主力于Java的唯品会,关于Java的一些小家.rar

    我们结合唯品会的内部经验,参考《Clean Code》、《Effective Java》等重磅资料,增补了一些条目,同时删减了一些相对不那么通用的规则,让规范更精炼易记。 比如《注释规约》中:“所有的类都必须添加创建者和创建...

    程序员面试刷题的书哪个好-CodeStyleGuidelines:代码风格指南

    程序员面试刷题的书哪个好 luobo-android 开发环境配置 Android Studio 插件安装 打开Settings&gt;Plugins&gt;Browse Repositories安装: FindBugs-IDEA Java代码的静态检查工具。...《Java并发编程的艺术》 And

    代码随想录最新第三版-最强八股文

    Java基础、Java内存模型、Java面向对象、Java集合体系、接口、Lambda表达式、类加载机制、内部类、代理类、Java并发、JVM、Java后端编译、Spring 3. Go defer底层原理、goroutine、select实现机制 4. 算法学习 数组...

    Effective JavaScript

    由于其语法让人联想到Java,并且具有许多脚本语言的共同特性(如函数、数组、字典和正则表达式),因此,具有少量编程经验的人也能够快速学习JavaScript。新手程序员几乎不需要培训就可以开始编写程序,这要归功于...

    notes:JavaJava后端工程师的学习笔记https

    loveincode's notes 学习工作中的一些记录,收藏。 操作系统 , 编译原理 , 计算机网络 , 互联网协议... 常用数据结构与算法 Java 实现 数据结构 与 排序算法 ...Effective Java , HTTP权威指南 , Java

    程序员面试刷题的书哪个好-wiki:维基

    《Java并发编程的艺术》 Android 低难度 《Android4高级编程》 中难度 《Android C++高级编程 使用NDK》 《Android应用性能优化》 《Android开发高手进阶》 《深入理解Android 卷3》 《Android群英传》 《SQLite权威...

    这是一篇有关 在线聊天系统 的系统报告书

    more effective handling of customer requests, and has a simpler and more easy database access methods, easy to maintain and update. view of the above program, the system uses Html, JavaScript and ...

    Packt.Java.7.Concurrency.Cookbook

    Over 60 simple but incredibly effective recipes for mastering multithreaded application development with Java 7 Master all that Java 7 has to offer for concurrent programming Get to grips with thread ...

    javaweb网上购书源码-awesome-books:开发者推荐阅读的书籍

    实战Java高并发程序设计 深入理解Java虚拟机(第2版) JVM高级特性与最佳实践 C++ 书名 豆瓣评分 操作 代码大全(第2版) 前端 书名 豆瓣评分 操作 CSS世界 CSS揭秘 ECMAScript6入门 Head First JavaScript程序设计 ...

    asp.net知识库

    第2章 并发操作的一致性问题 (2) Using sqlite with .NET Visual Studio 2005 中的新 DataSet 特性 MySQL 和 .Net2.0配合使用 与DotNet数据对象结合的自定义数据对象设计 (二) 数据集合与DataTable 与DotNet数据对象...

Global site tag (gtag.js) - Google Analytics