java多线程(二)——concurrent库

来源:互联网 发布:负载均衡网络拓扑图 编辑:程序博客网 时间:2024/06/03 23:42

简单介绍

   Util.concurrent包括互斥、信号量、集合类、工作队列、线程池实现

就算我们不主动使用线程,很多功能隐含有线程的实现:定时、Servlet、RMI、JSP

线程安全

多线程环境下,如何保证对变量的正确get和set,在JDK1.3之前只能使用synchronized

集合

集合是线程安全的重灾区,因为其除了get和set,还有iterator迭代、删除和判空什么的,不仅需要对单个操作考虑,还需要对组合操作考虑:如先判断contain,再put等。

         Concurrent中的集合有更好的并发

弱一致迭代器

         Java.util中的迭代器是fail-fast的,即假设在迭代中不会修改集合,如果有会报异常ConcurrentModificationException

         java.util.concurrent集合返回的迭代器称为弱一致的(weakly consistent)迭代器。对于这些类,如果元素自从迭代开始已经删除,且尚未由 next() 方法返回,那么它将不返回到调用者。如果元素自迭代开始已经添加,那么它可能返回调用者,也可能不返回。在一次迭代中,无论如何更改底层集合,元素不会被返回两次。

CopyOnWriteArrayList 和 CopyOnWriteArraySet

集合有的已经是线程安全的,如HashTable和vector;通过Collections.synchronizedMap()、synchronizedList() 和synchronizedSet() 其余集合也可以是线程安全的。这种方式的问题是:1. 迭代是fail-fast的,即迭代时执行修改会抛出ConcurrentModificationException错误;

         CopyOnWriteArrayList类通过每次添加或删除元素时创建支持数组的新副本,避免了这个问题,但是进行中的迭代保持对创建迭代器时的当前副本进行操作。当写入远小于迭代时,这种方法有很好的性能和并发性。

ConcurrentHashMap

         Hashtable 和Collections.synchronizedMap 通过同步每个方法获得线程安全。这意味着当一个线程执行一个 Map 方法时,无论其他线程要对 Map 进行什么样操作,都不能执行,直到第一个线程结束才可以。

对比来说,ConcurrentHashMap 允许多个读取几乎总是并发执行,读和写操作通常并发执行,多个同时写入经常并发执行。结果是当多个线程需要访问同一 Map 时,可以获得更高的并发性。

         putIfAbsend():不存在key,则插入

Queue

代表FIFO集合,比List效率更高

实现类有:PriorityQueue(优先队列);ArrayBlockQueue(固定大小的缓存区,已满插入、已空提取都会阻塞); LinkedBlockingQueue(无界阻塞);ConcurrentLinkedQueue(无界线程安全队列,用于多线程)

Deque

双端队列,支持FIFO和LIFO,实现类有:ArrayDeQueue(作为stack和queue使用,非线程同步)

原子变量

在单个变量上不使用锁也能实现线程安全,普通用户很少使用

多线程实现安全访问改变和获取值,内部使用wait/notify实现,作用是在线程之间发送通知,每次重新调用setValue(),任何在wait 中阻塞等待的线程都将得到通知。

几乎java.util.concurrent 中的所有类都是在 ReentrantLock 之上构建的,ReentrantLock 则是在原子变量类的基础上构建的。所以,虽然仅少数并发专家使用原子变量类,但java.util.concurrent 类的很多可伸缩性改进都是由它们提供的。

原子变量主要用于多线程频繁访问和更新的字段,如计数器或生成序号的自然机制,如:AtomicLong(序号)  AtomicBoolean(开关)  AtomicInteger(计数器)

threadLocal

对一个变量在不同的线程中创建副本,该副本在同一线程内共享,不同线程间不会产生冲突,多用于隔离多个线程间的共享冲突

线程池

多在服务器应用程序中,用于处理远程来源的大量短小任务,这样频繁的创建和销毁就有点浪费了,所以有线程池。

Executor

最顶级接口,runnable对象运行于哪个线程由其实现类决定,可以运行于后台线程、线程池、新的线程、其他JVM。

ExecutorService

