JavaConcurrencyInPractice-基础构建模块

1、同步容器类

  同步容器类包括Vector、HashTable以及在这两者基础上实现的类(如基于Vector实现的Stack),

  还包括通过Collections.synchronizedXxx工厂方法创建的同步封装器类。

  这些同步封装器类通过将底层类的状态封装起来,并对每个公有方法都进行了同步,来实现线程安全性。

 

2、同步容器类的问题

  同步容器类是线程安全的嘛?——是的。

  多线程下使用它们一定安全吗?——不一定。

  同步容器类只保证了多线程下它们的状态是有效的,但是,不保证是我们想要的。

  抛出异常也是一种保证有效的方式,但显然,我们不愿意看到。

//在一个线程执行remove后,另一个线程再去执行get,这时候就会发生ArrayIndexOutOfBoundsException。
//由于没有进行同步,线程获取到的Vector大小是失效的。
public static Object getLast(Vector list) { 
     int lastIndex = list.size() - 1; 
     return list.get(lastIndex); 
} 
public static void deleteLast(Vector list) { 
     int lastIndex = list.size() - 1; 
     list.remove(lastIndex); 
} 

//所以,要对线程操作进行同步,同步容器类是通过自身的锁来保护它的每个方法的,我们也要采取这种加锁方式
public static Object getLast(Vector list) { 
     synchronized (list) { 
         int lastIndex = list.size() - 1; 
         return list.get(lastIndex); 
     } 
} 
public static void deleteLast(Vector list) { 
     synchronized (list) { 
         int lastIndex = list.size() - 1; 
         list.remove(lastIndex); 
     } 
}    

 

 

3、迭代器与ConcurrentModificationException

List<Widget> widgetList = Collections.synchronizedList(new ArrayList<Widget>()); 
... 
// May throw ConcurrentModificationException 
for (Widget w : widgetList) 
     doSomething(w);

 

  在迭代器迭代过程中,如果检测到有其他线程修改了容器,就会抛出ConcurrentModificationException异常。

  与Vector一样,要避免该异常,就要在迭代过程中持有容器的锁。

  直接对容器加锁,会极大地影响效率。

  如果不想加锁,那么还有一个替代方法——复制容器,在副本上进行迭代。CopyOnWrite

public class HiddenIterator { 
     @GuardedBy("this") 
     private final Set<Integer> set = new HashSet<Integer>(); 

     public synchronized void add(Integer i) { set.add(i); } 
     public synchronized void remove(Integer i) { set.remove(i); } 
     public void addTenThings() { 
         Random r = new Random(); 
         for (int i = 0; i < 10; i++) 
            add(r.nextInt()); 
         //打印时,调用了toString方法,而toString方法中是调用了迭代器的
         System.out.println("DEBUG: added ten elements to " + set); 
 } 
}     

 

  虽然加锁可以防止抛出异常,但是必须要对所有对容器进行迭代的地方都进行加锁。

  然而,有些情况下,迭代器的使用是隐藏起来的。

这里得到的教训是:状态同保护它的同步代码相隔越远,开发人员就越容易在使用它时采取正确的同步。

封装对象的状态有助于维持不变性条件,而封装对象的同步机制有助于确保实施同步策略。

 

 

4、并发容器

  同步容器只是确保了在并发访问时的正确性,并发容器则是专门对并发访问而设计的。

通过并发容器来替代同步容器,可以极大地提高伸缩性并降低风险。

 

4.1、并发容器-ConcurrentHashMap

  同步容器类在执行每个操作时都要持有一个锁,这有很大的性能上的影响。

  和HashMap一样,ConcurrentMap也是基于散列的Map实现的,

  但是,ConcurrentHashMap采用了粒度更小的分段锁,

  在不同区域上的访问可以同时进行,以此来提高性能。

  ConcurrentHashMap和其他并发容器一起增强了同步容器类,它们提供的迭代器不会抛出异常,

  因此,在迭代过程中,不需要加锁,这种迭代器具有弱一致性,即创建迭代器后的容器的修改可能通过迭代器看不到。

  对于需要在整个Map上进行计算的方法(如获取大小),由于允许并发的修改,得到的值不准确,可能已经过期了。

  在ConcurrentHashMap中没有实现对Map的加锁以提供独占访问,因此,我们也不能通过客户端加锁的方式来扩展它的功能。

4.2、并发容器-CopyOnWriteArrayList

  只要安全发布一个事实不可变对象,对它的访问就不需要进一步的同步。

  CopyOnWrite容器在每一次修改时,都会发布一个新的容器副本,从而实现可见性。

  每一个线程都是在单独的副本上进行修改的,因此,不用加锁就能进行并发访问。

  但是,对于容器规模较大时,开销就很大了。

  应该在迭代操作远远多于修改操作时,才使用CopyOnWrite容器。

4.3、BlockingQueue和生产者-消费者模式

  

