85977328--并发--高级 二

原子变量  
    java.util.concurrent.atomic包定义了对单一变量进行原子操作的类。原子变量类提供了在整数或者对象引用上的细粒度原子操作(因此可伸缩性更高),并使用了现代处理器中提供的底层并发原语(例如比较并交换[compare-and-swap])。所有的类都提供了get和set方法,可以使用它们像读写volatile变量一样读写原子类。就是说,同一变量上的一个set操作对于任意后续的get操作存在happens-before关系。原子的compareAndSet方法也有内存一致性特点,就像应用到整型原子变量中的简单原子算法。    
    为了看看这个包如何使用,让我们返回到最初用于演示线程干扰的Counter类:  
Java代码    收藏代码
  1. public class Counter {  
  2.     private int c = 0;  
  3.   
  4.     public void increment() {  
  5.         c++;  
  6.     }  
  7.   
  8.     public void decrement() {  
  9.         c--;  
  10.     }  
  11.   
  12.     public int value() {  
  13.         return c;  
  14.     }  
  15. }  

使用同步是一种使Counter类变得线程安全的方法,如SynchronizedCounter:  
Java代码    收藏代码
  1. public class SynchronizedCounter {  
  2.     private int c = 0;  
  3.   
  4.     public synchronized void increment() {  
  5.         c++;  
  6.     }  
  7.   
  8.     public synchronized void decrement() {  
  9.         c--;  
  10.     }  
  11.   
  12.     public synchronized int value() {  
  13.         return c;  
  14.     }  
  15. }  

对于这个简单的类,同步是一种可接受的解决方案。但是对于更复杂的类,我们可能想要避免不必要同步所带来的活跃度影响。将int替换为AtomicInteger允许我们在不进行同步的情况下阻止线程干扰,如AtomicCounter:  
Java代码    收藏代码
  1. import java.util.concurrent.atomic.AtomicInteger;  
  2.   
  3. public class AtomicCounter {  
  4.     private AtomicInteger c = new AtomicInteger(0);  
  5.   
  6.     public void increment() {  
  7.         c.incrementAndGet();  
  8.     }  
  9.   
  10.     public void decrement() {  
  11.         c.decrementAndGet();  
  12.     }  
  13.   
  14.     public int value() {  
  15.         return c.get();  
  16.     }  
  17. }  


并发集合  
ConcurrentHashMap  
    使用的是一种更细粒度的锁,这种机制称为分段锁(Lock Striping)。在这种机制中,任意数量的读取线程可以并发的访问Map,执行读取操作的线程和执行写入操作的线程可以并发地访问Map,并且一定数量的写入线程可以并发地修改Map。  
    迭代的时候不会抛出ConcurrentModificationException,迭代器是拥有"弱一致性",而非"即时失败。size返回的是一个估计值。  
    ConcurrentNavigableMap是ConcurrentMap的子接口,支持近似匹配。ConcurrentNavigableMap的标准实现是ConcurrentSkipListMap,它是TreeMap的并发模式。ConcurrentSkipListMap和ConcurrentSkipListSet分别作为同步的SortedMap和SortedSet的并发替代品。  
Java代码    收藏代码
  1. import java.util.Map;  
  2.   
  3. public interface ConcurrentMap<K, V> extends Map<K, V> {  
  4.    
  5.     /** 
  6.      * 仅当K没有相应的映射值才插入 
  7.      * @param key 
  8.      * @param value 
  9.      * @return 
  10.      */  
  11.     V putIfAbsent(K key, V value);  
  12.   
  13.     /** 
  14.      * 仅当K被映射到V时才移除 
  15.      * @param key 
  16.      * @param value 
  17.      * @return 
  18.      */  
  19.     boolean remove(Object key, Object value);  
  20.   
  21.     /** 
  22.      * 仅当 K被映射到oldValue时才替换为newValue 
  23.      * @param key 
  24.      * @param oldValue 
  25.      * @param newValue 
  26.      * @return 
  27.      */  
  28.     boolean replace(K key, V oldValue, V newValue);  
  29.   
  30.     /** 
  31.      * 仅当K被映射到某个值时才替换为newValue 
  32.      * @param key 
  33.      * @param value 
  34.      * @return 
  35.      */  
  36.     V replace(K key, V value);  
  37. }  


CopyOnWriteArrayList  
    在每次修改时,都会创建并重新发布一个新的容器副本,从而实现复制。“写入时复制”返回迭代器不会抛出ConcurrentModificationException,并且返回的元素与迭代器创建时的元素完全一致,而不必考虑之后修改操作所带来的影响。  