对Executor的扩展,还管理执行任务的生命周期,这使它们更易于管理,并向生命可能比单独 Executor 的生命更长的应用程序提供服务。提供方法:shuwDown()  isTerminate()等

线程池大小

     Amdahl公式, 用 WT 表示每项任务的平均等待时间,ST 表示每项任务的平均服务时间(计算时间)。则 WT/ST 是每项任务等待所用时间的百分比。对于 N 处理器系统,池中可以近似有 N*(1+WT/ST) 个线程。

     好消息是不需要精确估计

创建线程池

通过Executors工厂方法创建线程池:

• Executors.newCachedThreadPool() 创建不限制大小的线程池,但是当以前创建的线程可以使用时将重新使用那些线程。如果没有现有线程可用,将创建新的线程并将其添加到池中。使用不到 60 秒的线程将终止并从缓存中删除。

• Executors.newFixedThreadPool(int n) 创建线程池,其重新使用在不受限制的队列之外运行的固定线程组。在关闭前,所有线程都会因为执行过程中的失败而终止,如果需要执行后续任务,将会有新的线程来代替这些线程。

• Executors.newSingleThreadExecutor() 创建 Executor,其使用在不受限制的队列之外运行的单一工作线程,与 Swing 事件线程非常相似保证顺序执行任务,在任何给定时间,不会有多个任务处于活动状态。

实例如下:

class ReliableWebServer {
  Executor pool =Executors.newFixedThreadPool(7);
    public static void main(String[] args) {
    ServerSocket socket = new ServerSocket(80);
      while (true) {
      final Socket connection =socket.accept();
      Runnable r = new Runnable() {
        public void run() {
         handleRequest(connection);
        }
      };
      pool.execute(r);
    }
  }
}
 
ThreadFactory

通过在工厂方法中传入 ThreadFactory的实现,可实现线程创建的自定义,如可以包含有用的线程名称,并且这些线程是守护线程,属于特定线程组或具有特定优先级,实现如下:

public class DaemonThreadFactory implements ThreadFactory {
    public Thread newThread(Runnable r) {
        Thread thread = newThread(r);
        thread.setDaemon(true);
        return thread;
    }
}
 
生产者消费者

         Concurrent中使用CompetitionService实现生产者和消费者模式,其基本思想是:通过构造函数参数传入( Executor,Queue(可选,不传入则为LinkedBlockQueue) )。生产者使用submit提交Callable对象到queue,内部使用参数传入的Executor进行,消费者使用take取得执行的结果Futue,可用于管理异步IO。

实例1

假定您想使用任务集中的第一个非 null 结果,而忽略任何遇到异常的任务,并且在第一个任务就绪时取消其他所有任务:

void solve(Executor e, Collection<Callable<Result>> solvers)
      throws InterruptedException {
       CompletionService<Result> ecs = newExecutorCompletionService<Result>(e);
        int n =solvers.size();
        List<Future<Result>>futures = new ArrayList<Future<Result>>(n);
        Resultresult = null;
        try {
            for(Callable<Result> s : solvers)
               futures.add(ecs.submit(s));   //用于取消执行
            for(int i = 0; i < n; ++i) {
                try {
                   Result r = ecs.take().get();
                   if (r != null) {
                       result = r;
                       break;
                   }
                }catch(ExecutionException ignore) {}
            }
        }
        finally {
            for(Future<Result> f : futures)
               f.cancel(true);
        }
 
        if (result!= null)
           use(result);
    }
实例2

多线程的生产者消费者模式的实现

void solve(Executor e, Collection<Callable<Result>> solvers)    throws InterruptedException, ExecutionException {        CompletionService<Result> ecs = new ExecutorCompletionService<Result>(e);        for (Callable<Result> s : solvers)            ecs.submit(s);        int n = solvers.size();        for (int i = 0; i < n; ++i) {            Result r = ecs.take().get();            if (r != null)                use(r);        }    } 

线程同步

Future接口

