一.线程与进程相关
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