众所周知,我们在创建线程时有四种方法可以用,分别是:
1、继承Thread类创建线程
2、实现Runnable接口创建线程
3、使用Callable和Future创建线程
4、使用线程池创建(使用java.util.concurrent.Executor接口)
其中第一、二中最为简单,我已经在前面线程部分详细解释过,不懂得可以去看看:多线程。
今天我们聊聊其他两种和他们的区别。
1、使用Callable接口和Future创建线程
和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大,其表现在:
1>call方法可以有返回值
2>call()方法可以声明抛出异常
Java5提供了Future接口来代表Callable接口里call()方法的返回值,并且为Future接口提供了一个实现类FutureTask,这个实现类既实现了Future接口,还实现了Runnable接口,因此可以作为Thread类的target。
介绍了相关的概念之后,创建并启动有返回值的线程的步骤如下:
1】创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
2】使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
3】使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
4】调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
其实现代码如下:
public class MyThread implements Callable<String>{//Callable是一个泛型接口
@Override
public String call() throws Exception {//返回的类型就是传递过来的V类型
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+" : "+i);
}
return "Hello Tom";
}
public static void main(String[] args) throws Exception {
MyThread myThread=new MyThread();
FutureTask<String> futureTask=new FutureTask<>(myThread);
Thread t1=new Thread(futureTask,"线程1");
Thread t2=new Thread(futureTask,"线程2");
Thread t3=new Thread(futureTask,"线程3");
t1.start();
t2.start();
t3.start();
System.out.println(futureTask.get());
}
}
FutureTask非常适合用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。 FutureTask可以确保即使调用了多次run方法,它都只会执行一次Runnable或者Callable任务,或者通过cancel取消FutureTask的执行等。
2、线程池
java中经常需要用到多线程来处理一些业务,但是我们非常不建议单纯使用继承Thread或者实现Runnable接口的方式来创建线程,那样势必有创建及销毁线程耗费资源、线程上下文切换问题。
但是,众所周知,线程有五种基本状态,分别是:
1、NEW(初始化)状态
实例化一个Thread类对象出来(未执行start()方法前),Thread的状态为NEW。
2、RUNNABLE(可运行)状态
调用线程的start()方法,此时线程进入RUNNABLE状态,该状态的线程位于可运行线程池中,等待被操作系统调度,获取CPU的使用权。
当获得CPU的时间片时,线程进入运行状态,执行程序代码。
3、BLOCKED(阻塞)状态
当线程等待获取monitor锁(synchronized)时,线程就会进入BLOCKED状态。
注意:
等待获取monitor锁,线程的状态是BLOCKED。
等待获取Lock锁(LockSupport.park()),线程的状态是WAITING。
4、TIMED_WAITING(超时等待)状态
当执行
Thread.sleep(time)
Thread.join(time)
Object.wait(time)
LockSupport.parkNanos(time)
LockSupport.partUntil(time)
等操作时,线程会从RUNNABLE状态进入TIMED_WAITING状态。
当执行超时时间到
Thread.join() 线程执行完
Object.notify()
notifyAll()
LockSupport.unpark()
线程被中断等操作时,线程会从TIMED_WAITING状态进入RUNNABLE状态。
5、WAITING(等待)状态
当执行Object.wait()、Thread.join()、LockSupport.park()等操作时,线程会从RUNNABLE状态进入WAITING状态。
当执行Object.notify()/notifyAll()、Thread.join()程序执行完、LockSupport.unpark()、线程被中断等操作时,线程会从WAITING状态进入RUNNABLE状态。
6、TERMINATED(终止)状态
当线程执行完毕、Thread.stop()、内存溢出时,线程就会进入TERMINATED状态。
线程一旦死亡,就无法复活。
在一个死亡的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
分别对应操作系统进程(线程)的五种状态:
Thread类中有一个内部枚举类描述这六种状态:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
所以可想而知,一个线程从创建到使用需要多少时间,又多么麻烦。
所以上述的三种创建线程的方法,我们现在几乎不考虑,因为有更方便和安全的。
有没有一种方法将这些繁琐的过程简单化呢?
有!那就是线程池,创建过多的线程也可能引发资源耗尽的风险,这个时候引入线程池比较合理,方便线程任务的管理。
java中涉及到线程池的相关类均在jdk1.5开始的java.util.concurrent包中,涉及到的几个核心类及接口包括:Executor、Executors、ExecutorService、ThreadPoolExecutor、FutureTask、Callable、Runnable等。
再看线程池之前,我们先看两段代码:
public class PuTongXianCheng {
public static void main(String[] args) throws Exception {
//计时
Long start = System.currentTimeMillis();
//生成随机数
Random random = new Random();
List list = new ArrayList();
for (int i = 0;i <= 100000;i++){
Thread thread = new Thread(){
//创建线程
@Override
public void run() {
list.add(random.nextInt());
}
};
//线程运行
thread.start();
thread.join();
}
System.out.println("时间:"+(System.currentTimeMillis() - start));
System.out.println("list = "+list.size());
}
}
这段代码的运行时间为:
public static void main(String[] args) throws Exception {
Long start = System.currentTimeMillis();
Random random = new Random();
List list = new ArrayList();
//创建线程池
ExecutorService service = Executors.newSingleThreadExecutor();
for (int i = 0;i <= 100000;i++){
service.execute(
new Thread(){
@Override
public void run() {
list.add(random.nextInt());
}
}
);
}
service.shutdown();
service.awaitTermination(1, TimeUnit.DAYS);
System.out.println("时间:"+(System.currentTimeMillis() - start));
System.out.println("list = "+list.size());
}
这段代码的运行时间为:
由此可见,线程池比普通线程优越多少,接下来我们学习创建他。
2.1、线程池的创建
线程池可以自动创建也可以手动创建:
1、自动创建体现在Executors工具类中,常见的可以创建newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor,对于这三种线程池,日后我会写博客仔细说。
public static ExecutorService newFixedThreadPool(int var0) {
return new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
}
public static ExecutorService newSingleThreadExecutor() {
return new Executors.FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue());
}
2、手动创建主要用ThreadPoolExecutor类,体现在可以灵活设置线程池的各个参数,体现在代码中即ThreadPoolExecutor类构造器上各个实参的不同:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
2.2、ThreadPoolExecutor中重要的几个参数详解
corePoolSize:核心线程数,也是线程池中常驻的线程数,线程池初始化时默认是没有线程的,当任务来临时才开始创建线程去执行任务。
maximumPoolSize:最大线程数,在核心线程数的基础上可能会额外增加一些非核心线程,需要注意的是只有当workQueue队列填满时才会创建多于corePoolSize的线程(线程池总线程数不超过maxPoolSize)
keepAliveTime:非核心线程的空闲时间超过keepAliveTime就会被自动终止回收掉,注意当corePoolSize=maxPoolSize时,keepAliveTime参数也就不起作用了(因为不存在非核心线程);
unit:keepAliveTime的时间单位
workQueue:用于保存任务的队列,可以为无界、有界、同步移交三种队列类型之一,当池子里的工作线程数大于corePoolSize时,这时新进来的任务会被放到队列中
threadFactory:创建线程的工厂类,默认使用Executors.defaultThreadFactory(),也可以使用guava库的ThreadFactoryBuilder来创建。
handler:线程池无法继续接收任务(队列已满且线程数达到maximunPoolSize)时的饱和策略,取值有AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy。
2.3、线程池中的线程创建流程

