concurrent套件新增类

 

15.3 concurrent套件新增类

从J2SE 5.0开始有了java.util.concurrent套件,其中的类可以使实现多线程相关功能更为方便。本节将简介concurrent套件中的几个简单常用的类。

15.3.1 BlockingQueue

队列(Queue)是一个先进先出(First In First Out, FIFO)的数据结构。在J2SE 5.0中增加了java.util.concurrent.BlockingQueue。在多线程情况下,如果BlockingQueue的内容为空,而有个线程试图从Queue中取出元素,则该线程会被Block,直到Queue有元素时才解除Block;反过来,如果 BlockingQueue满了,而有个线程试图再把数据填入Queue中,则该线程会被Block,直到Queue中有元素被取走后解除Block。

BlockingQueue的几个主要操作如表15-1所示。

表15-1  BlockingQueue的几个操作

方  法

说  明

add()

加入元素,如果队列是满的,则抛出IllegalStateException

remove()

传回并从队列移除元素,如果队列是空的,则抛出NoSuchElementException

element()

传回元素,如果队列是空的,则抛出NoSuchElementException

offer()

加入元素并传回true,如果队列是满的,则传回false

poll()

传回并从队列移除元素,如果队列是空的,则传回null

peek()

传回元素,如果队列是空的,则传回null

put()

加入元素,如果队列是满的,就block

take()

传回并移除元素,如果队列是空的,就block

java.util.concurrent中提供几种不同的BlockingQueue。ArrayBlockingQueue要指定容量大小来构建。LinkedBlockingQueue默认没有容量上限,但也可以指定容量上限。PriorityBlockingQueue严格来说不是Queue,因为它是根据优先权(Priority)来移除元素。

我们以在wait()、notify()介绍时的生产者、消费者程序为例,使用BlockQueue来加以改写,优点是不用亲自处理wait()、notify()的细节。首先生产者改写如范例15.21所示:

Ü 范例15.21  ProducerQueue.java

package onlyfun.caterpillar;

import java.util.concurrent.BlockingQueue;

public class ProducerQueue implements Runnable {

    private BlockingQueue<Integer> queue;

     

    public ProducerQueue(BlockingQueue<Integer> queue) {

        this.queue = queue;

    }

    public void run() {

        for(int product = 1; product <= 10; product++) {

            try {

                // wait for a random time

                Thread.sleep((int) Math.random() * 3000);

                queue.put(product);

            }

            catch(InterruptedException e) {

                e.printStackTrace();

            }

        }

    }

}

可以看到,直接使用BlockingQueue,会自动处理同步化、wait()和notify()的执行。消费者类改写如范例15.22所示:

Ü 范例15.22  ConsumerQueue.java

package onlyfun.caterpillar;

import java.util.concurrent.BlockingQueue;

public class ConsumerQueue implements Runnable {

    private BlockingQueue<Integer> queue;

     

    public ConsumerQueue(BlockingQueue<Integer> queue) {

        this.queue = queue;

    }

     

    public void run() {

        for(int i = 1; i <= 10; i++) {

            try {

                // wait for a random time

                Thread.sleep((int) (Math.random() * 3000));

                queue.take();

            }

            catch(InterruptedException e) {

                e.printStackTrace();

            }

        }

    }

}

可以使用范例15.23进行简单的测试:

Ü 范例15.23  BlockingQueueDemo.java

package onlyfun.caterpillar;

import java.util.concurrent.BlockingQueue;

public class ConsumerQueue implements Runnable {

    private BlockingQueue<Integer> queue;

     

    public ConsumerQueue(BlockingQueue<Integer> queue) {

        this.queue = queue;

    }

     

    public void run() {

        for(int i = 1; i <= 10; i++) {

            try {

                // 等待一个随机时间

                Thread.sleep((int) (Math.random() * 3000));

                queue.take();

            }

            catch(InterruptedException e) {

                e.printStackTrace();

            }

        }

    }

}