表示已经完成的任务、正在执行过程中的任务或者尚未开始执行的任务。通过 Future 接口,可以尝试取消尚未完成的任务,查询任务已经完成还是取消了,以及提取(或等待)任务的结果值。

         FutureTask类实现了 Future,并包含一些构造函数,允许将 Runnable 或 Callable(会产生结果的 Runnable)和 Future 接口封装。因为 FutureTask 也实现 Runnable,所以可以只将 FutureTask 提供给 Executor。一些提交方法(如 ExecutorService.submit())除了提交任务之外,还将返回 Future 接口。

    Future.get() 方法检索任务计算的结果(或如果任务完成,但有异常,则抛出 ExecutionException)。如果任务尚未完成,那么Future.get() 将被阻塞,直到任务完成;如果任务已经完成,那么它将立即返回结果。

public class Cache {
    ConcurrentMap> map =new ConcurrentHashMap();
    Executor executor =Executors.newFixedThreadPool(8);
    public V get(final Kkey) {
       FutureTask f = map.get(key);
       if (f == null) {
           Callable c = new Callable() {
               public V call() {
                   // return value associated with key
               }
           };
           f = new FutureTask(c);
           FutureTask old = map.putIfAbsent(key, f);
           if (old == null)
               executor.execute(f);
           else
               f = old;
       }
       return f.get();
    }
}

// 它利用ConcurrentHashMap中的原子 putIfAbsent()方法,确保仅有一个线程试图计算给定关键字的值。如果其他线程随后请求同一关键字的值,它仅能等待(通过Future.get()的帮助)第一个线程完成。因此两个线程不会计算相同的值。

信号量 semaphone

计数信号可以认为具有一定数量的许可权,该许可权可以获得或释放。如果有剩余的许可权,acquire() 方法将成功,否则该方法将被阻塞,直到有可用的许可权(通过其他线程释放许可权)。线程一次可以获得多个许可权。

用于限制有权对资源进行并发访问的线程数

下面的类使用信号量控制对内容池的访问:

 class Pool {
   private static final int MAX_AVAILABLE = 100;
   private final Semaphore available = new Semaphore(MAX_AVAILABLE, true);
 
   public Object getItem() throws InterruptedException {
     available.acquire();
     return getNextAvailableItem();
   }
 
   public void putItem(Object x) {
     if (markAsUnused(x))
       available.release();
   }
 
   // Not a particularly efficient data structure; just for demo
 
   protected Object[] items = ... whatever kinds of items being managed
   protected boolean[] used = new boolean[MAX_AVAILABLE];
 
   protected synchronized Object getNextAvailableItem() {
     for (int i = 0; i < MAX_AVAILABLE; ++i) {
       if (!used[i]) {
          used[i] = true;
          return items[i];
       }
     }
     return null; // not reached
   }
 
   protected synchronized boolean markAsUnused(Object item) {
     for (int i = 0; i < MAX_AVAILABLE; ++i) {
       if (item == items[i]) {
          if (used[i]) {
            used[i] = false;
            return true;
          } else
            return false;
       }
     }
     return false;
   }
 
 }
  
互斥

信号量的一种特殊情况,用于特定时间只有一个线程有访问许可

用于管理对共享资源的独占访问

虽然互斥许多地方与锁定一样,但互斥还有一个锁定通常没有的其他功能,就是互斥可以由具有许可权的线程之外的其他线程来释放。这在死锁恢复时会非常有用。

Fork/join模式

应用场景

一个任务可被分解为多个子任务,当子任务执行完后,组合子任务的运行结果就能得到最终结果: divide and conquer

实现

在jdk 中使用  java.util.concurrent 下的 CyclicBarrier和CountDownLatch,两者的区别是:CyclicBarrier支持计数的重置。

CountDownLatch

用给定int N初始化计数器,一般为工作线程个数;在需要等待的线程中调用await();当其他线程调用N次 countDown(). 等待线程可以向下执行。

与CyclicBarrier的区别

         1.不能重新使用

         2. CyclicBarrier是到达屏障的所有线程的大门,只有当所有线程都已经到达屏障或屏障被打破时,才允许这些线程通过,CountdownLatch 将到达和等待功能分离。任何线程都可以通过调用 countDown() 减少当前计数,这种不会阻塞线程,而只是减少计数。await() 方法的行为与CyclicBarrier.await() 稍微有所不同,调用 await() 任何线程都会被阻塞,直到闩锁计数减少为零,在该点等待的所有线程才被释放,对 await() 的后续调用将立即返回。