2.4、线程池的拒绝策略
在此之间,我们需要知道一个前提,所有拒绝策略都实现了接口 RejectedExecutionHandler:
public interface RejectedExecutionHandler {
/**
* @param r the runnable task requested to be executed
* @param executor the executor attempting to execute this task
* @throws RejectedExecutionException if there is no remedy
*/
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
这个接口只有一个 rejectedExecution 方法。r 为待执行任务;executor 为线程池;方法可能会抛出拒绝异常。
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize时,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:
2.4.1、ThreadPoolExecutor.AbortPolicy
丢弃任务并抛出RejectedExecutionException异常,也是线程池的默认拒绝策略。
private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();
如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。但是会中断调用者的处理过程,所以除非有明确需求,一般不推荐。
2.4.2、ThreadPoolExecutor.DiscardPolicy
丢弃任务,但是不抛出异常,如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。
使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略。
2.4.3、ThreadPoolExecutor.DiscardOldestPolicy
丢弃队列最前面的任务,然后重新提交被拒绝的任务。
此拒绝策略,是一种喜新厌旧的拒绝策略。是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。
2.4.4、ThreadPoolExecutor.CallerRunsPolicy
由调用线程(提交任务的线程)处理该任务
3、四种创建线程方法对比
实现Runnable和实现Callable接口的方式基本相同,不过是后者执行call()方法有返回值,后者线程执行体run()方法无返回值,并且如果使用FutureTask类的话,只执行一次Callable任务。
这种方式与继承Thread类的方法之间的差别如下:
1、线程只是实现Runnable或实现Callable接口,还可以继承其他类。
2、这种方式下,多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
3.继承Thread类只需要this就能获取当前线程。不需要使用Thread.currentThread()方法
4、继承Thread类的线程类不能再继承其他父类(Java单继承决定)。
5、前三种的线程如果创建关闭频繁会消耗系统资源影响性能,而使用线程池可以不用线程的时候放回线程池,用的时候再从线程池取,项目开发中主要使用线程池的方式创建多个线程。
6.实现接口的创建线程的方式必须实现方法(run() call())。