并发随机数  
    在JDK7中,java.util.concurrent包含了一个相当便利的类,ThreadLocalRandom,当应用程序期望在多个线程或ForkJoinTasks中使用随机数时。 对于并发访问,使用TheadLocalRandom代替Math.random()可以减少竞争,从而获得更好的性能。 你只需调用ThreadLocalRandom.current(), 然后调用它的其中一个方法去获取一个随机数即可。下面是一个例子:  
Java代码    收藏代码
  1. int r = ThreadLocalRandom.current().nextInt(4,77);   
概述  
    几乎所有应用程序,都会使用某种形式的缓存。重用之前的计算结果,能降低延时,提高吞吐量,但却要消耗更多的内存。用内存“换”CPU。缓存看上去非常简单,然而简单的缓存可能会将性能瓶颈装变为可伸缩性瓶颈,即使缓存是用于提升单线程的性能。笔者会循序渐进的介绍缓存的使用方法演进。  

模拟定义接口和功能  
声明一个计算函数,使用泛型,输入是A,输出是V。然后我们实现这个接口,再开发一个包装器,可以缓存计算的结果。  
接口:  
Java代码    收藏代码
  1. package com.chinaso.phl;  
  2.   
  3. /** 
  4.  * @author piaohailin 
  5.  * @date 2014-4-23 
  6.  */  
  7. public interface Computable<A, V> {  
  8.     V compute(A arg) throws InterruptedException;  
  9. }  

实现:  
Java代码    收藏代码
  1. package com.chinaso.phl;  
  2.   
  3. import java.math.BigInteger;  
  4. /** 
  5.  * @author piaohailin 
  6.  * @date 2014-4-23 
  7.  */  
  8. public class ExpensiveFunction implements Computable<String, BigInteger> {  
  9.   
  10.     @Override  
  11.     public BigInteger compute(String arg) throws InterruptedException {  
  12.         return new BigInteger(arg);  
  13.     }  
  14.   
  15. }  


使用HashMap和同步机制来初始化缓存  
Java代码    收藏代码
  1. package com.chinaso.phl;  
  2.   
  3. import java.util.HashMap;  
  4. import java.util.Map;  
  5.   
  6. import net.jcip.annotations.GuardedBy;  
  7.   
  8. /** 
  9.  * @author piaohailin 
  10.  * @date 2014-4-23 
  11.  */  
  12. public class Memorizer1<A, V> implements Computable<A, V> {  
  13.     @GuardedBy("this")  
  14.     private final Map<A, V>        cache = new HashMap<A, V>();  
  15.     private final Computable<A, V> c;  
  16.   
  17.     public Memorizer1(Computable<A, V> c) {  
  18.         this.c = c;  
  19.     }  
  20.   
  21.     @Override  
  22.     public synchronized V compute(A arg) throws InterruptedException {  
  23.         V result = cache.get(arg);  
  24.         if (result == null) {  
  25.             result = c.compute(arg);  
  26.             cache.put(arg, result);  
  27.         }  
  28.         return result;  
  29.     }  
  30. }  

    这种方法是最基本的缓存用法,是安全的。但是有一个明显的可伸缩性问题:每次只有一个线程能够执行compute。如果另一个线程正在计算结果,那么其他调用coumpute的线程可能被阻塞很长时间。如果有多个线程在排队等待还未计算出的结果,那么compute方法的计算时间可能比没有“记忆”操作的计算时间更长。  
85977328--并发--高级 二_第1张图片  

用ConcurrentHashMap替换HashMap  
Java代码    收藏代码
  1. package com.chinaso.phl;  
  2.   
  3. import java.util.Map;  
  4. import java.util.concurrent.ConcurrentHashMap;  
  5.   
  6. /** 
  7.  * @author piaohailin 
  8.  * @date 2014-4-23 
  9.  */  
  10. public class Memorizer2<A, V> implements Computable<A, V> {  
  11.   
  12.     private final Map<A, V>        cache = new ConcurrentHashMap<A, V>();  
  13.     private final Computable<A, V> c;  
  14.   
  15.     public Memorizer2(Computable<A, V> c) {  
  16.         this.c = c;  
  17.     }  
  18.   
  19.     @Override  
  20.     public V compute(A arg) throws InterruptedException {  
  21.         V result = cache.get(arg);  
  22.         if (result == null) {  
  23.             result = c.compute(arg);  
  24.             cache.put(arg, result);  
  25.         }  
  26.         return result;  
  27.     }  
  28. }  

    Memorizer2比Memorizer1有着更好的并发行为,ConcurrentHashMap是线程安全的,所以不需要同步compute方法。但是作为缓存仍然有问题----2个线程同时调用的compute的时候,可能会导致计算得到相同的值。因为缓存是用来避免相同的数据被计算多次,但对于更通用的缓存机制来说,这种情况是更糟糕的,对于提供单词初始化对象缓存来说,这个漏洞会存在安全风险。  
