一、实现线程方式
1.1 类继承Thread类
Thread本质上是实现了Runnable接口的实例,自己类继承Thread类,等于重写了Thread对Runnable接口的实现。启动线程的唯一方法就是通过Thread类的start()方法.
/**
* @Description
* @Date 2021/07/15 17:11
* @Created by cc
*/
public class MyThread extends Thread{
@Override
public void run(){
System.out.println("MyThread");
}
public static void main(String[] args){
Thread myThread = new MyThread();
myThread.start();
}
}
1.2 实现Runnable接口
实现Runnable接口,这个在类已经继承了一个类的场景下用的比较多,因为Java的类只能extends一个类。Runnable在运行时,需要占用一个Thread实例,然后通过Thread类的start()方法启动。所以实现代码如下:
/**
* @Description
* @Date 2021/07/15 17:16
* @Created by cc
*/
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("MyRunnable");
}
public static void main(String[] args){
Thread myThread = new Thread(new MyRunnable());
myThread.start();
}
}
1.3 实现Callable接口
需要获取异步线程执行的返回值时,可以实现Callable接口,执行Callable任务后,获取一个Future对象,通过Future的get()方法可以获取异步线程返回值。这里线程会自主检查Future是否都返回,是个阻塞过程,需要等到所有线程任务执行完成,才会继续获取Future之后的代码流程。
public class MyCallable implements Callable<String> {
private int i;
public MyCallable(int i){
this.i = i;
}
@Override
public String call() throws Exception {
Thread.sleep(1000);
return "this is thread-"+i;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask futureTask = new FutureTask(new MyCallable(10));
new Thread(futureTask).start();
System.out.println(futureTask.get());
}
}
二、线程池
线程是非常宝贵的资源,每次使用时才创建,使用完就销毁,非常耗时和消耗资源,所以将线程先预先初始化一定数量到内存,即线程池。
Java里面线程池最顶级的接口是Executor,但是严格意义上讲,Executor并不是一个线程池,而只是一个执行线程的工具,真正的线程池接口是ExecutorService。
ThreadExectorPool为了使用方便,提供了有四种线程池工厂:CachedThreadPool、FixedThreadPool、ScheduledThreadPool、SingleThreadExecutor。
2.1 CachedThreadPool
可缓存的线程池,线程空闲最长时间为60s,无限大的线程池,如果线程池的大小大于执行任务的大小,空闲线程(超过60s)会被回收。比较适合处理时间比较小的任务。
线程池接口源码如下
public static ExecutorService newCachedThreadPool(){
return new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.MILLISECONDS,new SynchronousQueue<Runnable>());
}
通过源码接口,可以看出CachedThreadPool的一些特性:
- 一个可以无限扩大的线程池,最大为JVM能够承受的最大线程数量
- corePoolSize为0,即回收线程最小剩余为0
- keepAliveTime为60s.
- 采用 SynchronousQueue 队列存储等待任务,这个阻塞队列没有存储空间,所以只要有新任务过来,就必须要新生成线程处理任务。
2.2 FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
特点分析:
- 是一种固定大小的线程池
- corePoolSize和maximunPoolSize都为用户设定的线程数量nThreads
- keepAliveTime为0,说明线程一旦空闲,就会被回收,但是没有什么用,因为已经固定corePoolSize数量和maximunPoolSize,并且二者相等,所以不会出现空闲线程,多的任务只会在队列中等待。
- 阻塞队列使用LinkedBlockingQueue,是一个无界队列。
- 由于阻塞队列是一个无界队列,因此永远不可能拒绝到来的任务。
- 有采用了无界队列,实际线程数将永远维持在nThreads数量,因此maximunPoolSize和keepAliveTime将没有用途。
2.3 SingleThreadExecutor
public static ExecutorService newSingleThreadExecutor(){
return new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}
- 只会有一条线程工作
- corePoolSize为1,
- 采用无界阻塞队列LinkedBlockingQueue存储等待任务。
2.4 ScheduledThreadPool
可通过参数设定延迟执行或者定时执行任务的线程池,构造接口可分为两类:仅指定corePoolSize和
(1)仅指定corePoolSize
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
- 指定核心线程数,corePoolSize
- maximunPoolSize为无限大
- 使用DelayedWorkQueue作为等待任务的存储队列,此队列为无界队列。
- keepAliveTime为0,由于DelayedWorkQueue为无界队列,所以keepAliveTime和maximunPoolSize无效
(2)ScheduledThreadPool实现延迟、定时任务
ScheduledThreadPool不仅仅继承了ExecutorService类,同时还继承了ScheduledExecutorService,ScheduledExecutorService也继承ExecutorService。在ScheduledExecutorService类中提供了延迟和定时任务的线程任务支持。方法源码接口如下:
- schedule(Runnable command,long delay,TimeUnit unit)
public ScheduledFuture<?> schedule(Runnable command,
long delay, TimeUnit unit);
* command: 实现Runnable接口的类
* delay:延迟执行时间
* unit:时间的单位
- schedule(Callable<String> callable,long delay,TimeUnit unit)
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
long delay, TimeUnit unit);
* callable:带返回的线程实现类
*delay:延迟执行时间
*unit:时间单位
- scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit)
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
- scheduleWithFixedDelay(Runnable command,long initialDelay,long period,TimeUnit unit)
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
说明:scheduleAtFixedRate和scheduleWithFixedDelay都是带延迟、定时功能的线程池执行任务接口,区别在于scheduleAtFixedRate只在线程池任务开始时延迟,scheduleWithFixedDelay线程池的每个任务都会延迟。
三、自建线程池
以上四个线程池的配置,是jdk提供,但是用着很不方便,比如队列无界、最大线程数参数无效、拒绝策略只是默认的抛出异常,阻止系统正常运行。
自己配置一个线程池,需要定义ThreadExectorPool对象,ThreadExectorPool提供的构造接口如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {}
- corePoolSize:核心线程池数量
- maxinumPoolSize:最大线程池数量
- keepAliveTime:线程空闲时间
- unit:时间单位
- workQueue:线程任务阻塞队列,后面介绍线程池中使用的队列类型以及功能。
- treadFactory:线程生成工厂,默认即可。
- handler:线程任务数量大于maximumPoolSize时,执行拒绝线程任务的操作。
3.1 阻塞队列介绍
3.1.1 ArrayBlockingQueue
由数组构成的有界阻塞队列,可以设置队列的大小,最大特色是可以设置公平队列,但是设置成公平队列后,会降低吞吐量。第二个参数设置成true,即可实现公平队列。
ArrayBlockingQueue queue1 = new ArrayBlockingQueue(1000,true);
3.1.2 LinkedBlockingQueue
基于链表的阻塞队列,是最常用的阻塞队列,其能高效的处理并发数据,是因为其在访问者和消费者两端使用独立的锁来保证数据的同步,即生产者和消费者可以同时访问队列。
LinkedBlockingQueue默认大小为Integer.MAX_VALUE,可主动设置队列大小。
LinkedBlockingQueue queue2 = new LinkedBlockingQueue(666);
3.1.3 PriorityBlockingQueue
是一个支持优先级的无界队列,默认情况是对元素采取自然顺序生序排列,可自定义compareTo()方法,来指定元素的进行排序规则。
3.1.4 DelayQueue
支持延迟获取元素的无界阻塞队列。队列中的元素必须实现Delayed接口,在创建元素时,可以指定元素多久才能从队列中出来,只有延迟期满时,才能从队列中获取到元素。
使用场景:
- 缓存系统,一旦能从DelayQueue中获取到缓存元素,表示元素的缓存有效期到了。
- 定时任务,delayQueue存储了定时了任务,一旦能取出定时任务,表示定时任务执行时间到了。ScheduledThreadPool使用了DelayWorkQueue
3.1.5 SynchronousQueue
不存储数据队列。
3.1.6 LinkedTranferQueue
由链表实现的无界阻塞队列。
3.1.7 LinkedBlockingDeque
链表实现的双向阻塞队列,可以从表头或者表尾拿元素。
总结:阻塞队列是以上7种,但是在线程池使用时,一般使用到的是LinkedBlockingQueue和SynchronousQueue,LinkedBlockingQueue使用场景很好理解,SynchronousQueue使用场景参照CacheThreadPool,想有任务过来,立马开启线程处理。DelayQueue也可以用作定时任务线程池,但是一般会直接使用Java提供的ScheduledThreadPool工厂获取线程池。
3.2 拒绝策略介绍
线程池默认的拒绝执行策略是直接抛出错误,阻止程序继续执行,Java还提供了其他拒绝策略供选择,具体如下:
- AbortPolicy:直接抛出异常,阻止程序继续运行。线程池默认拒绝策略。
- CallerRunsPolicy:有提交任务线程自己执行,这样会大大影响提交任务线程效率。
- DiscardOldestPolicy:丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交任务。
- DiscardPolicy:直接丢弃该任务,不做任何处理。
以上策略都实现了RejectedExecutionHandler接口,如果还不满足要求,可自行定义类实现RejectedExecutionHandler。
四、线程的生命周期
线程的生命周期分为新建、就绪、运行、阻塞、死亡五个状态。
3.1 新建状态
使用new关键字创建一个线程后,改线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值。
3.2 就绪状态
调用了start()方法后,线程处于就绪状体啊,JVM会为其创建方法调用栈合程序计数器,等待调度运行。
3.3 运行状态
处于就绪状态的线程获得CPU的时间片,开始执行线程的run()方法。
3.4 阻塞状态
线程由于某种原因放弃了CPU的使用权,即让出了CPU timeSlice,暂时停止运行。知道线程进入就绪(Runnable)状态,才有权利重新获取CPU的时间切片,进而继续运行。
线程的阻塞状态分为三种:
等待阻塞,o.wait(),JVM会把线程放到等待队列,等待唤醒。
同步阻塞,同步锁被其他线程占用而导致的阻塞。
其他阻塞(sleep/join),运行(Running)状态的线程调用Thread.sleep()方法或者t.join()方法,需要等到sleep()方法超时,活着join的线程终止或者超时,该线程才会重新变成就绪(Runnable)状态,有机会重新申请CPU的时间切片。
3.5 线程死亡
即线程结束,线程结束的方式分为三种:
正常结束,run()或call()方法执行完成,线程正常结束。
异常结束,线程抛出一个未捕获的Exception或者Error
调用stop,该方法会造成共享数据不一致、损坏的情况,已经废弃。
四、sleep()和wait()
1、sleep()方法是属于线程Thread的,wait()是属于Object对象的。
2、sleep()方法使线程暂停执行,交出CPU给其他线程,但是不会释放已经获取的对象锁。
3、当线程调用wait() 方法时,线程会放弃对象锁,知道锁对象调用notify(),可以重新和其他等待锁定池中的阻塞线程竞争获取对象锁。
五、JAVA的后台线程
Java的后台线程,又名守护线程,也成为服务线程,是为用户线程提供公共服务的,在没有用户线程可服务时,守护线程也立即离开。
通过setDaemon(true)方法,设置线程为守护线程。
比较显著的历史是GC线程, 当程序中不再有其他Thread时,GC线程也会退出。