参考:

《Thinking In JAVA》

《JAVA 并发编程实践》


 

 

  之前我们一直在研究关于java并发的理解部分,这阶段我们将会了解一些关于java并发在代码上的实践部分,例如执行任务,取消关闭等。事实上并发通常是提高运行在单处理器上的程序的性能,如果没有任务会阻塞,那么在单处理器机器上使用并发就没有任何意义。线程可以驱动任务,可以实现Runnable接口来描述一个任务,也就是说生成了一个Runable对象,将Runnable对象转变为工作任务的传统方式就是把它提交给一个Thread构造器,然后调用Thread.start()方法就会为该线程执行必须的初始化操作,然后调用Runnable.run()方法,以便在这个新线程中启动该任务。

 

  也就是说,如果你想自己简单实现一个多线程程序可以像如下的操作进行:

 

  1.创建一个类实现Runnable接口,代码类似: public class AAA implements Runnable{}, 将你需要执行的任务放在覆盖的run方法内。

 

  2.创建一个新的Thread,执行start,代码类似: Thread t=new Thread(new AAA()); t.start();

 

 

实现Runnable接口比继承Thread类所具有的优势:

    1):适合多个相同的程序代码的线程去处理同一个资源

    2):可以避免java中的单继承的限制

    3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。

 

  但是实际上,我们在真正设计多线程程序的时候大都采用Executor框架,在java类库中,任务执行的主要抽象不是Thread,而是Executor,Executor在客户端和任务执行之间提供了一个间接层,与客户端直接执行任务不同,这个中介对象将执行任务,它提供了一种标准的方法将任务的提交过程与执行过程解耦开来,并用Runnable来表示任务,并且还提供了对生命周期的支持以及统计信息的手机,应用程序管理机制和性能监视等机制。Executor基于生产者-消费者模式,提交任务的操作相当于生产者,执行任务的线程相当于消费者。非常常见的情况是,单个的Executor被用来创建和管理系统中的所有的任务,比如下面的代码示例:

public class CachedThreadPool {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        ExecutorService exec=Executors.newCachedThreadPool();
        for(int i=0;i<5;i++){
            exec.execute(new LiftOff());
        }
        exec.shutdown();
    }

}

  上面的例子使用了CachedThreadPool线程池,类似的还有FixedThreadPool,这两个线程池最常用,FixedThreadPool一次性预先执行代价高昂的线程分配,CachedThreadPool在程序执行过程中通常会创建与所需数量相同的线程,然后再它回收旧线程时停止创建新线程,因此是合理的Executor的首选,只有当cachedthreadpool引发问题的时候,才需要切换到FixedThreadPool。注意,在任何线程池中,现有线程在可能的情况下,都会被自动复用。

  Runnable是执行工作的独立任务,但是它不是返回值,如果你希望任务在完成时能够返回一个值,那么可以实现Callable接口,Callable是一种具有类型参数的泛型,但是它的类型参数表示的是从方法call(),而不是run()。下面是一个简单的程序例子:

public class CallableDemo {

    static class TaskWithResult implements Callable<String>{
        private int id;
        public TaskWithResult(int id){
            this.id=id;
        }
        @Override
        public String call() throws Exception {
            // TODO Auto-generated method stub
            return "result of TaskWithResult :"+id;
        }
    }
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        ExecutorService exec=Executors.newCachedThreadPool();
        ArrayList<Future<String>> results=new ArrayList<Future<String>>();
        
        for(int i=0;i<10;i++){
            results.add(exec.submit(new TaskWithResult(i)));
        }
        
        for(Future<String> fs: results){
            try{
                System.out.println(fs.get());
            }catch(Exception e){
                e.printStackTrace();
            }finally{
                exec.shutdown();
            }
        }
    }

  通过上述的代码基本上可以了解了最核心的多线程程序编写过程,这里需要额外提出的是需要了解java线程转换的关系.

 


 

其他:

  • 后台线程:

另外我们之前所谈论过的所有线程都是非后台线程,而后台(daemon)线程,是指在程序运行时在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。因此当所有的的非后台线程结束时,程序也就被终止了,同时会杀死进程中的所有后台进程。反过来说,只要有任何非后台线程还在运行,程序就不会终止,比如执行main()的就是一个非后台线程。必须在线程启动之前调用setDaemon方法,才能把它设置为后台线程。

  • Executor生命周期:

 因为executor以异步的方式来执行任务,因此在任何时刻,之前提交任务的状态不是立即可见的,有些任务可能已经完成,有些可能正在运行,而其他的任务可能在队列中等待执行。为了解决执行服务的生命周期问题,Executor扩展了ExecutorService接口,添加了一些用于生命周期管理的方法,另外Executor框架下有很多类似的类,这里的网址:javascript:void(0)给出了一些比较详细的解释。

  ExecutorService实际上是继承自Executor,它的生命周期有三种状态:运行、关闭和已终止,它拥有两种终止方法:

  1.   shutDown方法:不再接受新的任务,同时等待已经提交的任务执行完成--包括那些还未开始执行的任务。
  2.   shutDownNow方法:粗暴的关闭,它将尝试取消所有运行中的任务,并且不再启动队列中尚未开始执行的任务。
  • Runnable,Callable,Future

Runnable和Callable描述的都是抽象的计算任务。这些任务通常都是有范围的,即都有一个明确的起始点,并且最终会结束。Future表示一个任务的生命周期,并且提供了相应的方法来判断是否已经完成或者取消,以及获取任务的结果和取消任务等。在Future规范中包含的隐含意义是:任务的生命周期只能前进,不能后退,当某个任务完成后,它就永远停留在完成状态上。