由于BlockingQueue不需要您来控制,所以没有特意显示信息以表示生产者、消费者放入产品至Queue的信息,不过仍可以在ProducerQueue与ConsumerQueue中放入相关信息显示,以确认程序确实在运转。

15.3.2 Callable与Future

java.util.concurrent.Callable与java.util.concurrent.Future类可以协助您完成Future模式。Future模式在请求发生时,会先产生一个Future对象给发出请求的客户。它的作用类似于代理(Proxy)对象,而同时所代理的真正目标对象的生成是由一个新的线程持续进行。真正的目标对象生成之后,将之设置到Future之中,而当客户端真正需要目标对象时,目标对象也已经准备好,可以让客户提取使用。

 

关于Future模式的说明,可以参考:

http://caterpillar.onlyfun.net/Gossip/DesignPattern/FuturePattern.htm

Callable是一个接口,与Runnable类似,包含一个必须实现的方法,可以启动为让另一个线程来执行。不过Callable工作完成后,可以传回结果对象。Callable接口的定义如下:

public interface Callable<V> {
    V call() throws Exception;
}

例如,可以使用Callable完成某个费时的工作,工作结束后传回结果对象,例如求质数范例15.24:

Ü 范例15.24  PrimeCallable.java

package onlyfun.caterpillar;

import java.util.ArrayList;

import java.util.List;

import java.util.concurrent.Callable;

public class PrimeCallable implements Callable<int[]> {

    private int max;

     

    public PrimeCallable(int max) {

        this.max = max;

    }

     

    public int[] call() throws Exception {

        int[] prime = new int[max+1];

       

        List<Integer> list = new ArrayList<Integer>();

        for(int i = 2; i <= max; i++)

            prime[i] = 1;

        for(int i = 2; i*i <= max; i++) { // 这里可以改进

            if(prime[i] == 1) {

                for(int j = 2*i; j <= max; j++) {

                    if(j % i == 0)

                        prime[j] = 0;

                }

            }

        }

        for(int i = 2; i < max; i++) {

            if(prime[i] == 1) {

                list.add(i);

            }

        }

       

        int[] p = new int[list.size()];

        for(int i = 0; i < p.length; i++) {

        p[i] = list.get(i).intValue();

        }

       

        return p;

    }  

}

 

程序中的求质数方法是很简单的,但效率不好,这里只是为了示范方便,才使用简单的求质数方法,要更有效率地求质数,可以参考Eratosthenes筛选求质数:

http://caterpillar.onlyfun.net/Gossip/AlgorithmGossip/EratosthenesPrime.htm

假设现在求质数的需求是在启动PrimeCallable后的几秒之后,则可以使用Future来获得Callable执行的结果,从而在未来的时间点获得结果,例如范例15.25:

Ü 范例15.25  FutureDemo.java

package onlyfun.caterpillar;

import java.util.concurrent.Callable;

import java.util.concurrent.ExecutionException;

import java.util.concurrent.FutureTask;

public class FutureDemo {

    public static void main(String[] args) {

        Callable<int[]> primeCallable = new PrimeCallable(1000);

        FutureTask<int[]> primeTask =

                new FutureTask<int[]>(primeCallable);

              

        Thread t = new Thread(primeTask);

        t.start();

              

        try {

            // 假设现在做其他事情

            Thread.sleep(5000);

                       

            // 回来看看质数找好了吗

            if(primeTask.isDone()) {

                int[] primes = primeTask.get();

                for(int prime : primes) {

                    System.out.print(prime + " ");

                }

                System.out.println();

            }

        } catch (InterruptedException e) {

            e.printStackTrace();

        } catch (ExecutionException e) {

            e.printStackTrace();

        }       

    }

}

java.util.concurrent.FutureTask是一个代理,真正执行找质数功能的是Callable对象。使用另一个线程启动FutureTask,之后就可以做其他的事了。等到某个时间点,用isDone()观察任务是否完成,如果完成了,就可以获得结果。一个执行结果如下,显示所有找到的质数:

