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

 最近在项目里用到了多线程,包括线程池的创建,多个线程同步等,所以对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();
...初始化fileAll
List<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
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值