线程同步的三种方法(Java 并发编程 concurrent包复习)

来源:互联网 发布:php reflection的作用 编辑:程序博客网 时间:2024/06/01 07:59

 最近在项目里用到了多线程,包括线程池的创建,多个线程同步等,所以对executor框架简单复习一下。因为是简单复习,所以不会介绍太多概念,只是对一些基础知识点列举,并给出几个实际问题及其解决方法。

  一、executor框架在java5引入,为并发编程提供了一堆新的启动、调度和管理线程的API。它在java.util.cocurrent包下,其内部使用了线程池机制,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。因此,在Java 5之后,通过Executor来启动线程比使用Thread的start方法更好,更易管理,效率更好(用线程池实现,节约开销),它的主要内容包括:threadPool,Executor,Executors,ExecutorService,CompletionService,Future,Callable,以及CountDownLatch 等工具类。下面是一些基础概念:

1. Executor接口定义了一个execute(Runnable command)方法,接收一个Runnable实例。

2. ExecutorService接口继承自Executor接口,它提供了更丰富的实现多线程的方法,比如,ExecutorService提供了关闭自己的方法,以及可为跟踪一个或多个异步任务执行状况而生成 Future 的方法。 可以调用ExecutorService的shutdown()方法来平滑地关闭 ExecutorService,调用该方法后,将导致ExecutorService停止接受任何新的任务且等待已经提交的任务执行完成(已经提交的任务会分两类:一类是已经在执行的,另一类是还没有开始执行的),当所有已经提交的任务执行完毕后将会关闭ExecutorService。因此我们一般用该接口来实现和管理多线程。

3. Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。  其中两种是: 

    public static ExecutorService newFixedThreadPool(int nThreads)创建固定数目线程的线程池。

    public static ExecutorService newCachedThreadPool()创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线   程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。

关于使用哪一种,stackOverFlow上有很多问题回答。不解释了。

https://stackoverflow.com/questions/17957382/fixedthreadpool-vs-cachedthreadpool-the-lesser-of-two-evils

4. Java 5之后,任务分两类:一类是实现了Runnable接口的类,一类是实现了Callable接口的类。两者都可以被ExecutorService执行,两者的区别如下:

   a. Runnable任务没有返回值,而Callable任务有返回值。并且Callable的call()方法只能通过ExecutorService的submit(Callable<T> task) 方法来执行,并且返回一个 <T>Future<T>,是表示任务等待完成的 Future。

   b.Callable接口类似于Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常而Callable又返回结果,而且当获取返回结果时可能会抛出异常。Callable中的call()方法类似Runnable的run()方法,区别同样是有返回值,后者没有。

  c.当将一个Callable的对象传递给ExecutorService的submit方法,则该call方法自动在一个线程上执行,并且会返回执行结果Future对象。同样,将Runnable的对象传递给ExecutorService的submit方法,则该run方法自动在一个线程上执行,并且会返回执行结果Future对象,但是在该Future对象上调用get方法,将返回null。