在构建高可靠的应用程序时,有界队列是一种强大的资源管理工具,它们能够抑制并防止产生过多的工作项,使应用程序在负荷过载的情况下更加健壮。

 

   下面的示例是一个BlockingQueue实现的索引服务。

    producer不断从root目录开始检索文件,然后将文件放入BlockingQueue中,

    consumer从BlockingQueue中获取文件并对其进行编号。

    这里使用到了串行线程封闭的技术:

      通过BlockingQueue,将对象的所有权从生产者转移到消费者。

      因为在此过程中,仍然是只有一个线程对对象进行访问,对象依然是线程封闭的。

public class FileCrawler implements Runnable { 
     private final BlockingQueue<File> fileQueue; 
     private final FileFilter fileFilter; 
     private final File root; 
         ... 
     public void run() { 
         try { 
                 crawl(root); 
         } catch (InterruptedException e) { 
             Thread.currentThread().interrupt(); 
         } 
     } 
     private void crawl(File root) throws InterruptedException { 
         File[] entries = root.listFiles(fileFilter); 
         if (entries != null) { 
             for (File entry : entries) 
                 if (entry.isDirectory()) 
                     crawl(entry); 
                 else if (!alreadyIndexed(entry)) 
                     fileQueue.put(entry); 
         } 
     } 
} 

public class Indexer implements Runnable { 
     private final BlockingQueue<File> queue; 

     public Indexer(BlockingQueue<File> queue) { 
         this.queue = queue; 
     } 
     public void run() { 
         try { 
             while (true) 
             indexFile(queue.take()); 
         } catch (InterruptedException e) { 
             Thread.currentThread().interrupt(); 
         } 
     } 
} 

public static void startIndexing(File[] roots) { 
     BlockingQueue<File> queue = new LinkedBlockingQueue<File>(BOUND); 
     FileFilter filter = new FileFilter() { 
         public boolean accept(File file) { return true; } 
     }; 
     for (File root : roots) 
         new Thread(new FileCrawler(queue, filter, root)).start(); 

     for (int i = 0; i < N_CONSUMERS; i++) 
         new Thread(new Indexer(queue)).start(); 
} 
   

 

4.4、Deque列与工作密取

  Deque实现了在队列头和队列尾进行高效的插入和删除。

  工作密取(Work Stealing):

    每个消费者都有自己的双端队列。它们从一端取出工作任务。

    当一个消费者完成了它的工作任务后,它可以到其他线程的上端队列中“偷”任务,从另一端。好勤劳啊!

    一个工作线程找到新任务时,它会将任务放入自己的双端队列中(在工作共享模式中,也可以放入其他线程的工作队列)。

 

5、阻塞与中断方法

  当某方法带有InterruptedException受检异常时,表示该方法是一个阻塞方法。

  如果该方法被中断,那么它会提前结束阻塞状态。

  当自己的方法调用了一个会抛出InterruptedException异常的方法时,自己的方法也变成了阻塞方法。

  这时,需要对InterruptedException进行处理,有两种简单方式:

    1、不处理,不捕获,抛到上层。

    2、捕获异常,恢复中断

public class TaskRunnable implements Runnable { 
     BlockingQueue<Task> queue; 
         ... 
     public void run() { 
         try { 
                 processTask(queue.take()); 
         } catch (InterruptedException e) { 
             // restore interrupted status 
             Thread.currentThread().interrupt(); 
         } 
     } 
}     

 

  千万不要捕获了人家却不作为啊,太没责任心了。

 

6、同步工具类

  同步工具类可以是任意一个对象,只要能根据它自身的状态来协调线程的控制流。

  同步工具类包括:

    BlockingQueue:没有了(满了),你等一下吧。

    Semaphore:没有了,你等一下吧。

    Barrier(栅栏):大家都到了,才能一起走。

    Latch(闭锁):只要我活着,任何人都不能过去。

6.1、闭锁

  闭锁是一种同步工具类,能够延迟线程的进度,直到某个条件发生。

  就像一道门,在闭锁达到结束状态之前,都是关闭的。任何线程不能通过(全部阻塞),

  达到结束状态时,允许全部线程通过。开了就关不了了。

  闭锁可以用于确保活动在满足先验条件时,才能发生:

    1、计算在需要的资源都初始化后才能发生

    2、某个服务在它的依赖服务都启动后才能启动。

    3、操作直到所有参与者都就绪后才能执行。

CountDownLatch是闭锁的一种实现:
A CountDownLatch is initialized with a given count. The await methods block until the current count reaches zero due to invocations of the countDown() method, after which all waiting threads are released and any subsequent invocations of await
return immediately. This is a one-shot phenomenon -- the count cannot be reset.
If you need a version that resets the count, consider using a CyclicBarrier.
public class TestHarness { 
     public long timeTasks(int nThreads, final Runnable task)throws InterruptedException { 
         final CountDownLatch startGate = new CountDownLatch(1);     //起始门,初始值为1
         final CountDownLatch endGate = new CountDownLatch(nThreads);      //结束门,初始值为线程数量   
         for (int i = 0; i < nThreads; i++) { 
             Thread t = new Thread() { 
                 public void run() { 
                     try { 
                            startGate.await(); //阻塞,直到计数值减为0
                            try { 
                                 task.run(); 
                             } finally { 
                                 endGate.countDown(); //计数值减1
                             } 
                     } catch (InterruptedException ignored) { } 
                 } 
             }; 
             t.start(); 
         } 
         long start = System.nanoTime(); 
         startGate.countDown(); 
         endGate.await(); 
         long end = System.nanoTime(); 
         return end-start; 
     } 
}          

 