85977328--并发--高级 二_第2张图片
    Memorizer2问题在于,如果某个线程启动了一个开销很大的计算,而其他线程并不知道这个计算正在进行,那么很可能会重复这个计算。我们希望通过某种方法来表达“线程X正在计算f(1226)”这种情况,这样当另一个线程查找f(1226)时,他能够知道最高效的方法是等待线程X计算结束,然后去查询缓存“f(1226)的结果是多少”。  


基于FutureTask的Memorizer封装器  
    FutureTask表示一个计算过程,这个过程可能已经计算完成,也可能正在进行。如果有结果可用,那么FutureTask.get将立即返回结果,否则它会一直阻塞,直到结果计算出来再将其返回。  
Java代码    收藏代码
  1. package com.chinaso.phl;  
  2.   
  3. import java.util.Map;  
  4. import java.util.concurrent.Callable;  
  5. import java.util.concurrent.ConcurrentHashMap;  
  6. import java.util.concurrent.ExecutionException;  
  7. import java.util.concurrent.FutureTask;  
  8.   
  9. /** 
  10.  * @author piaohailin 
  11.  * @date 2014-4-23 
  12.  */  
  13. public class Memorizer3<A, V> implements Computable<A, V> {  
  14.   
  15.     private final Map<A, FutureTask<V>> cache = new ConcurrentHashMap<A, FutureTask<V>>();  
  16.     private final Computable<A, V>      c;  
  17.   
  18.     public Memorizer3(Computable<A, V> c) {  
  19.         this.c = c;  
  20.     }  
  21.   
  22.     @Override  
  23.     public V compute(final A arg) throws InterruptedException {  
  24.         FutureTask<V> f = cache.get(arg);  
  25.         if (f == null) {  
  26.             Callable<V> eval = new Callable<V>() {  
  27.                 @Override  
  28.                 public V call() throws Exception {  
  29.                     return c.compute(arg);  
  30.                 }  
  31.             };  
  32.             FutureTask<V> ft = new FutureTask<V>(eval);  
  33.             f = ft;  
  34.             cache.put(arg, ft);  
  35.             ft.run(); // 这里调用的是c.compute(arg);  
  36.         }  
  37.         try {  
  38.             return f.get();  
  39.         } catch (ExecutionException e) {  
  40.             throw new InterruptedException(e.getMessage());  
  41.         }  
  42.     }  
  43. }  

    Memorizer3进一步改进了代码。基于ConcurrentHashMap表现出了更好的并发性。但是他仍然有一个漏洞,就是2个线程计算相同值的漏洞。这个漏洞的概率远远小于Memorizer2的情况。但是compute方法中的if代码块仍然是非原子的“先检查再执行”操作。  
85977328--并发--高级 二_第3张图片

基于原子操作putIfAbsent的改进  
Java代码    收藏代码
  1. package com.chinaso.phl;  
  2.   
  3. import java.util.concurrent.Callable;  
  4. import java.util.concurrent.CancellationException;  
  5. import java.util.concurrent.ConcurrentHashMap;  
  6. import java.util.concurrent.ConcurrentMap;  
  7. import java.util.concurrent.ExecutionException;  
  8. import java.util.concurrent.FutureTask;  
  9.   
  10. /** 
  11.  * @author piaohailin 
  12.  * @date 2014-4-23 
  13.  */  
  14. public class Memorizer4<A, V> implements Computable<A, V> {  
  15.   
  16.     private final ConcurrentMap<A, FutureTask<V>> cache = new ConcurrentHashMap<A, FutureTask<V>>();  
  17.     private final Computable<A, V>                c;  
  18.   
  19.     public Memorizer4(Computable<A, V> c) {  
  20.         this.c = c;  
  21.     }  
  22.   
  23.     @Override  
  24.     public V compute(final A arg) throws InterruptedException {  
  25.         FutureTask<V> f = cache.get(arg);  
  26.         if (f == null) {  
  27.             Callable<V> eval = new Callable<V>() {  
  28.                 @Override  
  29.                 public V call() throws Exception {  
  30.                     return c.compute(arg);  
  31.                 }  
  32.             };  
  33.             FutureTask<V> ft = new FutureTask<V>(eval);  
  34.             // 只有第一个线程添加的时候才会为空,第二个线程此处会获取之前的FutureTask  
  35.             f = cache.putIfAbsent(arg, ft);  
  36.             if (f == null) {  
  37.                 f = ft;  
  38.                 ft.run(); // 这里调用的是c.compute(arg);  
  39.             }  
  40.         }  
  41.         try {  
  42.             return f.get();  
  43.         } catch (CancellationException e) {  
  44.             cache.remove(arg, f);  
  45.             return null;  
  46.         } catch (ExecutionException e) {  
  47.             throw new InterruptedException(e.getMessage());  
  48.         }  
  49.     }  
  50. }  

    Memorizer4使用了putIfAbsent的原子方法,从而有效避免了Memorizer3的漏洞。但是这个缓存仍然存在问题。  
    缓存污染  
    缓存逾期  
    缓存清理
 
