六十七、避免过度同步: 

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

 
  
  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方法中正在遍历该集合。对于该段代码,我只能说我们是幸运的,错误被及时抛出并迅速定位,这是因为我们的调用是在同一个线程内完成的,而Javasynchronized关键字构成的锁是可重入的,或者说是可递归的,即在同一个线程内可多次调用且不会被阻塞。如果恰恰相反,我们的冲突调用来自于多个线程,那么将会形成死锁。在多线程的应用程序中,死锁是一种比较难以重现和定位的错误。为了解决上述问题,我们需要做的一是将调用外部代码的部分移出同步代码块,再有就是针对该遍历,我们需要提前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的早期版本中即以出现,是数据操作同步类,即时我们是以单线程方式调用该类的方法,也不得不承受块同步带来的额外开销。Java1.5中提供了非同步版本的StringBuilder类,这样在单线程应用中可以消除因同步而带来的额外开销,对于多线程程序,可以继续选择StringBuffer,或者在自己认为需要同步的代码部分加同步块。

六十八、executortask优先于线程: 

      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类。

 

 

六十九、并发工具优先于waitnotify

      java.util.concurrent中更高级的工具分成三类:Executor Framework、并发集合(Concurrent Collection)以及同步器(Synchronizer)。相比于java.util中提供的集合类,java.util.concurrent中提供的并发集合就有更好的并发性,其性能通常数倍于普通集合,如ConcurrentHashMap等。换句话说,除非有极其特殊的原因存在,否则在并发的情况下,一定要优先选择ConcurrentHashMap,而不是Collections.syschronizedmap或者Hashtable

      java.util.concurrent包中还提供了阻塞队列,该队列极大的简化了生产者线程和消费者线程模型的编码工作。

      对于同步器,concurrent包中给出了四种主要的同步器对象:CountDownLatchSemaphoreCyclicBarrierExchanger。这里前两种比较常用。在该条目中我们只是简单介绍一个CountDownLatch的优势,该类允许一个或者多个线程等待一个或者多个线程来做某些事情。CountDownLatch的唯一构造函数带有一个int类型的参数 ,这个int参数是指允许所有在等待的线程被处理之前,必须在锁存器上调用countDown方法的次数。

      现在我们给出一个简单应用场景,然后再给出用CountDownLatch实现该场景的实际代码。场景描述如下:

      假设想要构建一个简单的框架,用来给一个动作的并发执行定时。这个框架中包含单个方法,这个方法带有一个执行该动作的executor,一个并发级别(表示要并发执行该动作的次数),以及表示该动作的runnable。所有的工作线程自身都准备好,要在timer线程启动时钟之前运行该动作。当最后一个工作线程准备好运行该动作时,timer线程就开始执行,同时允许工作线程执行该动作。一旦最后一个工作线程执行完该动作,timer线程就立即停止计时。直接在waitnotify之上实现这个逻辑至少来说会很混乱,而在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.      }