Android中的线程形态(一)(进程/线程/线程池)

本篇提纲.png

一.线程与进程相关

1.进程

  定义:进程是具有独立功能的程序关于某个数据集合上的一次运行活动,进程是操作系统分配资源的单位。
  当你运行一个程序,你就启动了一个进程。显然,程序只是一组指令的有序集合,它本身没有任何运行的含义,只是一个静态实体。而进程则不同,它是程序在某个数据集上的执行,是一个动态实体。它因创建而产生,因调度而运行,因等待资源或事件而被处于等待状态,因完成任务而被撤消,反映了一个程序在一定的数据集上运行的全部动态过程。

特点:

  • 进程是程序的一次执行过程!过程!~~活动的。
  • 系统资源(如内存、文件)以进程为单位分配。
  • 操作系统为每个进程分配了独立的地址空间

2.线程

  定义:线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。线程是操作系统调度和分派的基本单位。
  线程是属于进程的,线程自己是没有内存空间的,它运行在进程空间内。同一进程所产生的线程共享同一内存空间,当进程退出时该进程所产生的线程都会被强制退出并清除。
  线程可与属于同一进程的其它线程共享进程所拥有的全部资源,但是其本身基本上不拥有系统资源,只拥有一点在运行中必不可少的信息(如程序计数器、一组寄存器和栈)。

为什么要有线程?
  首先我们要明确一点,CPU计算的速度是非常非常快的,寄存器仅仅能够追的上他的脚步,RAM和别的挂在各总线上的设备更是难以望其项背。因此当多个任务需要执行的时候,就需要轮流着来,同时运行多个进程,即并发技术。实现并发技术相当复杂,最容易理解的是“时间片轮转进程调度“
  在操作系统的管理下,所有正在运行的进程轮流使用CPU,每个进程允许占用CPU的时间非常短(比如10毫秒),这样用户根本感觉不出来CPU是在轮流为多个进程服务,就好象所有的进程都在不间断地运行一样。但实际上在任何一个时间内有且仅有一个进程占有CPU。
  进程是操作系统分配资源(内存空间,文件)的基本单位,进程所分配的空间在不同的进程之间是相互独立的。嘉禾上边说的“时间片轮转进程调度”,可以知道,系统在不同进程之间切换的时候,必然要经过“开始执行A进程,保存进程A的上下文,调入下一个要执行的进程B的上下文,然后开始执行进程B,保存进程B的上下文”,上下文就是进程所处的环境,系统切出去执行另一个进程之后一段时间要切回来,而切回来的依据就是原来进程的所处的环境(得知道原来那个进程地方在哪,执行到哪了等)。由于A,B两个进程所属的系统空间、占用的资源都是相互独立的,因此这个切换不同进程上下文的过程所消耗的资源就比较大。而线程是在统一进程内部的,同一进程不同线程之间共用一段内存,贡献同一资源,所以线程之间的额切换显然要比进程之间的切换容易的多。
  同时,一个进程不只是做一个任务,我呢可能会有不同的任务需求。比如我打开一个QQ,可能我一遍下载文件,一遍发送语音,一遍打字——这里QQ就可以看做是一个进程,而下文件,发语音,发文字是由三个不同的线程完成的。

总结一下,引入线程有下面三方面的考虑:

  • 应用的需要。比如打开一个QQ,可能我一遍下载文件,一遍发送语音,一遍打字。如果QQ是一个进程,那么这样的需求需要线程机制。
  • 开销的考虑。在进程内创建、终止线程比创建、终止进程要快。同一进程内的线程间切换比进程间的切换要快,尤其是用户级线程间的切换。线程之间相互通信无须通过内核(同一进程内的线程共享内存和文件)
  • 性能的考虑。多个线程中,任务功能不同(有的负责计算,有的负责I/O),如果有多个处理器,一个进程就可以有很多的任务同时在执行。

