多线程的前置知识点
1 线程的几种状态
-
new 尚未启动的线程处于此状态 是指
还没调用Thread实例的start()方法。
Thread thread = new Thread(()->{}); System.out.println(thread.getState()); // 输出 NEW
-
runnable 在Java虚拟机中执行的线程处于此状态。
-
blocked 被阻塞等待监视器锁定的线程处于此状态。
-
waiting 正在等待另一个线程执行特定动作的线程处于此状态。
调用如下3种方法会使线程进入等待状态:
- Object.wait():使当前线程处于等待状态直到另⼀个线程唤醒它;
- Thread.join():等待线程执行完毕,底层调用的是Object实例的wait⽅法;
- LockSupport.park():除非获得调用许可,否则禁用当前线程进⾏线程调度。
-
timed_waiting
超时等待状态
。线程等待⼀个具体的时间,时间到后会被⾃动唤醒。调用如下方法会使线程进入超时等待状态:
-
Thread.sleep(long millis):使当前线程睡眠指定时间;
-
Object.wait(long timeout):线程休眠指定时间,等待期间可以通过
notify()/notifyAll()唤醒;
-
Thread.join(long millis):等待当前线程最多执⾏millis毫秒,如果millis为0,则
会⼀直执⾏;
-
LockSupport.parkNanos(long nanos): 除⾮获得调⽤许可,否则禁止当前线
程进⾏线程调度指定时间;
-
LockSupport.parkUntil(long deadline):同上,也是禁⽌线程进⾏调度指定时
间。
-
-
terminated 终⽌状态。此时线程已执⾏完毕。
线程状态转换图
关于start()的两个引申问题
- 反复调用同一个线程的start()的方法是否可行?
- 假如一个线程执行完毕(此时处于terminated状态),再次调用这个线程的start()是否可行?
先来看看start()的源码:
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
我们可以看到,在start()内部,这⾥有⼀个threadStatus的变量。如果它不等于0, 调⽤start()是会直接抛出异常的。
我们可以通过debug的方式看一下:
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("测试start方法");
}, "a");
thread.start();
thread.start();
}
我是在start()⽅法内部的最开始打的断点,打断点看到的结果:
- 第⼀次调⽤时threadStatus的值是0。
- 第⼆次调⽤时threadStatus的值不为0。
查看当前线程状态的源码:
// Thread.getState()方法源码
public State getState() {
// get current thread state
return sun.misc.VM.toThreadState(threadStatus);
}
// sun.misc.VM 源码
public static State toThreadState(int var0) {
if ((var0 & 4) != 0) {
return State.RUNNABLE;
} else if ((var0 & 1024) != 0) {
return State.BLOCKED;
} else if ((var0 & 16) != 0) {
return State.WAITING;
} else if ((var0 & 32) != 0) {
return State.TIMED_WAITING;
} else if ((var0 & 2) != 0) {
return State.TERMINATED;
} else {
return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
}
}
结合上面的源码可以得到两个问题的结果:
两个问题的答案都是不可行的,在调用一次start()之后,threadStatus的值会改变(threadStatus!=0),此时再次调用start()方法会抛出IllegalThreadStateException异常。
2 三种线程初始化方法
-
继承Thread类、重写run方法
class MyThread extends Thread{ @Override public void run() { System.out.println("继承Thread类"); } } public class CreateThread { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); } }
-
实现Runnbale接口,重写run方法
class MyThread implements Runnable{ @Override public void run() { System.out.println("实现Runnable接口"); } } public class CreateThread { public static void main(String[] args) { MyThread myThread = new MyThread(); Thread thread = new Thread(myThread); thread.start(); } }
-
通过Callable和FutureTask创建线程
class MyThread implements Callable<Integer> { @Override public Integer call() throws Exception { System.out.println("通过Callable创建线程"); return 1; } } public class CreateThread { public static void main(String[] args) { Callable<Integer> callable = new MyThread(); FutureTask<Integer> futureTask = new FutureTask<Integer>(callable); Thread thread = new Thread(futureTask); thread.start(); } }
-
区别
-
Thread有单继承的局限性,若一个类继承线程Thread就不能继承其他类。
-
Runnable是接口,可以多实现,解决了单继承的问题,不过没有返回值。
-
Callable是接口,线程运行完后会有返回值,是泛型的,可以自己指定。
-
Callable的call()可以抛出异常。而Runnable只有通过setUncaughtExceptionHandler()的方式才能在主线程中捕捉到子线程异常。
class MyThread implements Runnable{ @Override public void run() { System.out.println("实现Runnable接口"); int i = 10/0; } } public class CreateThread { public static void main(String[] args) { MyThread myThread = new MyThread(); Thread thread = new Thread(myThread); thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { System.out.println(t + "\t"+e.getMessage()); } }); thread.start(); } }
-
3 线程池
3.1 为什么要使用线程池
使用线程池主要有以下三个原因:
- 创建/销毁线程需要消耗系统资源,
线程池可以复用已创建的线程。
控制并发的数量
。并发数量过多,可能会导致资源消耗过多,从而造成服务器,从而造成服务器崩溃。- 可以
对线程做统一管理
。
3.2 线程池原理
Java中线程池顶层接口是Executor接口,ThreadPoolExecutor
是这个接口的实现类。
先看看ThreadPoolExecutor
类。
ThreadPoolExecutor
提供的构造方法
// 五个参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
// 六个参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory)
// 六个参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
// 七个参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
七大参数分析
-
int corePoolSize:该线程池中常驻核心线程数。
核心线程
:线程池中有两类线程,核心线程和非核心线程。核心线程默认情况下会一直存在于线程池中,即使这个核心线程什么都不干(铁饭碗),而非核心线程如果长时间的闲置,就会被销毁(临时工)。 -
int maximumPoolSize:该线程池中线程总数最大值。
该值等于核心线程数量 + 非核心线程数量。
-
long keepAliveTime:非核心线程闲置超时时长
非核心线程如果处于闲置状态超过该值,就会被销毁。
-
TimeUnit unit:keepAliveTime的单位。
-
BlockingQueue workQueue:阻塞队列,维护着等待执行的Runnable任务对象。
常见的几个阻塞队列:
- LinkedBlockingQueue:链式阻塞队列,底层数据结构是链表,默认大小是
Integer.MAX_VALUE
,也可以指定大小。 - ArrayBlockingQueue:数组阻塞队列,底层数据结构是数组,需要指定队列的大小。
- SynchronousQueue:同步队列,内部容量为0,每个put操作必须等待一个take操作,反之亦然。
- DelayedWorkQueue:延迟队列,该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。
- LinkedBlockingQueue:链式阻塞队列,底层数据结构是链表,默认大小是
-
RejectedExecutionHandler handler :拒绝策略,表示当阻塞队列满了,并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝请求执行的runnable的策略)。
-
ThreadFactory threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认的即可。
线程池底层工作原理
- 在创建了线程池后,开始等待请求。
- 当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
- 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
- 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入阻塞队列;
- 如果这个时候阻塞队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
- 如果阻塞队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动
饱和和拒绝策略
来执行。
- 当一个线程完成任务时,它会从阻塞队列中去下一个任务来执行。
- 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断: 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉;所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小
四种拒绝策略
- ThreadPoolExecutor.AbortPolicy:默认拒绝处理策略,丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:丢弃新来的任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列中等待最久的任务,然后把当前任务加入队列中。
- ThreadPoolExecutor.CallerRunsPolicy:调用任务的run方法绕过线程池直接执行。
3.3 五种常见的线程池
Executors
类中提供的⼏个静态⽅法来创建线程池。
-
newCachedThreadPool:maximumPoolSize最大可以至
Integer.MAX_VALUE
,存在OOM风险。线程可回收。public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
-
newScheduledTheadPool:线程数最大至
Integer.MAX_VALUE
,存在OOM风险。线程不可回收。public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); } public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue()); }
-
newSingleThreadPool:创建一个单线程的线程池,相当于单线程串行执行所有任务,保证按任务的提交顺序依次执行。
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
-
newFixedThreadPool:固定线程数,不存在非核心线程(空闲线程)。
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
-
newWorkStealingPool:JDK8引入,创建持有足够线程的线程池支持给定的并行度,并通过使用多个队列减少竞争,此构造方法中把CPU的数量设置为默认的并行度。
适合使用在很耗时的任务中
。public static ExecutorService newWorkStealingPool() { return new ForkJoinPool (Runtime.getRuntime().availableProcessors(), ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true); }