一、创建线程的三种方式
1. 继承Thread类
Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。
启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是native方法,它将通知底层操作系统,最终由操作系统启动一个新线程,操作系统将执行run()。
这种方式创建线程很简单,通过自己的类直接extends Thread,并重写run()方法,就可以自动启动新线程并执行自己定义的run()方法。
实现过程:
1. 定义自己的线程类,继承Thread
2. 重写run方法,里面是我们自己的业务
3. 创建自定义线程类对象
4. 通过线程对象.start()将线程加入到就绪队列中
优点:编写简单,如果需要访问当前线程无需使用Thread.currentThread()方法,直接使用this即可获得当前线程
缺点:自定义的线程类已经继承了Thread类,所以后续无法再继承其他的类
2. 实现Runnable/Callable接口
如果自己的类已经继承了另一个类,就无法多继承,此时可以实现一个Runnable接口。(JDK1.5后有更优秀的Callable接口)
实现过程:
1. 定义自己的业务类,实现Runnable接口
2. 实现接口里抽象方法run,里面是我们自己的业务(注意这里的run里getName()要Thread.currentThread.getName())
3. 创建唯一的自定义业务类对象
4. 创建Thread类对象作为单个线程对象,将刚刚的业务对象传入
5. 使用线程类对象调用start(),将线程加入到就绪队列中
优点:自定义的线程类只是实现了Runnable接口或Callable接口,后续还可以继承其他类,再这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况。从而可以将CPU、代码、还有数据分开(解耦),形成清晰的模型,较好地体现了面向对象的思想
缺点:编程稍微复杂,如果想访问当前线程,则需使用Thread.currentThread()方法
3. 通过线程池和辅助创建线程池的工具类
ExecutorService是用来存储线程的池子,把新建线程/启动线程/关闭线程的任务都交给池来管理;Executors 辅助创建线程池的工具类。(JDK1.5之后)
实现过程:
1. Executor.newFixedThreadPool(int n) ;创建包含最多n个线程的线程池i对象
2. 使用pool.excute()来将线程池中的线程以多线程的方式启动,每次调用都会将一个线程对象加入到就绪队列中。这个线程池对象负责新建/启动/关闭线程,而我们主要负责的是自定义的业务。
优点:
1. 降低系统的资源消耗:减少系统创建与销毁线程对象的次数,每个线程都可以重复利用,执行多次任务
2. 提高相应速度:当任务到达时,任务可以不用等待线程创建就能立即执行
3. 提高线程的可管理性:可以根据系统的承受能力,调正线程池中线程的数目
二、各种创建线程方法之间的比较
1、实现Runnable或Callable接口来创建线程的方式基本相同,不过后者执行call()方法有返回值,执行run()方法无返回值,通过实现接口的方式的话,还可以继承其他类。(注:实现接口的创建线程的方式必须实现方法(run()/call()))
2、这种方式下,多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
3、这种方式下需要使用Thread.currentThread()方法访问当前线程,而继承Thread类得方法只需要this就能获取当前线程,编程比较简单。
4、继承Thread类的线程类不能再继承其他父类(由Java单继承的性质决定)。
5、前两种的线程创建方式(继承Thread和实现Runnable/Callable接口)如果创建关闭频繁会消耗系统资源影响性能,而使用线程池可以不用线程的时候放回线程池,用的时候再从线程池取,项目开发中主要使用线程池的方式创建多个线程。
三、线程池七大参数含义
下面是ThreadPoolExecutor含7个参数的构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
1、corePoolSize - 池中所保存的线程数,包括空闲线程。需要注意的是在初创建线程池时线程不会立即启动,直到有任务提交才开始启动线程并逐渐时线程数目达到corePoolSize。若想一开始就创建所有核心线程需调用prestartAllCoreThreads方法。
2、maximumPoolSize - 池中允许的最大线程数。需要注意的是当核心线程满且阻塞队列也满时才会判断当前线程数是否小于最大线程数,并决定是否创建新线程。
3、keepAliveTime - 当线程数大于核心时,多于的空闲线程最多存活时间。
4、unit - keepAliveTime 参数的时间单位。
5、workQueue - 当线程数目超过核心线程数时用于保存任务的队列。主要有3种类型的BlockingQueue可供选择:无界队列,有界队列和同步移交。
6、threadFactory - 执行程序创建新线程时使用的工厂。
7、handler - 阻塞队列已满且线程数达到最大值时所采取的饱和策略。java默认提供了4种饱和策略的实现方式:中止、抛弃、抛弃最旧的、调用者运行。
四、总结/防杠
其实从本质上来讲,创建线程本质只有1种,即创建Thread类,以上的所谓的各种创建方式其实是实现run方法的方式:
1. 要么是通过继承Thread类重写其run()方法,Thread.start()会执行run()方法;
2. 要么是通过实现Runnable/Callable接口的run()方法,把其实例作为target对象传给Thread类,最终调用target里自己实现的run()方法。
3. 线程池亦如此,通过ExecutorService实例里的execute()方法,将我们实现了run()方法的Runnable/Callable实例作为参数执行。(这里底层代码会比较复杂,不过我们还是能够从发现最终还是调用了Thread.start()方法。因此第3条个人感觉可以并入第2条。)