2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 223 227 229 233 239 241 251 257 263 269 271 277 281 283 293 307 311 313 317 331 337 347 349 353 359 367 373 379 383 389 397 401 409 419 421 431 433 439 443 449 457 461 463 467 479 487 491 499 503 509 521 523 541 547 557 563 569 571 577 587 593 599 601 607 613 617 619 631 641 643 647 653 659 661 673 677 683 691 701 709 719 727 733 739 743 751 757 761 769 773 787 797 809 811 821 823 827 829 839 853 857 859 863 877 881 883 887 907 911 919 929 937 941 947 953 967 971 977 983 991 997

用户可能需要快速翻页浏览文件,但在浏览到有图片的页数时,由于图片文件很大,导致图片加载较慢,造成用户浏览文件时会有停顿的现象。因此希望在文件开启之后,仍有一个后台作业持续加载图片。这样,用户在快速浏览页面时,所造成的停顿可以获得改善,从而可以考虑使用这里所介绍的功能。

15.3.3 Executors

有时候需要建立一堆线程来执行一些小任务,然而频繁地建立线程有时会开销较大。因为线程的建立必须与操作系统互动,如果能建立一个线程池(Thread pool)来管理这些小线程并加以重复使用,对于系统效能是个改善的方式。

可以使用Executors来建立线程池,Executors有几个静态(static)方法,如表15-2所示。

表15-2  Executors几个静态方法

方  法

说  明

newCachedThreadPool()

建立可以快速获取的线程,每个线程默认可idle的时间为60秒

newFixedThreadPool()

包括固定数量的线程

newSingleThreadExecutor()

只有一个线程,循序的执行指定给它的每个任务

newScheduledThreadPool()

可调度的线程

newSingleThreadScheduledExecutor()

单一可调度的线程

下面的程序使用newFixedThreadPool()方法建立线程池,当中包括5个可以重复使用的线程。您可以指定Runnable对象给它,程序中会产生10个Runnable对象。由于线程池中只有5个可用的线程,所以后来建立的5个Runnable必须等待有空闲的线程才会被执行,如范例15.26:

Ü 范例15.26  ExecutorDemo.java

package onlyfun.caterpillar;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

public class ExecutorDemo {

    public static void main(String[] args) {

        ExecutorService service = Executors.newFixedThreadPool(5);            

        for(int i = 0; i < 10; i++) {

            final int count = i;

            Runnable runnable =

                        new Runnable() {

                            public void run() {

                                System.out.println(count);

                                try {

                                    Thread.sleep(2000);

                                } catch (InterruptedException e) {

                                    e.printStackTrace();

                                }        

                            }

                        };

            service.submit(runnable);

        }                

        service.shutdown(); // 最后记得关闭Thread pool

    }

}

执行结果会显示0到9,不过关键是在执行时的时间顺序,您可以自己执行程序,看看显示的时间顺序有何不同。

submit()方法也接受实现Callable接口的对象,最后传回Future对象,可以获得Callable执行过后的传回结果。

如果想利用Executors进行调度,例如要求某个工作30秒后执行,一个设置的例子如下:

ScheduledExecutorService scheduler = 
                Executors.newSingleThreadScheduledExecutor();
scheduler.schedule(new Runnable() {
                               public void run() {
                                   // 调度工作
                               }
                           },
                       30, TimeUnit.SECONDS);

或是要求某个工作5秒后执行,之后每30秒执行一次,一个设置的例子如下:

ScheduledExecutorService scheduler = 
        Executors.newSingleThreadScheduledExecutor();
final ScheduledFuture future = scheduler.scheduleAtFixedRate(
                                new Runnable() {
                                       public void run() {
                                           // 调度工作
                                           System.out.println("t");
                                       }
                                },
                                0, 5, TimeUnit.SECONDS);
/ 调度60 秒后取消future
scheduler.schedule(new Runnable() {
                        public void run() {
                            future.cancel(false);
                        }
                      }, 
                        60, TimeUnit.SECONDS);

如以上程序所示,如果想要取消调度任务,则可以调用ScheduledFuture的cancel()方法。

你可能感兴趣的:(concurrent套件新增类)