线程的特点:

  • 有自己的栈和栈指针
  • 共享所在进程的地址空间和其它资源
  • 不运行时需要保存线程上下文环境(需要程序计数器等寄存器,和进程一样,切回来的时候得知道之前的线程执行到哪了)
  • 有标识符ID(如JAVA中Thread.currentThread())

3.一些通俗的解释

  上面说了一大堆,实际上,线程和进程本质上是CPU两种不同的工作时间段的描述,只不过颗粒大小不同。为什么这么说呢?上面我们反复强调过很多遍:进程是操作系统分配资源的基本单位,线程是操作系统运算调度的基本单位。
  说的跟通俗一点,CPU正真“时间片轮转调度”的是线程,真正处理工作的地方也是线程。但是线程是属于进程的,加入有两个进程A和B,每个进程中都有两个线程A1,A2,B1,B2。CPU的执行时间就在A1,A2,B1,B2这四个线程之间轮转,如果不慎从A1切换到B2,那么也就是进程A切换到了进程B。

从三个角度来剖析二者之间的区别:

  • 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位。
  • 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可以并发执行。
  • 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。

4.Android中的线程与进程

  在Android系统中,每一个App都是一个Linux用户。一般情况下,每个App都是运行在一个进程的一个线程中,这个线程习惯称为主线程或者UI线程(注意,一个进程的一个线程之中)。
  Zygote是一个虚拟机进程,同时也是一个虚拟机实例的孵化器,每当系统要求执行一个 Android应用程序,Zygote就会FORK出一个子进程来执行该应用程序。
  Zygote进程是在系统启动时产生的,它会完成虚拟机的初始化,库的加载,预置类库的加载和初始化等等操作,而在系统需要一个新的虚拟机实例时,Zygote通过复制自身,最快速的提供个系统。

以上内容参考:
腾讯面试题04.进程和线程的区别?
android 线程与进程 区别 联系
线程和进程的区别是什么? 知乎.zhonyong的回答

二.线程与线程池

1.线程

  Java中有两种创建线程的方式,即我们所熟知的继承thread类实现Runnable接口。于是乎就来了一个非常“经典”并且并用烂了的例子——买票!这里我们也展示一下:

①继承Thread类:
public class TicketThread extends Thread{
    private int ticket = 10;
    private String name;

    public TicketThread(String name){
        this.name =name;
    }

    public void run(){
         for(int i =0;i<500;i++){
                if(this.ticket>0){
                    System.out.println(this.name+"卖票---->"+(this.ticket--));
                }
          }
    }

    public static class ThreadDemo {
           public static void main(String[] args) {
               TicketThread mt1= new TicketThread("一号窗口");
               TicketThread mt2= new TicketThread("二号窗口");
               TicketThread mt3= new TicketThread("三号窗口");
               mt1.start();
               mt2.start();
               mt3.start();
            }
        }
}

结果是:

一号窗口卖票---->10
一号窗口卖票---->9
一号窗口卖票---->8
三号窗口卖票---->10
二号窗口卖票---->10
二号窗口卖票---->9
二号窗口卖票---->8
二号窗口卖票---->7
二号窗口卖票---->6
二号窗口卖票---->5
......

后面的一串结果我就不贴了,意思就是说,票被卖重复了,每张票都卖了三遍。

②实现Runnable接口:
public class TicketRunnable implements Runnable{
    private int ticket =10;  
    private String name;  

    @Override
    public void run() {
        // TODO Auto-generated method stub
        for(int i =0;i<500;i++){  
            if(this.ticket>0){  
                System.out.println(Thread.currentThread().getName()+"卖票---->"+(this.ticket--));  
            }  
        }  
    }

    public static  class RunnableDemo {     
        public static void main(String[] args) {  
            // TODO Auto-generated method stub  
            //设计三个线程  
            TicketRunnable mt = new TicketRunnable(); 
             Thread t1 = new Thread(mt,"一号窗口");  
             Thread t2 = new Thread(mt,"二号窗口");  
             Thread t3 = new Thread(mt,"三号窗口");  
             t1.start();  
             t2.start();  
             t3.start();  
        }
    }
}

结果为:

