`

Effective Java (并发)

 
阅读更多

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

      在Java中很多时候都是通过synchronized关键字来实现共享对象之间的同步的。事实上,对象同步并不仅限于当多个线程操作同一可变对象时,仍然能够保证该共享对象的状态始终保持一致。与此同时,他还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护的之前所有的修改效果。
      Java的语言规范保证了读写一个变量是原子的,除非这个变量的类型为long或double。换句话说,读取一个非long或double类型的变量,可以保证返回的值是某个线程保存在该变量中的,即时多个线程在没有同步的情况下并发地修改这个变量也是如此。然而需要特别指出的是,这样的做法是非常危险的。即便这样做不会带来数据同步修改的问题,但是他会导致另外一个更为隐匿的错误发生。见如下代码:

复制代码
1     public class StopThread {
2         private static boolean stopRequested = false;
3         public static void main(String[] args) throw InterruptedException {
4             Thread bgThread = new Thread(new Runnable() {
5                 public void run() {
6                     int i = 0;
7                     while (!stopRequested)
8                         i++;
9                 }
10             });
11             bgThread.start();
12             TimeUnit.SECONDS.sleep(1);
13             stopRequested = true;
14         }
15     }
 
复制代码

      对于上面的代码片段,有些人会认为在主函数sleep一秒后,工作者线程的循环状态标志(stopRequested)就会被修改,从而致使工作者线程正常退出。然而事实却并非如此,因为Java的规范中并没有保证在非同步状态下,一个线程修改的变量,在另一个线程中就会立即可见。事实上,这也是Java针对内存模型进行优化的一个技巧。为了把事情描述清楚,我们可以将上面代码中run方法的代码模拟为优化后的代码,见如下修改后的run方法:

复制代码
1 public void run() {
2         int i = 0;
3         if (!stopRequested) {
4             while (true)
5                 i++;
6         }
7     }
 
复制代码

      这种优化被称为提升,正是HotSpot Server VM的工作。
      要解决这个问题并不难,只需在读取和写入stopRequested的时候加入synchronized关键字即可,见如下代码:

复制代码
1     public class StopThread {
2         private static boolean stopRequested = false;
3         private static synchronized void requestStop() {
4             stopRequested = true;
5         }
6         private static synchronized boolean stopRequested() {
7             return stopRequested;
8         }
9         public static void main(String[] args) throw InterruptedException {
10             Thread bgThread = new Thread(new Runnable() {
11                 public void run() {
12                     int i = 0;
13                     while (!stopRequested())
14                         i++;
15                 }
16             });
17             bgThread.start();
18             TimeUnit.SECONDS.sleep(1);
19             requestStop();
20         }
21     }
 
复制代码

      在上面的修改代码中,读写该变量的函数均被加以同步。
      事实上,Java中还提供了另外一种方式用于处理该类问题,即volatile关键字。该单词的直译为“易变的”,引申到这里就是告诉cpu该变量是容易被改变的变量,不能每次都从当前线程的内存模型中获取该变量的值,而是必须从主存中获取,这种做法所带来的唯一负面影响就是效率的折损,但是相比于synchronized关键字,其效率优势还是非常明显的。见如下代码:

复制代码
1     public class StopThread {
2         private static volatile boolean stopRequested = false;
3         public static void main(String[] args) throw InterruptedException {
4             Thread bgThread = new Thread(new Runnable() {
5                 public void run() {
6                     int i = 0;
7                     while (!stopRequested)
8                         i++;
9                 }
10             });
11             bgThread.start();
12             TimeUnit.SECONDS.sleep(1);
13             stopRequested = true;
14         }
15     }
 
复制代码

      和第一个代码片段相比,这里只是在stopRequested域变量声明之前加上volatile关键字,从而保证该变量为易变变量。然而需要说明的是,该关键字并不能完全取代synchronized同步方式,见如下代码:

1 public class Test {

2         private static volatile int nextID = 0;

3         public static int generateNextID() {

4             return nextID++;

5         }

6     }

      generateNextID方法的用意为每次都给调用者生成不同的ID值,遗憾的是,最终结果并不是我们期望的那样,当多个线程调用该方法时,极有可能出现重复的ID值。这是因为++运算符并不是原子操作,而是由两个指令构成,首先是读取该值,加一之后再重新赋值。由此可见,这两个指令之间的时间窗口极有可能造成数据的不一致。如果要修复该问题,我们可以使用JDK(1.5 later)中java.util.concurrent.atomic包提供的AtomicLong类,使用该类性能要明显好于synchronized的同步方式,见如下修复后的代码:

复制代码
1     public class Test {
2         private static final AtomicLong nextID = new AtomicLong();
3         public static long generateNextID() {
4             return nextID.getAndIncrement();
5         }
6     }
复制代码

    
六十七、避免过度同步:

      过度同步所导致的最明显问题就是性能下降,特别是在如今的多核时代,再有就是可能引发的死锁和一系列不确定性的问题。当同步函数或同步代码块内调用了外来方法,如可被子类覆盖的方法,或外部类的接口方法等。由于这些方法的行为存在一定的未知性,如果在同步块内调用了类似的方法,将极有可能给当前的同步带来未知的破坏性。见如下代码:

复制代码
1     public class ObservableSet<E> extends ForwardingSet<E> {
2         public ObservableSet(Set<E> set) {
3             super(set);
4         }
5         private final List<SetObserver<E>> observers = new ArrayList<SetObserver<E>>();
6         public void addObserver(SetObserver<E> observer) {
7             synchronized(observers) {
8                 observers.add(observer);
9             }
10         }
11         public boolean removeObserver(SetObserver<E> observer) {
12             synchronized(observers) {
13                 return observers.remover(observer);
14             }
15         }
16         private void notifyElementAdded(E element) {
17             synchronized(observers) {
18                 for (SetObserver<E> observer : observers)
19                     observer.added(this,element);
20             }
21         }
22         @Override public boolean add(E element) {
23             boolean added = super.add(element);
24             if (added)
25                 notifyElementAdded(element);
26             return added;
27         }
28         @Override public boolean addAll(Collection<? extends E> c) {
29             boolean result = false;
30             for (E element : c)
31                 result |= add(element);
32             return result;
33         }
34     }
 
复制代码

      下面的代码片段是回调接口和测试调用:

复制代码
1     public interface SetObserver<E> {
2         void added(ObservableSet<E> set,E element);
3     }
4    
5     public static void main(String[] args) {
6         ObservableSet<Integer> set = new ObservableSet<Integer>(new HashSet<Integer>());
7         set.addObserver(new SetObserver<Integer>() {
8             public void added(ObservableSet<Integer> s, Integer e) {
9                 System.out.println(e);
10             }
11         });
12         for (int i = 0; i < 100; i++)
13             set.add(i);
14     }
 
复制代码

      对于这个测试用例,他完全没有问题,可以保证得到正确的输出,即打印出0-99的数字。
      现在我们换一个观察者接口的实现方式,见如下代码片段:

复制代码
1     set.addObserver(new SetObserver<Integer>() {
2         public void added(ObservableSet<Integer> s,Integer e) {
3             System.out.println(e);
4             if (e == 23)
5                 s.removeObserver(this);
6         }
7     });
 
复制代码

      对于以上代码,当执行s.removeObserver(this)的时候,将会抛出ConcurrentModificationException异常,因为在notifyElementAdded方法中正在遍历该集合。对于该段代码,我只能说我们是幸运的,错误被及时抛出并迅速定位,这是因为我们的调用是在同一个线程内完成的,而Java中synchronized关键字构成的锁是可重入的,或者说是可递归的,即在同一个线程内可多次调用且不会被阻塞。如果恰恰相反,我们的冲突调用来自于多个线程,那么将会形成死锁。在多线程的应用程序中,死锁是一种比较难以重现和定位的错误。为了解决上述问题,我们需要做的一是将调用外部代码的部分移出同步代码块,再有就是针对该遍历,我们需要提前copy出来一份,并基于该对象进行遍历,从而避免了上面的并发访问冲突,如:

复制代码
1     private void notifyElementAdded(E element) {
2         List<SetObserver<E>> snapshot = null;
3         synchronized(observers) {
4             snapshot = new ArrayList<SetObserver<E>>(observers);
5         }
6         for (SetObserver<E> Observer : snapshot)
7             Observer.added(this,element);
8     }
 
复制代码

      减少不必要的代码同步还可以大大提高程序的并发执行效率,一个非常明显的例子就是StringBuffer,该类在JDK的早期版本中即以出现,是数据操作同步类,即时我们是以单线程方式调用该类的方法,也不得不承受块同步带来的额外开销。Java在1.5中提供了非同步版本的StringBuilder类,这样在单线程应用中可以消除因同步而带来的额外开销,对于多线程程序,可以继续选择StringBuffer,或者在自己认为需要同步的代码部分加同步块。
    
六十八、executor和task优先于线程:

      在Java 1.5 中提供了java.util.concurrent包,在这个包中包含了Executor Framework框架,这是一个很灵活的基于接口的任务执行工具。该框架提供了非常方便的调用方式和强大的功能,如:
      ExecutorService executor = Executors.newSingleThreadExecutor();  //创建一个单线程执行器对象。
      executor.execute(runnable);  //提交一个待执行的任务。
      executor.shutdown();  //使执行器优雅的终止。
      事实上,Executors对象还提供了更多的工厂方法,如适用于小型服务器的Executors.newCachedThreadPool()工厂方法,该方法创建的执行器实现类对于小型服务器来说还是比较有优势的,因为在其内部实现中并没有提供任务队列,而是直接将任务提交给当前可用的线程,如果此时没有可用的线程了,则创建一个新线程来执行该任务。因此在任务数量较多的大型服务器上,由于该机制创建了大量的工作者线程,这将会导致系统的整体运行效率下降。对于该种情况,Executors提供了另外一个工厂方法Executors.newFixedThreadPool(),该方法创建的执行器实现类的内部提供了任务队列,用于任务缓冲。
      相比于java.util.Timer,该框架也提供了一个更为高效的执行器实现类,通过工厂方法Executors.ScheduledThreadPool()可以创建该类。它提供了更多的内部执行线程,这样在执行耗时任务是,其定时精度要优于Timer类。


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

      java.util.concurrent中更高级的工具分成三类:Executor Framework、并发集合(Concurrent Collection)以及同步器(Synchronizer)。相比于java.util中提供的集合类,java.util.concurrent中提供的并发集合就有更好的并发性,其性能通常数倍于普通集合,如ConcurrentHashMap等。换句话说,除非有极其特殊的原因存在,否则在并发的情况下,一定要优先选择ConcurrentHashMap,而不是Collections.syschronizedmap或者Hashtable。
      java.util.concurrent包中还提供了阻塞队列,该队列极大的简化了生产者线程和消费者线程模型的编码工作。
      对于同步器,concurrent包中给出了四种主要的同步器对象:CountDownLatch、Semaphore、CyclicBarrier和Exchanger。这里前两种比较常用。在该条目中我们只是简单介绍一个CountDownLatch的优势,该类允许一个或者多个线程等待一个或者多个线程来做某些事情。CountDownLatch的唯一构造函数带有一个int类型的参数 ,这个int参数是指允许所有在等待的线程被处理之前,必须在锁存器上调用countDown方法的次数。
      现在我们给出一个简单应用场景,然后再给出用CountDownLatch实现该场景的实际代码。场景描述如下:
      假设想要构建一个简单的框架,用来给一个动作的并发执行定时。这个框架中包含单个方法,这个方法带有一个执行该动作的executor,一个并发级别(表示要并发执行该动作的次数),以及表示该动作的runnable。所有的工作线程自身都准备好,要在timer线程启动时钟之前运行该动作。当最后一个工作线程准备好运行该动作时,timer线程就开始执行,同时允许工作线程执行该动作。一旦最后一个工作线程执行完该动作,timer线程就立即停止计时。直接在wait和notify之上实现这个逻辑至少来说会很混乱,而在CountDownLatch之上实现则相当简单。见如下示例代码:

复制代码
1     public static long time(Executor executor,int concurrency,final Runnable action) {
2         final CountDownLatch ready = new CountDownLatch(concurrency);
3         final CountDownLatch start = new CountDownLatch(1);
4         final CountDownLatch done = new CountDownLatch(concurrency);
5         for (int i = 0; i < concurrency; i++) {
6             executor.execute(new Runnable() {
7                 public void run() {
8                     ready.countDown();
9                     try {
10                         start.await();
11                         action.run();
12                     } catch (InterruptedException e) {
13                         Thread.currentThread().interrupt();
14                     } finally {
15                         done.countDown();
16                     }
17                 }
18             });
19             //等待工作者线程准备可以执行,即所有的工作线程均调用ready.countDown()方法。
20             ready.await();
21             //这里使用nanoTime,是因为其精确度高于System.currentTimeMills()。
22             long startNanos = System.nanoTime();
23             //该语句执行后,工作者线程中的start.await()均将被唤醒。
24             start.countDown();
25             //下面的等待,只有在所有的工作者线程均调用done.countDown()之后才会被唤醒。
26             done.await();
27             return System.nanoTime() - startNanos;
28         }
29     }
 
复制代码


七十一、慎用延迟初始化:

      延迟初始化作为一种性能优化的技巧,它要求类的域成员在第一次访问时才执行必要的初始化动作,而不是在类构造的时候完成该域字段的初始化。和大多数优化一样,对于延迟初始化,最好的建议"除非绝对必要,否则就不要这么做"。延迟初始化如同一把双刃剑,它确实降低了实例对象创建的开销,却增加了访问被延迟初始化的域的开销,这一点在多线程访问该域时表现的更为明显。见如下代码:

复制代码
1     public class TestClass {
2         private final FieldType field;
3         synchronized FieldType getField() {
4             if (field == null)
5                 field = computeFieldValue();
6             return field;
7         }
8     }
 
复制代码

      从上面的代码可以看出,在每次访问该域字段时,均需要承担同步的开销。如果在真实的应用中,在多线程环境下,我们确实需要为一个实例化开销很大的对象实行延迟初始化,又该如何做呢?该条目提供了3中技巧:
      1. 对于静态域字段,可以考虑使用延迟初始化Holder class模式:

复制代码
1     public class TestClass {
2         private static class FieldHolder {
3             static final FieldType field = computeFieldValue();
4         }
5         static FieldType getField() {
6             return FieldHolder.field;
7         }
8     }
 
复制代码

      当getField()方法第一次被调用时,它第一次读取FieldHolder.field,导致FieldHolder类得到初始化。这种模式的魅力在于,getField方法没有被同步,并且只执行一个域访问,因此延迟初始化实际上并没有增加任何访问成本。现在的VM将在初始化该类的时候,同步域的访问。一旦这个类被初始化,VM将修补代码,以便后续对该域的访问不会导致任何测试或者同步。
      2. 对于实例域字段,可使用双重检查模式:

复制代码
1     public class TestClass {
2         private volatile FieldType f;
3         FieldType getField() {
4             FieldType result = f;
5             if (result == null) {
6                 synchronized(this) {
7                     result = f;
8                     if (result == null)
9                         f = result = computeFieldValue();
10                 }
11             }
12             return result;
13         }
14     }
 
复制代码

      注意在上面的代码中,首先将域字段f声明为volatile变量,其语义在之前的条目中已经给出解释,这里将不再赘述。再者就是在进入同步块之前,先针对该字段进行验证,如果不是null,即已经初始化,就直接返回该域字段,从而避免了不必要的同步开销。然而需要明确的是,在同步块内部的判断极其重要,因为在第一次判断之后和进入同步代码块之前存在一个时间窗口,而这一窗口则很有可能造成不同步的错误发生,因此第二次验证才是决定性的。
      在该示例代码中,使用局部变量result代替volatile的域字段,可以避免在后面的访问中每次都从主存中获取数据,从而提高函数的运行性能。事实上,这只是一种代码优化的技巧而已。
      针对该技巧,最后需要补充的是,在很多并发程序中,对某一状态的测试,也可以使用该技巧。
      3. 对于可以接受重复初始化实例域字段,可使用单重检查模式:

复制代码
1     public class TestClass {
2         private volatile FieldType f;
3         FieldType getField() {
4             FieldType result = f;
5             if (result == null)
6                 f = result = computeFieldValue();
7             return result;
8         }
9     } 
分享到:
评论

相关推荐

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

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

    java并发编程实战(英文版)

    ### Java并发编程实战知识点概述 #### 一、Java并发特性详解 在《Java并发编程实战》这本书中,作者深入浅出地介绍了Java 5.0和Java 6中新增的并发特性。这些特性旨在帮助开发者更高效、安全地编写多线程程序。书中...

    Effective Java第三版1

    讨论Java并发工具和最佳实践,如何编写线程安全的代码,以及避免并发问题。 ### 第九章 序列化 解释如何有效地实现序列化,以及序列化可能带来的安全和性能问题。 ### 第十章 注解 介绍注解的使用,自定义注解的...

    JAVA并发编程实践.pdf

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

    effectiveJava课件分享

    在编程领域,特别是Java开发中,"Effective Java"是一本非常经典的书籍,由Joshua Bloch撰写,书中提出了一系列最佳实践和设计原则,以帮助开发者编写出更高效、更安全的代码。根据提供的标题和描述,我们将探讨三个...

    《Effective Java》读书分享.pptx

    "Effective Java 读书分享" 《Effective Java》读书分享.pptx 是一本 Java 编程语言指南,旨在帮助开发者编写高质量、可维护的 Java 代码。该书包含 90 个条目,每个条目讨论一条规则,涵盖了 Java 编程语言的...

    Effective.Enterprise.Java.中文版 高清pdf 下载

    其内容涵盖了Java编程的多个方面,包括设计模式、并发处理、异常处理、类库使用等,为程序员提供了实用的指导原则和最佳实践。 在Java的世界里,"Effective"系列书籍以其深入浅出的讲解和实用的编程技巧深受好评。...

    Java并发实践英文版(Java Concurrency in Practice)

    ### Java并发实践知识点详解 #### 一、书籍基本信息与作者介绍 - **书名**:《Java并发实践》(Java Concurrency in Practice) - **作者**:Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David ...

    java 并发编程实践 英文版 English

    《Java并发编程实践》一书深入探讨了Java平台在Java 5.0和Java 6中引入的并发特性,以及并发编程的一般性原理。本书不仅由参与设计和实现这些特性的团队撰写,而且得到了业界专家的高度评价,如Sun Microsystems的...

    Effective.Java_Java8_并发_java_effectivejava_

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

    Effective Java.zip

    《Effective Java》是一本经典Java编程指南,作者是Joshua Bloch,这本书深入探讨了如何编写高质量、高效、可维护的Java代码。以下是对压缩包中各章节主要知识点的详细阐述: 1. **第2章 创建和销毁对象** - 单例...

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

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

    JAVA并发编程实践 高清 中文版 PDF

    《JAVA并发编程实践》是一本关于Java多线程编程技术的书籍,由多位在Java并发编程领域有深厚背景的专家共同撰写。该书内容详尽,不仅深入讨论了并发编程的基础理论,还提供了大量实战技巧和案例分析,对于希望提高...

    effective enterprise java 中文版

    《Effective Enterprise Java》是一本由James Gosling、Bill Venners和Cay S. Horstmann合著的经典著作,旨在帮助Java开发者深入理解和利用企业级Java技术。这本书提供了78条具体的建议,涵盖了从设计模式到并发编程...

    Java并发程序设计教程

    参考高质量的书籍和在线资源,如《Java并发编程实战》、《Effective Java》等,是深入学习Java并发的关键。 #### 十一、业界发展情况:GPGPU、OpenCL 随着并行计算的发展,通用图形处理器(GPGPU)和开放计算语言...

    effecctivejava 第三版中文

    《Effective Java》是Java编程领域的一本经典著作,由Joshua Bloch撰写,现在已经更新到第三版。这本书深入探讨了如何编写高效、可维护且设计良好的Java代码,是每一个Java开发者提升技能的重要参考资料。以下是对该...

    [Java并发编程实践].(Java.Concurrency.in.Practice).Brian.Goetz.文字版

    ### Java并发编程实践 #### 背景与目标受众 《Java并发编程实践》由Brian Goetz撰写,是一本深入探讨Java并发特性的专著。本书面向那些希望在Java编程领域达到更高层次的专业人士,尤其是那些希望掌握多线程编程...

    effectiveJava的笔记

    《Effective Java》是Java开发领域的经典著作,由Joshua Bloch编写,旨在提供一系列实用的编程准则和最佳实践。这本书的第三版包含了大量更新,涵盖了Java语言和平台的新发展,如Java 8和Java 9的新特性。以下是对...

    Effective-Java:Effective Java中文版第二版示例代码

    《Effective Java》是Java开发领域的经典著作,由Joshua Bloch撰写,中文版第二版更是深受广大Java开发者喜爱。这本书提供了许多实用的编程实践和经验教训,帮助开发者编写出更高效、可维护的Java代码。这里我们将...

Global site tag (gtag.js) - Google Analytics