java中创建线程的四种方式及线程池详解

众所周知,我们在创建线程时有四种方法可以用,分别是:
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())。


版权声明:本文为qq_45901741原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。