二号窗口卖票---->10
二号窗口卖票---->8
三号窗口卖票---->9
二号窗口卖票---->7
三号窗口卖票---->6
二号窗口卖票---->5
二号窗口卖票---->3
二号窗口卖票---->2
三号窗口卖票---->4
二号窗口卖票---->1

结果刚好,每张票卖一次。

  于是就有博客说了,上面两种实现方式,继承Thread类是各自线程卖三份票,会把票卖重复了;实现Runnable接口是三个线程卖同一份票,所以结果正确——这说了好像跟没说一样??!更有甚者说,第一种方法中“保证安全的方法:把卖票的步骤用synchronized包起来。那么就不会出问题了”——你在逗我??!

  好吧~~我们来看看这两种方法——事实上,不论是继承Thread类还是实现Runnable接口,其本质都要:①重写Runnale接口中的Run方法,在其中定义我们在线程中具体要做的事情。②调用Thread.start()方法从系统中new一个线程出来。
我们可以看下。
不信你回过头去看看上面两端代码,都做了这两件事情。
  我们可以看下Thread类源码:

public class Thread implements Runnable {

看到了吧?Thread类也实现了Runnable接口,而Runnable接口:

public interface Runnable {
    public abstract void run();
}

就两句代码,也就是抽象的run()方法,所以无论你是继承的Thread类还是直接实现的Runnable方法,实际上最终都要重写其中的run方法。我们回到Thread类中,看看我们的new Thread()也就是构造函数:

    public Thread(String name) {    //上述第一种方法
        init(null, null, name, 0);
    }

    public Thread(Runnable target, String name) {   //上述第二种方法
        init(null, target, name, 0);
    }

我们主要要看到这个target就是我们传进去的Runnable对象。我们接下来直接看start()方法。