二。实际问题

     问题1.  我们需要某件事准备好之后,开始执行一组任务。而且要这组任务都结束后,才进行后续动作。

    解决方法: 使用CountDownLatch,它是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。 用给定的计数初始化 CountDownLatch。每个被等待的工作线程完成后,调用了 countDown() 方法,计数器减1。在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回,不在阻塞。代码如下:

    public String test1() {        final int N = 3;        CountDownLatch doneSignal = new CountDownLatch(N);        CountDownLatch startSignal = new CountDownLatch(1);//开始执行信号        for (int i = 1; i <= N; i++) {            new Thread(new Worker(i, doneSignal, startSignal)).start();//线程启动了        }        System.out.println("begin------------");        startSignal.countDown();//开始执行啦        try {            doneSignal.await();//这句使得主线程等待所有的线程执行完毕,才会继续往下走,输出OK        } catch (InterruptedException e) {            e.printStackTrace();        }        System.out.println("Ok");        return "done";   }
在test1里,当startSignal信号变为0时,for循环里的N个工作线程才开始执行。并且等这N个线程都执行结束后,主线程才能输出OK;worker代码如下:

class Worker implements Runnable {    private final CountDownLatch doneSignal;    private final CountDownLatch startSignal;    private int beginIndex;    Worker(int beginIndex, CountDownLatch doneSignal,           CountDownLatch startSignal) {        this.startSignal = startSignal;        this.beginIndex = beginIndex;        this.doneSignal = doneSignal;    }    public void run() {        try {            startSignal.await(); //等待开始执行信号的发布            beginIndex = (beginIndex - 1) * 2+ 1;            for (int i = beginIndex; i <= beginIndex + 2; i++) {                System.out.println(i);            }        } catch (InterruptedException e) {            e.printStackTrace();        } finally {            doneSignal.countDown();//调用countDown表示自己执行结束。共享计数减1        }    }}
    问题2. 文件批量下载,每个文件通过一个线程下载,需要将所有文件都下载后,打包成zip压缩文件返回(文件上传下载会在后面介绍)。

    解决方法: 此问题使用上面的countDownLatch同样可以解决,但是这次我们给出另外一种方法,使用Future.回顾概念:ExecutorService的submit(Callable<T> task) 方法执行一个Callbale实例,并且返回一个 <T>Future<T>,是表示任务等待完成的 Future。通过跟踪future,可以判断任务是否完成。关键代码如下:

List<File>fileAll=Lists.new ArrayList();...初始化fileAllList<Future<String>> futures=Lists.newArrayList();//跟踪每个任务的执行结果for(int i=0;i<fileAll.size();i++){                File e=fileAll.get( i );                String filename=e.getFilename();                String url = MessageFormat.format( downloadUrl, e.getUrl() );                FileDownloadTask fs=new FileDownloadTask( file,filename,url );//下载文件的任务                futures.add( scheduledExecutorComponent.submit(fs));//将任务返回加入列表。进行跟踪            }            for (Future<String> fs : futures){//此循环跟踪每个任务的执行结果                try{                    while(!fs.isDone());//Future返回如果没有完成,则一直循环等待,直到Future返回完成                    LOG.debug( "文件下载结果:"+documentid+":"+fs.get() );                }catch(Exception e){                    e.printStackTrace();                }            }


问题3  同问题1以及问题2的场景类似。只不过我们给出另外一种解决方法:invokeAll.关键代码如下:

public String test2() {    List<Callable<Integer>> tasks = new ArrayList<Callable<Integer>>();    Callable<Integer> task = null;    for (int i = 0; i < 5; i++)    {        task = new Callable<Integer>()        {            @Override            public Integer call() throws Exception            {                int ran = new Random().nextInt(1000);                Thread.sleep(ran);                System.out.println(Thread.currentThread().getName()+" 执行了 " + ran );                return ran;            }        };        tasks.add(task);    }    long s = System.currentTimeMillis();    List<Future<Integer>> results = null;    try {        results = this.scheduledExecutorComponent.invokeAll(tasks);    } catch (InterruptedException e) {        e.printStackTrace();    }    System.out.println("执行任务消耗了 :" + (System.currentTimeMillis() - s) +"毫秒");    for (int i = 0; i < results.size(); i++)    {       try        {            System.out.println(results.get(i).get());//3        } catch (Exception e)        {            e.printStackTrace();        }    }    return "ok";}

如果有一个任务执行失败,3初会报异常,所以,invokeAll 还可以结合ExecutorCompletionService来使用,通过一个blockingQueue来管理,一旦有线程执行失败,可以立即获得结果。具体请参考:https://stackoverflow.com/questions/18202388/how-to-use-invokeall-to-let-all-thread-pool-do-their-taski

 invokeAll是阻塞方法,它必须等待所有的任务执行完成后统一返回,一方面内存持有的时间长;另一方面响应性也有一定的影响、所以对于问题场景,我们更倾向于使用前面两种方法。

阅读全文
0 0
原创粉丝点击