CountDownLatch,一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。  
主要方法  
public CountDownLatch(int count);  
构造方法参数指定了计数的次数  
public void countDown();  
countDown方法,当前线程调用此方法,则计数减一  
public void await() throws InterruptedException  
awaint方法,调用此方法会一直阻塞当前线程,直到计时器的值为0  

示例代码  
Java代码    收藏代码
  1. import java.text.SimpleDateFormat;  
  2. import java.util.Date;  
  3. import java.util.concurrent.CountDownLatch;  
  4.   
  5. /** 
  6.  *  
  7.  * @author piaohailin 
  8.  * @date   2014-5-6 
  9.  */  
  10. public class CountDownLatchDemo {  
  11.     final static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
  12.   
  13.     public static void main(String[] args) throws InterruptedException {  
  14.         CountDownLatch latch = new CountDownLatch(2);//两个工人的协作  
  15.         Worker worker1 = new Worker("piaohailin"5000, latch);  
  16.         Worker worker2 = new Worker("chinaso"8000, latch);  
  17.         worker1.start();//  
  18.         worker2.start();//  
  19.         latch.await();//等待所有工人完成工作  
  20.         System.out.println("all work done at " + sdf.format(new Date()));  
  21.     }  
  22.   
  23.     static class Worker extends Thread {  
  24.         String         workerName;  
  25.         int            workTime;  
  26.         CountDownLatch latch;  
  27.   
  28.         public Worker(String workerName, int workTime, CountDownLatch latch) {  
  29.             this.workerName = workerName;  
  30.             this.workTime = workTime;  
  31.             this.latch = latch;  
  32.         }  
  33.   
  34.         public void run() {  
  35.             System.out.println("Worker " + workerName + " do work begin at " + sdf.format(new Date()));  
  36.             doWork();//工作了  
  37.             System.out.println("Worker " + workerName + " do work complete at " + sdf.format(new Date()));  
  38.             latch.countDown();//工人完成工作,计数器减一  
  39.   
  40.         }  
  41.   
  42.         private void doWork() {  
  43.             try {  
  44.                 Thread.sleep(workTime);  
  45.             } catch (InterruptedException e) {  
  46.                 e.printStackTrace();  
  47.             }  
  48.         }  
  49.     }  
  50.   
  51. }  

阿姆达尔定律  
阿姆达尔(Amdahl)定律是计算机系统设计的重要定量原理之一,于1967年由IBM360系列机的主要设计者阿姆达尔首先提出。该定律是指:系统中对某一部件采用更快执行方式所能获得的系统性能改进程度,取决于这种执行方式被使用的频率,或所占总执行时间的比例。阿姆达尔定律实际上定义了采取增强(加速)某部分功能处理的措施后可获得的性能改进或执行时间的加速比。简单来说是通过更快的处理器来获得加速是由慢的系统组件所限制。  
阿姆达尔曾致力于并行处理系统的研究。对于固定负载情况下描述并行处理效果的加速比s,阿姆达尔经过深入研究给出了如下公式:  
S=1/(a+(1-a)/n)  
其中,a为串行计算部分所占比例,n为并行处理结点个数。这样,当a=0时,最大加速比s=n;当a=1时,最小加速比s=1;当n→∞时,极限加速比s→ 1/a,这也就是加速比的上限。例如,若串行代码占整个代码的25%,则并行处理的总体性能不可能超过4。这一公式已被学术界所接受,并被称做“阿姆达尔定律”(Amdahl law)。
    如果在某算法中,一个线程的失败或挂起不会导致其他线程也失败挂起,那么这种算法就被称为非阻塞算法。  
    如果在算法的每个步骤中都存在某个线程能够执行下去,那么这种算法也被称为无所算法(Lock-Free)

你可能感兴趣的:(并发)