应用1:

将计数 1 初始化的 CountDownLatch 用作一个简单的开/关锁存器,:在通过调用countDown() 的线程打开入口前,所有调用await 的线程都一直在入口处等待。可以在多个线程中执行 await

应用2:

第一个类是一个启动信号,在 driver 为继续执行 worker 做好准备之前,它会阻止所有的 worker 继续执行

第二个类是一个完成信号,它允许 driver 在完成所有worker 之前一直等待。

class Driver { // ...   void main() throws InterruptedException {     CountDownLatch startSignal = new CountDownLatch(1);     CountDownLatch doneSignal = new CountDownLatch(N);     for (int i = 0; i < N; ++i) // create and start threads       new Thread(new Worker(startSignal, doneSignal)).start();     doSomethingElse();            // don't let run yet     startSignal.countDown();      // let all threads proceed     doSomethingElse();     doneSignal.await();           // wait for all to finish   } } class Worker implements Runnable {   private final CountDownLatch startSignal;   private final CountDownLatch doneSignal;   Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {      this.startSignal = startSignal;      this.doneSignal = doneSignal;   }   public void run() {      try {        startSignal.await();        doWork();        doneSignal.countDown();} catch (InterruptedException ex) {} // return;   }   void doWork() { ... } }
应用3:

结合Executors 线程池使用

class Driver2 { // ...
   void main() throws InterruptedException {
     CountDownLatch doneSignal = new CountDownLatch(N);
     Executor e = ...
 
     for (int i = 0; i < N; ++i) // create and start threads
       e.execute(new WorkerRunnable(doneSignal, i));
 
     doneSignal.await();           // wait for all to finish
   }
 }
 
 class WorkerRunnable implements Runnable {
   private final CountDownLatch doneSignal;
   private final int i;
   WorkerRunnable(CountDownLatch doneSignal, int i) {
      this.doneSignal = doneSignal;
      this.i = i;
   }
   public void run() {
      try {
        doWork(i);
        doneSignal.countDown();
      } catch (InterruptedException ex) {} // return;
   }
 
   void doWork() { ... }
 }
 
CyclicBarrier

它允许一组线程等待整个线程组到达公共屏障点。CyclicBarrier 是使用整型变量构造的,其确定组中的线程数。当一个线程到达屏障时(通过调用CyclicBarrier.await()),它会被阻塞,直到所有线程都到达屏障,然后在该点允许所有线程继续执行。该操作与许多家庭逛商业街相似 -- 每个家庭成员都自己走,并商定 1:00 在电影院集合。当您到电影院但不是所有人都到了时,您会坐下来等其他人到达。然后所有人一起离开。

认为屏障是循环的是因为它可以重新使用;一旦所有线程都已经在屏障处集合并释放,则可以将该屏障重新初始化到它的初始状态。 还可以指定在屏障处等待时的超时;如果在该时间内其余线程还没有到达屏障,则认为屏障被打破,所有正在等待的线程会收到BrokenBarrierException。

该Barrier在释放等待线程后可以重用,用于N个大小线程执行中不时的相互等待。

有一个Runnable的可选参数,在屏障释放后执行,用于更新线程池的中线程状态比较有用。

应用1:
class Solver {
   final int N;
   final float[][] data;
   final CyclicBarrier barrier;
   
   class Worker implements Runnable {
     int myRow;
     Worker(int row) { myRow = row; }
     public void run() {
       while (!done()) {
         processRow(myRow);
 
         try {
           barrier.await(); 
         } catch (InterruptedException ex) { 
return; 
         } catch (BrokenBarrierException ex) { 
return; 
         }
       }
     }
   }
 