    public synchronized void start() {

        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        group.add(this);

        started = false;
        try {
            nativeCreate(this, stackSize, daemon);
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

这个段代码中nativeCreate(this, stackSize, daemon);这句代码就是向系统请求创建一个线程的方法,这是一个native方法,我们不作分析。然后再看我们都要重写的run方法:

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

看到了吧?我没骗你吧~~重写的run方法中调用了target.run();,也就是我们new Thread(mt,"一号窗口");穿进去的mt这个Runnable对象。
  所以,显而易见,上述两个结论是正确的:①重写Runnale接口中的Run方法,在其中定义我们在线程中具体要做的事情。②调用Thread.start()方法从系统中new一个线程出来。
我们可以看下。

  至于为什么第一种情况会出现票重复卖的情况而第二种没有呢?这主要是因为,第一种情况中:

public class TicketThread extends Thread{
    private int ticket = 10;

这个ticket是TicketThread类的实例变量,而我们在start这个线程的时候,通过new TicketThread("一号窗口");new TicketThread("二号窗口");TicketThread mt3= new TicketThread("三号窗口");看到没有,这里每new一个TicketThread对象,都会把该类中的实例变量拷贝一份到自己的内存中,new了三次,也就拷贝了三份ticket到三个对象中,然后start之后当然是每个线程跑自己线程中的ticket,所以就出现跑重复了
  如果我们把ticket声明为static类型,即private static int ticket = 10;,再跑一遍,结果就和第二种情况一样了!这是因为,静态成员是属于整个类的,不是属于对象的。类加载的时候,JAW就会给静态成员分配一个特定的内存空间,所有之后取用这个静态成员的时候,都会去这个特定的内存中取用(保证了可见性),并不会存在拷贝值的问题,因此就不会出错了。

  对于第二种情况:

public class TicketRunnable implements Runnable{
    private int ticket =10;

这里ticket是TicketRunnable类的实例变量,而下面在start()的时候,写法为:

 TicketRunnable mt = new TicketRunnable();

 Thread t1 = new Thread(mt,"一号窗口");
 Thread t2 = new Thread(mt,"二号窗口");
 Thread t3 = new Thread(mt,"三号窗口");

可到没有,TicketRunnable类只被new了一次,那具体使用的过程中ticket自然也就只有一份了。如果我们把这里改成:

 TicketRunnable mt1 = new TicketRunnable();
 TicketRunnable mt2 = new TicketRunnable();
 TicketRunnable mt3 = new TicketRunnable();

 Thread t1 = new Thread(mt1,"一号窗口");
 Thread t2 = new Thread(mt2,"二号窗口");
 Thread t3 = new Thread(mt3,"三号窗口");

运行一下,结果就和第一种情况一样,每张票被卖了三次,这是因为,new了三次TicketRunnable,ticket被拷贝了三次。

那么我们在使用中到底是继承Thread还是实现Runnable接口呢?因为Java不支持类的多重继承,但允许你调用多个接口。所以如果你要继承其他类,当然是调用Runnable接口更好了。一般我们在新建一个线程的时候,直接

    new Thread(new Runnable() {
        @Override
        public void run() {
            //do sth .
        }
    }).start();

就可以了,简洁明了。

2.线程池

1)Java中创建线程的第三种方式——Callable+FutureTask+ExecutorService

  我们在上面讲的创建线程的两种方式,都存在一个缺陷就是:在执行完之后,无法直接获取执行的结果。如果需要获取结果,就需要通过共享变量或者线程间通信的方式来达到效果,这显然比较麻烦。而我们现在介绍的Callable+FutureTask的方式,则能很轻松很随意的实现结果的获取。

①Callable与Runnable

  Runnable方法我们之前说过他的使用,这里在贴一遍源码,我们知道,这个run是我们要在程序中手动重写的,里边写的是我们要具体做的事情,而且这个run方法是void类型的,也就是说我们执行完了之后无法获取结果。

public interface Runnable {
    public abstract void run();
}

我们再来看Callable:

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

可以看到这是一个泛型接口,其中call()方法的返回值就是我们传进来的泛型V。而且,这里的call方法和上面的run()方法一样,也是需要我们在程序中手动重写的,其中写我们具体的要做的事情;不同的是,这里的call方法是需要return的。
  Callable一般配合ExecutorService类来使用,我们之后会通过实例展示它的使用:

<T> Future<T> submit(Callable<T> task);
②Future接口与FutureTask类

  首先,Future是一个接口,他当中封装了几个必要的方法:

    //方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。参数mayInterruptIfRunning表示
    //是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果已经完成,
    //则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;
    //如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,
    //则返回false;如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。
    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();  //表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。
    boolean isDone();   //表示任务是否已经完成,若任务完成,则返回true;
    V get() throws InterruptedException, ExecutionException;    //方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;
    V get(long timeout, TimeUnit unit)  //用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null。
        throws InterruptedException, ExecutionException, TimeoutException;

也就是说Future提供了三种功能:

  • 判断任务是否完成;
  • 能够中断任务;
  • 能够获取任务执行结果。

因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的FutureTask。

  FutureTask是一个具体类,实现了RunnableFuture接口,而RunnableFuture接口实现了Runnable和Future<V>接口:

public class FutureTask<V> implements RunnableFuture<V> {
public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

FutureTask类的两个构造器:

public FutureTask(Callable<V> callable) {
public FutureTask(Runnable runnable, V result) {

事实上,FutureTask是Future接口的唯一实现类。

③举个栗子

  使用Callable+FutureTask获取执行结果:

public class FutureTaskThread {
    public static void main(String[] args) {
        //第一种方式,使用线程池,即ExecutorService
        ExecutorService executor = Executors.newCachedThreadPool();
        Task task = new Task();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
        executor.submit(futureTask);
        executor.shutdown();

        //第二种方式,注意这种方式和第一种方式效果是类似的,只不过一个使用的是ExecutorService,一个使用的是Thread
        /*Task task = new Task();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
        Thread thread = new Thread(futureTask);
        thread.start();
        */

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        System.out.println("主线程:"+Thread.currentThread().getName());

        try {
            System.out.println("task运行结果"+futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("所有任务执行完毕");
    }
}

class Task implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        System.out.println("子线程:"+Thread.currentThread().getName());
        Thread.sleep(3000);
        int sum = 0;
        for(int i=0;i<100;i++)
            sum += i;
        return sum;
    }
}

打印结果为:

子线程:pool-1-thread-1
主线程:main
task运行结果4950
所有任务执行完毕

可以看到,ExecutorService executor = Executors.newCachedThreadPool();这里我们先从线程池中拿出一个线程,然后FutureTask<Integer> futureTask = new FutureTask<Integer>(task);新建一个任务,再将这个任务加入到线程池中去跑executor.submit(futureTask);,跑完之后executor.shutdown();关闭线程池,并通过futureTask.get()方法来获取跑完之后的结果。从结果来看,打印线程名——子线程:pool-1-thread-1,主线程:main,显而易见子线程是在线程池中跑的。
  这里我们需要强调的一点是,上面的例子中,Callable+FutureTask只是创建一个能够获取执行结果的任务,真正创建线程的地方是在ExecutorService线程池中
  如果我们换一种方式,用new Thread来替换线程池,也就是上面注释掉的第二种方法,运行结果为:

子线程:Thread-0
主线程:main
task运行结果4950
所有任务执行完毕

可以看到,执行结果完全一样,只不过子线程的线程名是“Thread-0”,而不是线程池了。这里我们已经引入了线程池的概念,那我们接下来就说说线程池的那些事。

2)Executor框架与线程池

  上面说了创建一般线程的方法,new Thread(new Runnable() {,这种方法在线程并发不多的程序中确实不错,但是如果出现高并发需要大量创建线程的情况下,劲导致系统的性能变的非常糟糕,主要因为:

  • 线程的创建和销毁都需要时间,当有大量的线程创建和销毁时,那么这些时间的消耗则比较明显,将导致性能上的缺失
  • 大量的线程创建、执行和销毁是非常耗cpu和内存的,这样将直接影响系统的吞吐量,导致性能急剧下降,如果内存资源占用的比较多,还很可能造成OOM
  • 大量的线程的创建和销毁很容易导致GC频繁的执行,从而发生内存抖动现象,而发生了内存抖动,对于移动端来说,最大的影响就是造成界面卡顿

  这个时候,就要用到线程池(ThreadPoolExecutor)了。线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。

①Executor接口与ExecutorService接口

  首先,这是两个接口:

public interface Executor {
    void execute(Runnable command);
}
public interface ExecutorService extends Executor {
    void shutdown();
    List<Runnable> shutdownNow();
    boolean isShutdown();
    boolean isTerminated();
    boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException;
    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);
    Future<?> submit(Runnable task);
    ......
}

可以看到,ExecutorService接口继承自Executor接口。Executor接口中只定义了一个方法execute(Runnable command),该方法接收一个Runable实例,它用来执行一个任务。这个任务就是一个实现了Runnable接口的类。
  ExecutorService继承自Executor接口,再此基础之上实现了更加丰富的实现多线程的方法,如shutdown(),submit()等。调用ExecutorService的shutdown()方法来平滑地关闭 ExecutorService——调用该方法后,将导致ExecutorService停止接受任何新的任务且等待已经提交的任务执行完成(已经提交的任务会分两类:一类是已经在执行的,另一类是还没有开始执行的),当所有已经提交的任务执行完毕后将会关闭ExecutorService。因此我们一般用该接口来实现和管理多线程。
  ExecutorService的生命周期包括三种状态:运行、关闭、终止。创建后便进入运行状态,当调用了shutdown()方法时,便进入关闭状态,此时意味着ExecutorService不再接受新的任务,但它还在执行已经提交了的任务,当所有已经提交了的任务执行完后,便到达终止状态。如果不调用shutdown()方法,ExecutorService会一直处在运行状态,不断接收新的任务,执行新的任务,服务器端一般不需要关闭它,保持一直运行即可。

②Executors类与ThreadPoolExecutor类

  首先这是两个类,注意Executors类Executor接口,多了一个s,不要搞混了。Executors类是一个很单纯的类,他没有实现任何接口,也没有继承任何父类:

public class Executors {

他的作用是,通过一系列工厂方法用于创建线程池,也就是new ThreadPoolExecutor类,我们可以看下ThreadPoolExecutor类:

public class ThreadPoolExecutor extends AbstractExecutorService {
public abstract class AbstractExecutorService implements ExecutorService {

可以看到,ThreadPoolExecutor继承自AbstractExecutorService类,但是AbstractExecutorService类实现了ExecutorService接口,所以相当于ThreadPoolExecutor实现了ExecutorService接口。因此,我们可以通过ExecutorService executor = Executors.newCachedThreadPool();这种方式来创建线程池。
  Executors类中有一下几种常用的创建线程池的方法:;

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

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

    public static ExecutorService newSingleThreadExecutor()
    创建一个单线程化的Executor。

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
    创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。

  一般来说,CachedTheadPool在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程,因此它是合理的Executor的首选,只有当这种方式会引发问题时(比如需要大量长时间面向连接的线程时),才需要考虑用FixedThreadPool。(该段话摘自《Thinking in Java》第四版)

线程池内部实现原理比较复杂,我们这里不对其做深究,我们目前只需要掌握它的用法:

③Executor执行Runnable任务

  通过Executors的以上四个静态工厂方法获得 ExecutorService实例,而后调用该实例的execute(Runnable command)方法即可。一旦Runnable任务传递到execute()方法,该方法便会自动在一个线程上执行。下面是是Executor执行Runnable任务的示例代码:

public class CachedThreadPoolRunnable {
     public static void main(String[] args){   
          ExecutorService executorService = Executors.newCachedThreadPool();   
//        ExecutorService executorService = Executors.newFixedThreadPool(5);  
//        ExecutorService executorService = Executors.newSingleThreadExecutor();  
            for (int i = 0; i < 5; i++){   
                executorService.execute(new TestRunnable());   
                System.out.println(" a" + i );   
            }   
            executorService.shutdown();   
      }   
}   
      
class TestRunnable implements Runnable{   
    public void run(){   
        System.out.println(Thread.currentThread().getName() + "线程被调用了");   
    }   
}

运行结果为:

 a0
pool-1-thread-1线程被调用了
 a1
 a2
pool-1-thread-2线程被调用了
pool-1-thread-1线程被调用了
 a3
 a4
pool-1-thread-1线程被调用了
pool-1-thread-3线程被调用了

可以看到,pool-1-thread-1这条线程被执行了三次,这说明:①线程池中线程的使用是随机的,execute会首先在线程池中选择一个已有空闲线程来执行任务,如果线程池中没有空闲线程,它便会创建一个新的线程来执行任务。②通过Executors.newCachedThreadPool(); 这种方式来创建的线程池是可以缓存其中的线程并重复利用的。
  如果我们把上面代码中Executors.newCachedThreadPool();这种方式换成Executors.newFixedThreadPool(5);这种方式,得到结果为:

 a0
pool-1-thread-1线程被调用了
 a1
 a2
 a3
pool-1-thread-2线程被调用了
 a4
pool-1-thread-4线程被调用了
pool-1-thread-3线程被调用了
pool-1-thread-5线程被调用了

可以看到,没有线程被复用,全部都是新创建的线程。

  再换成Executors.newSingleThreadExecutor();这种方式,可以看到,只有一条线程了:

 a0
 a1
 a2
pool-1-thread-1线程被调用了
pool-1-thread-1线程被调用了
pool-1-thread-1线程被调用了
pool-1-thread-1线程被调用了
 a3
 a4
pool-1-thread-1线程被调用了

还有一点,由于上面是通过Runnable这种方式实现的,因此最后执行的结果不能直接返回,下面我们来看Callable这种方式:

④Executor执行Callable任务

  在Java 5之后,任务分两类:一类是实现了Runnable接口的类,一类是实现了Callable接口的类。两者都可以被ExecutorService执行,但是Runnable任务没有返回值,而Callable任务有返回值。并且Callable的call()方法只能通过ExecutorService的submit(Callable<T> task) 方法来执行,并且返回一个 <T>Future<T>,是表示任务等待完成的 Future。
  当将一个Callable的对象传递给ExecutorService的submit方法,则该call方法自动在一个线程上执行,并且会返回执行结果Future对象,在该Future对象上调用get方法,将返回程序执行的结果。同样,将Runnable的对象传递给ExecutorService的submit方法,则该run方法自动在一个线程上执行,并且会返回执行结果Future对象,但是在该Future对象上调用get方法,将返回null。

下面给出一个Executor执行Callable任务的示例代码:

public class ExecutorCallable {
    public static void main(String[] args){   
        ExecutorService executorService = Executors.newCachedThreadPool();   
        List<Future<String>> resultList = new ArrayList<Future<String>>();   
  
        //创建10个任务并执行   
        for (int i = 0; i < 10; i++){   
            //使用ExecutorService执行Callable类型的任务,并将结果保存在future变量中   
            Future<String> future = executorService.submit(new TaskResultCallable(i));   
            //将任务执行结果存储到List中   
            resultList.add(future);   
        }   
  
        //遍历任务的结果   
        for (Future<String> fs : resultList){   
                try{   
                    while(!fs.isDone());//Future返回如果没有完成,则一直循环等待,直到Future返回完成  
                    System.out.println("任务返回结果输出:"+fs.get());     //打印各个线程(任务)执行的结果   
                }catch(InterruptedException e){   
                    e.printStackTrace();   
                }catch(ExecutionException e){   
                    e.printStackTrace();   
                }finally{   
                    //启动一次顺序关闭,执行以前提交的任务,但不接受新任务  
                    executorService.shutdown();   
                }   
        }   
    }   
}   
  
  
class TaskResultCallable implements Callable<String>{   
    private int id;   
  
    public TaskResultCallable(int id){   
        this.id = id;   
    }   
  
    /**  
     * 任务的具体过程,一旦任务传给ExecutorService的submit方法, 
     * 则该方法自动在一个线程上执行 
     */   
    public String call() throws Exception {  
        System.out.println("子线程  :" + Thread.currentThread().getName());   
        //该返回结果将被Future的get方法得到  
        return "call()方法被自动调用,任务返回的结果是:" + id + "    " + Thread.currentThread().getName();   
    }   
}

输出结果为:

子线程  :pool-1-thread-2
子线程  :pool-1-thread-1
任务返回结果输出:call()方法被自动调用,任务返回的结果是:0    pool-1-thread-1
任务返回结果输出:call()方法被自动调用,任务返回的结果是:1    pool-1-thread-2
子线程  :pool-1-thread-3
任务返回结果输出:call()方法被自动调用,任务返回的结果是:2    pool-1-thread-3
子线程  :pool-1-thread-4
任务返回结果输出:call()方法被自动调用,任务返回的结果是:3    pool-1-thread-4
子线程  :pool-1-thread-5
任务返回结果输出:call()方法被自动调用,任务返回的结果是:4    pool-1-thread-5
子线程  :pool-1-thread-7
子线程  :pool-1-thread-6
任务返回结果输出:call()方法被自动调用,任务返回的结果是:5    pool-1-thread-6
任务返回结果输出:call()方法被自动调用,任务返回的结果是:6    pool-1-thread-7
子线程  :pool-1-thread-8
任务返回结果输出:call()方法被自动调用,任务返回的结果是:7    pool-1-thread-8
子线程  :pool-1-thread-9
任务返回结果输出:call()方法被自动调用,任务返回的结果是:8    pool-1-thread-9
子线程  :pool-1-thread-10
任务返回结果输出:call()方法被自动调用,任务返回的结果是:9    pool-1-thread-10

可以看到,你在callable中的return结果,就是future.get()中得到的结果。

站在巨人的肩膀上摘苹果:
【Java并发编程】之十九:并发新特性—Executor框架与线程池(含代码)
Java并发编程:Callable、Future和FutureTask

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容