6.2、栅栏

  闭锁是一次性的,而栅栏可以多次重置。

  闭锁用于等待事件,而栅栏用于等待线程。

  只有在栅栏中等待的线程达到预设的数量时,栅栏才会开启。

CyclicBarrier是一种栅栏的实现方式:
A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point. CyclicBarriers are useful in programs involving a fixed sized party of threads that must occasionally wait for each other. The barrier is called cyclic because it can be re-used after the waiting threads are released. A CyclicBarrier supports an optional Runnable command that is run once per barrier point, after the last thread in the party arrives, but before any threads are released. This barrier action is useful for updating shared-state before any of the parties continue.

 

/**
* 计算被分给若干个Worker来完成,
* 当且仅当所有Worker都完成计算时,才记录结果并进入下一轮计算
*/
public
class CellularAutomata { private final Board mainBoard; private final CyclicBarrier barrier; private final Worker[] workers; public CellularAutomata(Board board) { this.mainBoard = board;     int count = Runtime.getRuntime().availableProcessors();     this.barrier = new CyclicBarrier(count, new Runnable() {       public void run() {         mainBoard.commitNewValues();       }});     this.workers = new Worker[count];     for (int i = 0; i < count; i++)        workers[i] = new Worker(mainBoard.getSubBoard(count, i));    }  
      
private class Worker implements Runnable {     private final Board board;     public Worker(Board board) { this.board = board; }      public void run() {       while (!board.hasConverged()) {         for (int x = 0; x < board.getMaxX(); x++)            for (int y = 0; y < board.getMaxY(); y++)              board.setNewValue(x, y, computeValue(x, y));          try {             barrier.await();          } catch (InterruptedException ex) {              return;         } catch (BrokenBarrierException ex) {             return;         }        }      }    }   public void start() {     for (int i = 0; i < workers.length; i++)     new Thread(workers[i]).start();     mainBoard.waitForConvergence();
  } } }

 

6.3 FutureTask

  

A cancellable asynchronous computation. 
This class provides a base implementation of Future, with methods to start and cancel a computation, query to see if the computation is complete, and retrieve the result of the computation. 
The result can only be retrieved when the computation has completed; the get methods will block if the computation has not yet completed. 
Once the computation has completed, the computation cannot be restarted or cancelled (unless the computation is invoked using runAndReset()).
A FutureTask can be used to wrap a Callable or Runnable object. 
Because FutureTask
implements Runnable, a FutureTask can be submitted to an Executor for execution.

 

public class Preloader { 
   private final FutureTask<ProductInfo> future = new FutureTask<ProductInfo>(new Callable<ProductInfo>() { 
     public ProductInfo call() throws DataLoadException { 
       return loadProductInfo(); 
     } 
   }); 
   private final Thread thread = new Thread(future); 
   public void start() { thread.start(); } 
   public ProductInfo get()  throws DataLoadException, InterruptedException { 
     try { 
       return future.get(); 
     } catch (ExecutionException e) { 
       Throwable cause = e.getCause(); 
       if (cause instanceof DataLoadException) 
         throw (DataLoadException) cause; 
       else 
         throw launderThrowable(cause); 
     } 
   } 
} 

/** If the Throwable is an Error, throw it; if it is a 
 * RuntimeException return it, otherwise throw IllegalStateException 
 */ 
public static RuntimeException launderThrowable(Throwable t) { 
 if (t instanceof RuntimeException) 
   return (RuntimeException) t; 
 else if (t instanceof Error) 
   throw (Error) t; 
 else 
   throw new IllegalStateException("Not unchecked", t); 
} 

 

6.4 Semaphore

A counting semaphore. Conceptually, a semaphore maintains a set of permits. Each acquire() blocks if necessary until a permit is available, and then takes it. 
Each release() adds a permit, potentially releasing a blocking acquirer. However, no actual permit objects are used;
 the Semaphore just keeps a count of the number available and acts accordingly.
Semaphores are often used to restrict the number of threads than can access some (physical or logical) resource. 

 

  下面的例子用Semaphore实现了为容器设置边界的功能:

public class BoundedHashSet<T> { 
   private final Set<T> set; 
   private final Semaphore sem; 
   public BoundedHashSet(int bound) { 
     this.set = Collections.synchronizedSet(new HashSet<T>()); 
     sem = new Semaphore(bound); 
   } 
   public boolean add(T o) throws InterruptedException { 
     sem.acquire(); 
     boolean wasAdded = false; 
     try { 
       wasAdded = set.add(o); 
       return wasAdded; 
     } finally { 
       if (!wasAdded) 
         sem.release(); 
     } 
   } 
   public boolean remove(Object o) { 
     boolean wasRemoved = set.remove(o); 
     if (wasRemoved) 
       sem.release(); 
     return wasRemoved; 
   } 
} 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2021-09-17 12:43  Lqblalala  阅读(55)  评论(0)    收藏  举报