   public Solver(float[][] matrix) {
     data = matrix;
     N = matrix.length;
     barrier = new CyclicBarrier(N, 
                                 new Runnable() {
                                   public void run() { 
                                     mergeRows(...); 
                                   }
                                 });
     for (int i = 0; i < N; ++i) 
       new Thread(new Worker(i)).start();
 
     waitUntilDone();
   }
 }

//在本例中,直到N个线程执行了 await()后,才调用CyclicBarrier传入的Runnable( 修改属性,让done() 将返回true),再调用waitUtilDone()

Exchanger

两个线程使用同一个Exchanger变量,操作相同数据类型;一个线程执行到exchange方法后,会等待另一个线程;当都执行到exchange方法后实现数据双向交换。类似于具有计数为 2 的 CyclicBarrier,并且两个线程在都到达屏障时可以"交换"一些状态。(Exchanger 模式有时也称为聚集。)

以下是重点介绍的一个类,该类使用Exchanger 在线程间交换缓冲区,因此,在需要时,填充缓冲区的线程获取一个新腾空的缓冲区,并将填满的缓冲区传递给腾空缓冲区的线程。

class FillAndEmpty {
   Exchanger<DataBuffer> exchanger = new Exchanger<DataBuffer>();
   DataBuffer initialEmptyBuffer = ... a made-up type
   DataBuffer initialFullBuffer = ...
 
   class FillingLoop implements Runnable {
     public void run() {
       DataBuffer currentBuffer = initialEmptyBuffer;
       try {
         while (currentBuffer != null) {
           addToBuffer(currentBuffer);
           if (currentBuffer.isFull())
             currentBuffer = exchanger.exchange(currentBuffer);
         }
       } catch (InterruptedException ex) { ... handle ... }
     }
   }
 
   class EmptyingLoop implements Runnable {
     public void run() {
       DataBuffer currentBuffer = initialFullBuffer;
       try {
         while (currentBuffer != null) {
           takeFromBuffer(currentBuffer);
           if (currentBuffer.isEmpty())
             currentBuffer = exchanger.exchange(currentBuffer);
         }
       } catch (InterruptedException ex) { ... handle ...}
     }
   }
 
   void start() {
     new Thread(new FillingLoop()).start();
     new Thread(new EmptyingLoop()).start();
   }
  }
参考实例:

http://zheng12tian.iteye.com/blog/987376

 

高级功能

锁定
Synchronized

当线程获得监视器时(内置锁定),其他线程如果试图获得相同锁定,那么它们将被阻塞,直到第一个线程释放该锁定。同步还确保随后获得相同锁定的线程可以看到之前的线程在具有该锁定时所修改的变量的值,从而确保如果类正确地同步了共享状态的访问权,那么线程将不会看到变量的"失效"值,这是缓存或编译器优化的结果。

Lock

相对于synchronized,Lock除实现synchronzied已有功能外,还可实现计时的等待、可中断的等待、锁定轮询、每个锁定有多个条件等待集合以及无阻塞结构的锁定。

ReentrantLock是Lock的实现类,和synchronized具有相同的语义,不过比较危险的是忘记释放锁,首选还是synchronized,使用如下:

Lock lock = newReentrantLock();
...
lock.lock();
try {
  // performoperations protected by lock
}
catch(Exception ex) {
 // restoreinvariants
}
finally {
  lock.unlock();
}
ReadWriteLock

   ReentrantLock 实现的锁定规则非常简单 -- 每当一个线程具有锁定时,其他线程必须等待,直到该锁定可用。有时,当对数据结构的读取通常多于修改时,可以使用更复杂的称为读写锁定的锁定结构,它允许有多个并发读者,同时还允许一个写入者独占锁定。该方法在一般情况下(只读)提供了更大的并发性,同时在必要时仍提供独占访问的安全性。ReadWriteLock 接口和 ReentrantReadWriteLock 类提供这种功能 -- 多读者、单写入者锁定规则,可以用这种功能来保护共享的易变资源。

Condition

就像 Lock 接口是同步的具体化,Condition 接口是 Object 中 wait() 和 notify() 方法的具体化。Lock 中的一个方法是 newCondition(),它要求锁定向该锁定返回新的 Condition 对象限制。await()、signal() 和 signalAll() 方法类似于 wait()、notify() 和 notifyAll(),但增加了灵活性,每个 Lock 都可以创建多个条件变量。这简化了一些并发算法的实现。

参考资料:

http://www.cnblogs.com/sarafill/archive/2011/05/18/2049461.html 大神的论述呀

0 0
原创粉丝点击