程序员究竟要不要读源码?
很多人觉得读源码比较枯燥,确实,读源码是要比看那些表面教你如何使用的文章要枯燥的多,也比不上刷抖音和微博来的轻松愉 快。但是,读源码是一名程序员突破自我瓶颈,获得高薪和升职加薪的一个有效途径。通过阅读优秀的开源框架的源码,我们能够领略到框架作者设计框架的思维和思路,从中学习优秀的架构设计和代码设计。这些都是在那些只告诉你如何使用的文章中所学不
到的,就更别提是刷抖音和微博了。 当你只停留在业务层面的 CRUD 开发而不思进取时,工作几年之后,你会发现你几乎除了使用啥都不会!此时,你在职场其实是毫无竞争优势的。你所反反复复做的工作对于刚入行的毕业生来说,给他们3 个月时间,他们就能熟练上手。而你,反反复复做了几年的CRUD,没啥改变。对于企业来说,他们更加愿意雇佣那些成本低廉的新手,而不愿雇佣你!为啥?因为你给企业产出的价值未必比新入行的新手高,而你为企业带来的成本却远远高于新手!看到这里,知道为啥你工作几年后,想跳槽时,面试一个月薪几万+ 的职位,却只能仰望叹气了吧!!而比你工作年限少的人,却能够轻松面试比你薪资高出好几倍的职位!!不是他们运气好,而是他们比你掌握了更加深入的技能!!
当你在几年的工作时间里做的都是 CRUD 时,其实你的工作经验只有 3 个月;当你在 3 个月里,充分为自己规划好,在掌握基础业务开发的同时,抽时间为自己充电,掌握一些更加深入的技能,则你的工作经验会高于那些混迹职场几年的CRUD 人员。
在职场还有一个现象,就是在某些企业会有一些不断加班疯狂撸代码的人,不是公司压榨员工,就是员工本身能力不行。当然,公司开发人员比较少,项目时间短的情况可以除外。往往那些疯狂加班撸代码的都是长期的CRUD 者,他们干的比谁都累,拿的比谁都少。往往那些掌握了深入技能的人,看似很轻松,但是他们单位时间产出的价值远远高于CRUD 人员疯狂撸一天代码产出的价值,因为那些CRUD 人员一天下来产出的 Bug ,需要三天时间进行修正!!!
其实在职场,对于每个人非常重要的技能就是提升自己的核心竞争力,让自己变得更加有价值。
希望能够唤起你对知识的渴望。记住:工作年限并不等于工作经验!!!
线程与线程池
线程与多线程
1. 线程
在操作系统中,线程是比进程更小的能够独立运行的基本单位。同时,它也是 CPU 调度的基本单位。线程本身基本上不拥有系统资源,只是拥有一些在运行时需要用到的系统资源,例如程序计数器,寄存器和栈等。一个进程中的所有线程可以共享进程中的所有资源。
2. 多线程
多线程可以理解为在同一个程序中能够同时运行多个不同的线程来执行不同的任务,这些线程可以同时利用 CPU 的多个核心运行。
多线程编程能够最大限度的利用 CPU 的资源。如果某一个线程的处理不需要占用 CPU 资源时(例如 IO 线程),可以使当前线程让出CPU资源来让其他线程能够获取到 CPU 资源,进而能够执行其他线程对应的任务,达到最大化利用 CPU 资源的目的。
线程的实现方式
在 Java 中,实现线程的方式大体上分为三种,通过继承 Thread 类、实现 Runnable 接口,实现 Callable接口。
线程的生命周期
1. 生命周期
一个线程从创建,到最终的消亡,需要经历多种不同的状态,而这些不同的线程状态,由始至终也构成了线程生命周期的不同阶段。线程的生命周期可以总结为下图。
其中,几个重要的状态如下所示。
NEW :初始状态,线程被构建,但是还没有调用 start() 方法。
RUNNABLE :可运行状态,可运行状态可以包括:运行中状态和就绪状态。
BLOCKED :阻塞状态,处于这个状态的线程需要等待其他线程释放锁或者等待进入 synchronized 。
WAITING :表示等待状态,处于该状态的线程需要等待其他线程对其进行通知或中断等操作,进而进入下一个状态。
TIME_WAITING :超时等待状态。可以在一定的时间自行返回。
TERMINATED :终止状态,当前线程执行完毕。
线程的执行顺序
线程的执行顺序是不确定的
调用 Thread 的 start() 方法启动线程时,线程的执行顺序是不确定的。也就是说,在同一个方法中,连续创建多个线程后,调用线程的start() 方法的顺序并不能决定线程的执行顺序。
如何确保线程的执行顺序
1. 确保线程执行顺序的简单示例
在实际业务场景中,有时,后启动的线程可能需要依赖先启动的线程执行完成才能正确的执行线程中的业务逻辑。此时,就需要确保线程的执行顺序。那么如何确保线程的执行顺序呢?
可以使用 Thread 类中的 join() 方法来确保线程的执行顺序。
public class ThreadSort02 {
public static void main ( String [] args ) throws InterruptedException {
Thread thread1 = new Thread (() -> {
System . out . println ( "thread1" );
});
Thread thread2 = new Thread (() -> {
System . out . println ( "thread2" );
});
Thread thread3 = new Thread (() -> {
System . out . println ( "thread3" );
});
thread1 . start ();
// 实际上让主线程等待子线程执行完成
thread1 . join ();
thread2 . start ();
thread2 . join ();
thread3 . start ();
thread3 . join ();
}
}
需要注意的是,调用线程的 wait() 方法时,会使主线程处于等待状态,等待子线程执行完成后再次向下执行。也就是说,在 ThreadSort02 类的 main() 方法中,调用子线程的 join() 方法,会阻塞 main() 方法的执行,当子线程执行完成后, main() 方法会继 续向下执行,启动第二个子线程,并执行子线程的业务逻辑,以此类推。
Java 中的 Callable 和 Future
在 Java 的多线程编程中,除了 Thread 类和 Runnable 接口外,不得不说的就是 Callable 接口 Future 接口了。使用继承 Thread 类或者实现Runnable 接口的线程,无法返回最终的执行结果数据,只能等待线程执行完成。此时,如果想要获取线程执行后的返回结果,那
么, Callable 和 Future 就派上用场了。
Callable 接口
1.Callable 接口介绍
Callable 接口是 JDK1.5 新增的泛型接口,在 JDK1.8 中,被声明为函数式接口,如下所示。
@FunctionalInterface
public interface Callable < V > {
V call () throws Exception ;
}
在 JDK 1.8 中只声明有一个方法的接口为函数式接口,函数式接口可以使用 @FunctionalInterface 注解修饰,也可以不使用 @FunctionalInterface注解修饰。只要一个接口中只包含有一个方法,那么,这个接口就是函数式接口。 在JDK 中,实现 Callable 接口的子类如下图所示
Executors 类中的静态内部类: PrivilegedCallable 、 PrivilegedCallableUsingCurrentClassLoader 、 RunnableAdapter 和 Task类下的 TaskCallable 。
2. 实现 Callable 接口的重要类分析
接下来,分析的类主要有: PrivilegedCallable 、 PrivilegedCallableUsingCurrentClassLoader 、 RunnableAdapter 和 Task 类下的TaskCallable。虽然这些类在实际工作中很少被直接用到,但是作为一名合格的开发工程师,设置是秃顶的资深专家来说,了解并掌握这些类的实现有助你进一步理解Callable 接口,并提高专业技能(头发再掉一批,哇哈哈哈。。。)。
PrivilegedCallablePrivilegedCallable类是 Callable 接口的一个特殊实现类,它表明 Callable 对象有某种特权来访问系统的某种资源.
两种异步模型与深度解析 Future 接口
两种异步模型
在 Java 的并发编程中,大体上会分为两种异步编程模型,一类是直接以异步的形式来并行运行其他的任务,不需要返回任务的结果数据。一类是以异步的形式运行其他任务,需要返回结果。
1. 无返回结果的异步模型
无返回结果的异步任务,可以直接将任务丢进线程或线程池中运行,此时,无法直接获得任务的执行结果数据,一种方式是可以使用回调方法来获取任务的运行结果。
具体的方案是:定义一个回调接口,并在接口中定义接收任务结果数据的方法,具体逻辑在回调接口的实现类中完成。将回调接口与任务参数一同放进线程或线程池中运行,任务运行后调用接口方法,执行回调接口实现类中的逻辑来处理结果数据。这里,给出一个简单的示例供参考。
2. 有返回结果的异步模型
尽管使用回调接口能够获取异步任务的结果,但是这种方式使用起来略显复杂。在 JDK 中提供了可以直接返回异步结果的处理方案。 最常用的就是使用Future 接口或者其实现类 FutureTask 来接收任务的返回结果。 使用Future 接口获取异步结果使用Future 接口往往配合线程池来获取异步执行结果
public static void main ( String [] args ) throws ExecutionException , InterruptedException {
ExecutorService executorService = Executors . newSingleThreadExecutor ();
Future < String > future = executorService . submit ( new Callable < String > () {
@Override
public String call () throws Exception {
return " 测试 Future 获取异步结果 " ;
}
});
System . out . println ( future . get ());
executorService . shutdown ();
}
使用 FutureTask 类获取异步结果 FutureTask类既可以结合 Thread 类使用也可以结合线程池使用,接下来,就看下这两种使用方式。 结合Thread 类的使用示例如下所示。
public static void main ( String [] args ) throws ExecutionException , InterruptedException {
FutureTask < String > futureTask = new FutureTask <> ( new Callable < String > () {
@Override
public String call () throws Exception {
return " 测试 FutureTask 获取异步结果 " ;
}
});
new Thread ( futureTask ). start ();
System . out . println ( futureTask . get ());
}
可以看到,在 Future 接口中,总共定义了 5 个抽象方法。接下来,就分别介绍下这 5 个方法的含义。
cancel(boolean) 取消任务的执行,接收一个boolean 类型的参数,成功取消任务,则返回 true ,否则返回 false 。当任务已经完成,已经结束或者因其他原因不能取消时,方法会返回false ,表示任务取消失败。当任务未启动调用了此方法,并且结果返回 true (取消成功),则当前任务不再运行。如果任务已经启动,会根据当前传递的boolean 类型的参数来决定是否中断当前运行的线程来取消当前运行的任务。
isCancelled() 判断任务在完成之前是否被取消,如果在任务完成之前被取消,则返回true ;否则,返回 false 。
这里需要注意一个细节:只有任务未启动,或者在完成之前被取消,才会返回 true ,表示任务已经被成功取消。其他情况都会返回 false 。
isDone()
判断任务是否已经完成,如果任务正常结束、抛出异常退出、被取消,都会返回 true ,表示任务已经完成。
get() 当任务完成时,直接返回任务的结果数据;当任务未完成时,等待任务完成并返回任务的结果数据。
get(long, TimeUnit) 当任务完成时,直接返回任务的结果数据;当任务未完成时,等待任务完成,并设置了超时等待时间。在超时时间内任务完成,则返回结果;否则,抛出TimeoutException 异常。
2.RunnableFuture 接口
Future 接口有一个重要的子接口,那就是 RunnableFuture 接口, RunnableFuture 接口不但继承了 Future 接口,而且继承了
java.lang.Runnable 接口,其源代码如下所示。
package java . util . concurrent ;
public interface RunnableFuture < V > extends Runnable , Future < V > {
void run ();
}
这个接口比较简单 run() 方法就是运行任务时调用的方法。
3.FutureTask 类
FutureTask 类是 RunnableFuture 接口的一个非常重要的实现类,它实现了 RunnableFuture 接口、 Future 接口和 Runnable 接口的所
有方法。 FutureTask 类的源代码比较多,这个就不粘贴了,大家自行到 java.util.concurrent 下查看。
( 1 ) FutureTask 类中的变量与常量
在 FutureTask 类中首先定义了一个状态变量 state ,这个变量使用了 volatile 关键字修饰,这里,大家只需要知道 volatile 关键字通过内存屏障和禁止重排序优化来实现线程安全,后续会单独深度分析volatile 关键字是如何保证线程安全的。紧接着,定义了几个任务运行时的状态常量
又看到我们所熟悉的 Callable 接口了, Callable 接口那肯定就是用来调用 call() 方法执行具体任务了。
outcome : Object 类型,表示通过 get() 方法获取到的结果数据或者异常信息。
runner :运行 Callable 的线程,运行期间会使用 CAS 保证线程安全,这里大家只需要知道 CAS 是 Java 保证线程安全的一种方式, 后续会深度分析CAS 如何保证线程安全。
waiters : WaitNode 类型的变量,表示等待线程的堆栈,在 FutureTask 的实现中,会通过 CAS 结合此堆栈交换任务的运行状态。
看一下 WaitNode 类的定义,如下所示。
static final class WaitNode {
volatile Thread thread ;
volatile WaitNode next ;
WaitNode () { thread = Thread . currentThread (); }
}
可以看到, WaitNode 类是 FutureTask 类的静态内部类,类中定义了一个 Thread 成员变量和指向下一个 WaitNode 节点的引用。其中通过构造方法将thread 变量设置为当前线程。
SimpleDateFormat 类的线程安全问题
提起 SimpleDateFormat 类,想必做过 Java 开发的童鞋都不会感到陌生。没错,它就是 Java 中提供的日期时间的转化类。这里,为什 么说SimpleDateFormat 类有线程安全问题呢?有些小伙伴可能会提出疑问:我们生产环境上一直在使用 SimpleDateFormat 类来解
析和格式化日期和时间类型的数据,一直都没有问题啊!我的回答是:没错,那是因为你们的系统达不到 SimpleDateFormat 类出现 问题的并发量,也就是说你们的系统没啥负载!
接下来,我们就一起看下在高并发下 SimpleDateFormat 类为何会出现安全问题,以及如何解决 SimpleDateFormat 类的安全问题。
重现 SimpleDateFormat 类的线程安全问题
为了重现 SimpleDateFormat 类的线程安全问题,一种比较简单的方式就是使用线程池结合 Java 并发包中的 CountDownLatch 类和 Semaphore类来重现线程安全问题。
SimpleDateFormat 类为何不是线程安全的
那么,接下来,我们就一起来看看真正引起 SimpleDateFormat 类线程不安全的根本原因。
通过查看 SimpleDateFormat 类的源码,我们得知: SimpleDateFormat 是继承自 DateFormat 类, DateFormat 类中维护了一个全局
的 Calendar 变量,如下所示。
在 CalendarBuilder.establish() 方法中先后调用了 cal.clear() 与 cal.set() ,也就是先清除 cal 对象中设置的值,再重新设置新的值。由于
Calendar 内部并没有线程安全机制,并且这两个操作也都不是原子性的,所以当多个线程同时操作一个 SimpleDateFormat 时就会引
起 cal 的值混乱。类似地, format() 方法也存在同样的问题。
因此, SimpleDateFormat 类不是线程安全的根本原因是: DateFormat 类中的 Calendar 对象被多线程共享,而 Calendar 对象本
身不支持线程安全。
那么,得知了 SimpleDateFormat 类不是线程安全的,以及造成 SimpleDateFormat 类不是线程安全的原因,那么如何解决这个问题 呢?接下来,我们就一起探讨下如何解决SimpleDateFormat 类在高并发场景下的线程安全问题。
解决 SimpleDateFormat 类在高并发场景下的线程安全问题可以有多种方式,这里,就列举几个常用的方式供参考,大家也可以在评论区给出更多的解决方案。
1. 局部变量法
最简单的一种方式就是将 SimpleDateFormat 类对象定义成局部变量,如下所示的代码,将 SimpleDateFormat 类对象定义在 parse(String)方法的上面,即可解决问题。
public class SimpleDateFormatTest02 {
// 执行总次数
private static final int EXECUTE_COUNT = 1000 ;
// 同时运行的线程数量
private static final int THREAD_COUNT = 20 ;
public static void main ( String [] args ) throws InterruptedException {
final Semaphore semaphore = new Semaphore ( THREAD_COUNT );
final CountDownLatch countDownLatch = new CountDownLatch ( EXECUTE_COUNT );
ExecutorService executorService = Executors . newCachedThreadPool ();
for ( int i = 0 ; i < EXECUTE_COUNT ; i ++ ){
executorService . execute (() -> {
try {
semaphore . acquire ();
try {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat ( "yyyy-MM-dd" );
simpleDateFormat . parse ( "2020-01-01" );
} catch ( ParseException e ) { 此时运行修改后的程序,输出结果如下所示。
至于在高并发场景下使用局部变量为何能解决线程的安全问题,会在【 JVM 专题】的 JVM 内存模式相关内容中深入剖析,这里不做过
多的介绍了。
当然,这种方式在高并发下会创建大量的 SimpleDateFormat 类对象,影响程序的性能,所以,这种方式在实际生产环境不太被推
荐。
2.synchronized 锁方式
将 SimpleDateFormat 类对象定义成全局静态变量,此时所有线程共享 SimpleDateFormat 类对象,此时在调用格式化时间的方法
时,对 SimpleDateFormat 对象进行同步即可,代码如下所示。
System . out . println ( " 线程: " + Thread . currentThread (). getName () + " 格式化日期失
败 " );
e . printStackTrace ();
System . exit ( 1 );
} catch ( NumberFormatException e ){
System . out . println ( " 线程: " + Thread . currentThread (). getName () + " 格式化日期失
败 " );
e . printStackTrace ();
System . exit ( 1 );
}
semaphore . release ();
} catch ( InterruptedException e ) {
System . out . println ( " 信号量发生错误 " );
e . printStackTrace ();
System . exit ( 1 );
}
countDownLatch . countDown ();
});
}
countDownLatch . await ();
executorService . shutdown ();
System . out . println ( " 所有线程格式化日期成功 " );
}
}
2.synchronized 锁方式
将 SimpleDateFormat 类对象定义成全局静态变量,此时所有线程共享 SimpleDateFormat 类对象,此时在调用格式化时间的方法 时,对SimpleDateFormat 对象进行同步即可
需要注意的是,虽然这种方式能够解决 SimpleDateFormat 类的线程安全问题,但是由于在程序的执行过程中,为 SimpleDateFormat类对象加上了 synchronized 锁,导致同一时刻只能有一个线程执行 parse(String) 方法。此时,会影响程序的执行性能,在要求高并发的生产环境下,此种方式也是不太推荐使用的。
3.Lock 锁方式
Lock 锁方式与 synchronized 锁方式实现原理相同,都是在高并发下通过 JVM 的锁机制来保证程序的线程安全。
4.ThreadLocal 方式
使用 ThreadLocal 存储每个线程拥有的 SimpleDateFormat 对象的副本,能够有效的避免多线程造成的线程安全问题,使用
ThreadLocal 解决线程安全问题的代码如下所示。
5.
DateTimeFormatter 是 Java8 提供的新的日期时间 API 中的类, DateTimeFormatter 类是线程安全的,可以在高并发场景下直接使用
DateTimeFormatter 类来处理日期的格式化操作。代码如下所示。
深度解析 ThreadPoolExecutor 类源码
Thread 直接创建线程的弊端
( 1 )每次 new Thread 新建对象,性能差。
( 2 )线程缺乏统一管理,可能无限制的新建线程,相互竞争,有可能占用过多系统资源导致死机或 OOM 。
( 3 )缺少更多的功能,如更多执行、定期执行、线程中断。
( 4 )其他弊端,大家自行脑补,多动脑,没坏处,哈哈。
线程池的好处
( 1 )重用存在的线程,减少对象创建、消亡的开销,性能佳。
( 2 )可以有效控制最大并发线程数,提高系统资源利用率,同时可以避免过多资源竞争,避免阻塞。
( 3 )提供定时执行、定期执行、单线程、并发数控制等功能。
( 4 )提供支持线程池监控的方法,可对线程池的资源进行实时监控。
( 5 )其他好处,大家自行脑补,多动脑,没坏处,哈哈。
线程池
1. 线程池类结构关系
2. 创建线程池常用的类 ——Executors
Executors.newCachedThreadPool :创建一个可缓存的线程池,如果线程池的大小超过了需要,可以灵活回收空闲线程,如果 没有可回收线程,则新建线程
Executors.newFixedThreadPool :创建一个定长的线程池,可以控制线程的最大并发数,超出的线程会在队列中等待 Executors.newScheduledThreadPool:创建一个定长的线程池,支持定时、周期性的任务执行 Executors.newSingleThreadExecutor: 创建一个单线程化的线程池,使用一个唯一的工作线程执行任务,保证所有任务按照指定顺序(先入先出或者优先级)执行
Executors.newSingleThreadScheduledExecutor: 创建一个单线程化的线程池,支持定时、周期性的任务执行 Executors.newWorkStealingPool:创建一个具有并行级别的 work-stealing 线程池
3. 线程池实例的几种状态
Running: 运行状态,能接收新提交的任务,并且也能处理阻塞队列中的任务
Shutdown: 关闭状态,不能再接收新提交的任务,但是可以处理阻塞队列中已经保存的任务,当线程池处于 Running 状态时, 调用shutdown() 方法会使线程池进入该状态
Stop: 不能接收新任务,也不能处理阻塞队列中已经保存的任务,会中断正在处理任务的线程,如果线程池处于 Running 或 Shutdown状态,调用 shutdownNow() 方法,会使线程池进入该状态
Tidying: 如果所有的任务都已经终止,有效线程数为 0 (阻塞队列为空,线程池中的工作线程数量为 0 ),线程池就会进入该状态。
Terminated: 处于 Tidying 状态的线程池调用 terminated() 方法,会使用线程池进入该状态
注意:不需要对线程池的状态做特殊的处理,线程池的状态是线程池内部根据方法自行定义和处理的。
4. 合理配置线程的一些建议
( 1 ) CPU 密集型任务,就需要尽量压榨 CPU ,参考值可以设置为 NCPU+1(CPU 的数量加 1) 。
( 2 ) IO 密集型任务,参考值可以设置为 2*NCPU ( CPU 数量乘以 2 )
线程池最核心的类之一 ——ThreadPoolExecutor
1. 构造方法
ThreadPoolExecutor 参数最多的构造方法如下:
public ThreadPoolExecutor ( int corePoolSize ,
int maximumPoolSize ,
long keepAliveTime ,
TimeUnit unit ,
BlockingQueue < Runnable > workQueue ,
ThreadFactory threadFactory ,
RejectedExecutionHandler rejectHandler )
其他的构造方法都是调用的这个构造方法来实例化对象,可以说,我们直接分析这个方法之后,其他的构造方法我们也明白是怎么
回事了!接下来,就对此构造方法进行详细的分析。
注意:为了更加深入的分析 ThreadPoolExecutor 类的构造方法,会适当调整参数的顺序进行解析,以便于大家更能深入的理解
ThreadPoolExecutor 构造方法中每个参数的作用。
上述构造方法接收如下参数进行初始化:
( 1 ) corePoolSize :核心线程数量。
( 2 ) maximumPoolSize :最大线程数。
( 3 ) workQueue :阻塞队列,存储等待执行的任务,很重要,会对线程池运行过程产生重大影响。
其中,上述三个参数的关系如下所示:
如果运行的线程数小于 corePoolSize ,直接创建新线程处理任务,即使线程池中的其他线程是空闲的。
如果运行的线程数大于等于 corePoolSize ,并且小于 maximumPoolSize ,此时,只有当 workQueue 满时,才会创建新的线程处理任务。
如果设置的 corePoolSize 与 maximumPoolSize 相同,那么创建的线程池大小是固定的,此时,如果有新任务提交,并且 workQueue没有满时,就把请求放入到 workQueue 中,等待空闲的线程,从 workQueue 中取出任务进行处理。
如果运行的线程数量大于 maximumPoolSize ,同时, workQueue 已经满了,会通过拒绝策略参数 rejectHandler 来指定处理策略。
根据上述三个参数的配置,线程池会对任务进行如下处理方式:
当提交一个新的任务到线程池时,线程池会根据当前线程池中正在运行的线程数量来决定该任务的处理方式。处理方式总共有三种:直接切换、使用无限队列、使用有界队列。
直接切换常用的队列就是 SynchronousQueue 。
使用无限队列就是使用基于链表的队列,比如: LinkedBlockingQueue ,如果使用这种方式,线程池中创建的最大线程数就是
corePoolSize ,此时 maximumPoolSize 不会起作用。当线程池中所有的核心线程都是运行状态时,提交新任务,就会放入等待
队列中。
使用有界队列使用的是 ArrayBlockingQueue ,使用这种方式可以将线程池的最大线程数量限制为 maximumPoolSize ,可以降
低资源的消耗。但是,这种方式使得线程池对线程的调度更困难,因为线程池和队列的容量都是有限的了。
根据上面三个参数,我们可以简单得出如何降低系统资源消耗的一些措施:
如果想降低系统资源的消耗,包括 CPU 使用率,操作系统资源的消耗,上下文环境切换的开销等,可以设置一个较大的队列容量和较小的线程池容量。这样,会降低线程处理任务的吞吐量。
如果提交的任务经常发生阻塞,可以考虑调用设置最大线程数的方法,重新设置线程池最大线程数。如果队列的容量设置的较小,通常需要将线程池的容量设置的大一些,这样,CPU 的使用率会高些。如果线程池的容量设置的过大,并发量就会增加,则需要考虑线程调度的问题,反而可能会降低处理任务的吞吐量。接下来,我们继续看ThreadPoolExecutor 的构造方法的参数。
( 4 ) keepAliveTime :线程没有任务执行时最多保持多久时间终止
当线程池中的线程数量大于 corePoolSize 时,如果此时没有新的任务提交,核心线程外的线程不会立即销毁,需要等待,直到等待的时间超过了keepAliveTime 就会终止。
( 5 ) unit : keepAliveTime 的时间单位
( 6 ) threadFactory :线程工厂,用来创建线程
默认会提供一个默认的工厂来创建线程,当使用默认的工厂来创建线程时,会使新创建的线程具有相同的优先级,并且是非守护的线程,同时也设置了线程的名称
( 7 ) rejectHandler :拒绝处理任务时的策略
如果 workQueue 阻塞队列满了,并且没有空闲的线程池,此时,继续提交任务,需要采取一种策略来处理这个任务。
线程池总共提供了四种策略:
直接抛出异常,这也是默认的策略。实现类为 AbortPolicy 。
用调用者所在的线程来执行任务。实现类为 CallerRunsPolicy 。
丢弃队列中最靠前的任务并执行当前任务。实现类为 DiscardOldestPolicy 。
直接丢弃当前任务。实现类为 DiscardPolicy 。
2.ThreadPoolExecutor 提供的启动和停止任务的方法
( 1 ) execute(): 提交任务,交给线程池执行
( 2 ) submit(): 提交任务,能够返回执行结果 execute+Future
( 3 ) shutdown(): 关闭线程池,等待任务都执行完
( 4 ) shutdownNow(): 立即关闭线程池,不等待任务执行完
3.ThreadPoolExecutor 提供的适用于监控的方法
( 1 ) getTaskCount() :线程池已执行和未执行的任务总数
( 2 ) getCompletedTaskCount() :已完成的任务数量
( 3 ) getPoolSize() :线程池当前的线程数量
( 4 ) getCorePoolSize() :线程池核心线程数
( 5 ) getActiveCount(): 当前线程池中正在执行任务的线程数量 深度解析线程池中重要的顶层接口和抽象类
前面我们从整体上介绍了 Java 的线程池。如果细细品味线程池的底层源码实现,你会发现整个线程池体系的设计是非常优雅的!这些代码的设计值得我们去细细品味和研究,从中学习优雅代码的设计规范,形成自己的设计思想,为我所用!哈哈,说多了,接下来,我们就来看看线程池中那些非常重要的接口和抽象类,深度分析下线程池中是如何将抽象这一思想运用的淋漓尽致的!
通过对线程池中接口和抽象类的分析,你会发现,整个线程池设计的是如此的优雅和强大,从线程池的代码设计中,我们学到的不只是代码而已!!
接口和抽象类总览
说起线程池中提供的重要的接口和抽象类,基本上就是如下图所示的接口和类
接口与类的简单说明:
Executor 接口:这个接口也是整个线程池中最顶层的接口,提供了一个无返回值的提交任务的方法。
ExecutorService 接口:派生自 Executor 接口,扩展了很过功能,例如关闭线程池,提交任务并返回结果数据、唤醒线程池中的任务等。
AbstractExecutorService 抽象类:派生自 ExecutorService 接口,实现了几个非常实现的方法,供子类进行调用。
ScheduledExecutorService 定时任务接口,派生自 ExecutorService 接口,拥有 ExecutorService 接口定义的全部方法,并扩展了定时任务相关的方法。
AbstractExecutorService 抽象类
AbstractExecutorService 类是一个抽象类,派生自 ExecutorService 接口,在其基础上实现了几个比较实用的方法,提供给子类进行调用。我们还是来看下AbstractExecutorService 类的源码。
从源码角度分析创建线程池究竟有哪些方
前言
在 Java 的高并发领域,线程池一直是一个绕不开的话题。有些童鞋一直在使用线程池,但是,对于如何创建线程池仅仅停留在使用Executors工具类的方式,那么,创建线程池究竟存在哪几种方式呢?就让我们一起从创建线程池的源码来深入分析究竟有哪些方式可以创建线程池。
使用 Executors 工具类创建线程池
在创建线程池时,初学者用的最多的就是 Executors 这个工具类,而使用这个工具类创建线程池时非常简单的,不需要关注太多的线程池细节,只需要传入必要的参数即可。Executors 工具类提供了几种创建线程池的方法,如下所示。
Executors.newCachedThreadPool :创建一个可缓存的线程池,如果线程池的大小超过了需要,可以灵活回收空闲线程,如果没有可回收线程,则新建线程
Executors.newFixedThreadPool :创建一个定长的线程池,可以控制线程的最大并发数,超出的线程会在队列中等待
Executors.newScheduledThreadPool :创建一个定长的线程池,支持定时、周期性的任务执行
Executors.newSingleThreadExecutor: 创建一个单线程化的线程池,使用一个唯一的工作线程执行任务,保证所有任务按照指定顺序(先入先出或者优先级)执行
Executors.newSingleThreadScheduledExecutor: 创建一个单线程化的线程池,支持定时、周期性的任务执行
Executors.newWorkStealingPool :创建一个具有并行级别的 work-stealing 线程池其中,Executors.newWorkStealingPool 方法是 Java 8 中新增的创建线程池的方法,它能够为线程池设置并行级别,具有更高的并发度和性能。除了此方法外,其他创建线程池的方法本质上调用的是ThreadPoolExecutor 类的构造方法。
例如,我们可以使用如下代码创建线程池。
Executors . newWorkStealingPool ();
Executors . newCachedThreadPool ();
Executors . newScheduledThreadPool ( 3 );
使用 ThreadPoolExecutor 类创建线程池
从代码结构上看 ThreadPoolExecutor 类继承自 AbstractExecutorService ,也就是说, ThreadPoolExecutor 类具有 AbstractExecutorService类的全部功能。
既然 Executors 工具类中创建线程池大部分调用的都是 ThreadPoolExecutor 类的构造方法,所以,我们也可以直接调用 ThreadPoolExecutor类的构造方法来创建线程池,而不再使用 Executors 工具类。
通过源码深度解析 ThreadPoolExecutor 类是如何保证线程池正确运行的
问题:
对于线程池的核心类 ThreadPoolExecutor 来说,有哪些重要的属性和内部类为线程池的正确运行提供重要的保障呢?
ThreadPoolExecutor 类中的重要属性
在 ThreadPoolExecutor 类中,存在几个非常重要的属性和方法,接下来,我们就介绍下这些重要的属性和方法。
ctl 相关的属性
AtomicInteger 类型的常量 ctl 是贯穿线程池整个生命周期的重要属性,它是一个原子类对象,主要用来保存线程的数量和线程池的状
态,我们看下与这个属性相关的代码如下所示。
}
public ScheduledThreadPoolExecutor ( int corePoolSize , ThreadFactory threadFactory ,
RejectedExecutionHandler handler ) {
super ( corePoolSize , Integer . MAX_VALUE , 0 , NANOSECONDS ,
new DelayedWorkQueue (), threadFactory , handler );
}
new ScheduledThreadPoolExecutor ( 3 )
// 主要用来保存线程数量和线程池的状态,高 3 位保存线程状态,低 29 位保存线程数量
private final AtomicInteger ctl = new AtomicInteger ( ctlOf ( RUNNING , 0 ));
// 线程池中线程的数量的位数( 32-3 )
private static final int COUNT_BITS = Integer . SIZE - 3 ;
// 表示线程池中的最大线程数量
// 将数字 1 的二进制值向右移 29 位,再减去 1
private static final int CAPACITY = ( 1 << COUNT_BITS ) - 1 ;
// 线程池的运行状态
private static final int RUNNING = - 1 << COUNT_BITS ;
private static final int SHUTDOWN = 0 << COUNT_BITS ;
private static final int STOP = 1 << COUNT_BITS ;
private static final int TIDYING = 2 << COUNT_BITS ;
private static final int TERMINATED = 3 << COUNT_BITS ;
// 获取线程状态
private static int runStateOf ( int c ) { return c & ~CAPACITY ; }
// 获取线程数量
private static int workerCountOf ( int c ) { return c & CAPACITY ; }
private static int ctlOf ( int rs , int wc ) { return rs | wc ; }
private static boolean runStateLessThan ( int c , int s ) {
return c < s ;
}
private static boolean runStateAtLeast ( int c , int s ) {
return c >= s ;
}
private static boolean isRunning ( int c ) {
return c < SHUTDOWN ;
}
private boolean compareAndIncrementWorkerCount ( int expect ) {
return ctl . compareAndSet ( expect , expect + 1 );
}
private boolean compareAndDecrementWorkerCount ( int expect ) {
return ctl . compareAndSet ( expect , expect - 1 );
}
private void decrementWorkerCount () {
do {} while ( ! compareAndDecrementWorkerCount ( ctl . get ()));
}
对于线程池的各状态说明如下所示。
RUNNING: 运行状态,能接收新提交的任务,并且也能处理阻塞队列中的任务
SHUTDOWN: 关闭状态,不能再接收新提交的任务,但是可以处理阻塞队列中已经保存的任务,当线程池处于 RUNNING 状态时,调用shutdown() 方法会使线程池进入该状态
STOP: 不能接收新任务,也不能处理阻塞队列中已经保存的任务,会中断正在处理任务的线程,如果线程池处于 RUNNING 或 SHUTDOWN状态,调用 shutdownNow() 方法,会使线程池进入该状态
TIDYING: 如果所有的任务都已经终止,有效线程数为 0 (阻塞队列为空,线程池中的工作线程数量为 0 ),线程池就会进入该状态。
TERMINATED: 处于 TIDYING 状态的线程池调用 terminated () 方法,会使用线程池进入该状态
也可以按照 ThreadPoolExecutor 类的注释,将线程池的各状态之间的转化总结成如下图所示。
其他重要属性
除了 ctl 相关的属性外, ThreadPoolExecutor 类中其他一些重要的属性如下所示。
// 用于存放任务的阻塞队列
private final BlockingQueue < Runnable > workQueue ;
// 可重入锁
private final ReentrantLock mainLock = new ReentrantLock ();
// 存放线程池中线程的集合,访问这个集合时,必须获得 mainLock 锁
private final HashSet < Worker > workers = new HashSet < Worker > ();
// 在锁内部阻塞等待条件完成
private final Condition termination = mainLock . newCondition ();
// 线程工厂,以此来创建新线程
private volatile ThreadFactory threadFactory ;
// 拒绝策略
private volatile RejectedExecutionHandler handler ;
// 默认的拒绝策略
private static final RejectedExecutionHandler defaultHandler = new AbortPolicy ();
ThreadPoolExecutor 类中的重要内部类
在 ThreadPoolExecutor 类中存在对于线程池的执行至关重要的内部类, Worker 内部类和拒绝策略内部类。接下来,我们分别看这些内部类。
Worker 内部类
Worker类从源代码上来看,实现了 Runnable 接口,说明其本质上是一个用来执行任务的线程,接下来,我们看下 Worker 类的源代
码,如下所示。
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
private static final long serialVersionUID = 6138294804551838833L ;
// 真正执行任务的线程
final Thread thread ;
// 第一个 Runnable 任务,如果在创建线程时指定了需要执行的第一个任务
// 则第一个任务会存放在此变量中,此变量也可以为 null
// 如果为 null ,则线程启动后,通过 getTask 方法到 BlockingQueue 队列中获取任务
Runnable firstTask ;
// 用于存放此线程完全的任务数,注意:使用了 volatile 关键字
volatile long completedTasks ;
//Worker 类唯一的构造放大,传递的 firstTask 可以为 null
Worker ( Runnable firstTask ) {
// 防止在调用 runWorker 之前被中断
setState ( - 1 );
this . firstTask = firstTask ;
// 使用 ThreadFactory 来创建一个新的执行任务的线程
this . thread = getThreadFactory (). newThread ( this );
}
// 调用外部 ThreadPoolExecutor 类的 runWorker 方法执行任务
public void run () {
runWorker ( this );
}
// 是否获取到锁
//state=0 表示锁未被获取
//state=1 表示锁被获取
protected boolean isHeldExclusively () {
return getState () != 0 ;
}
protected boolean tryAcquire ( int unused ) {
if ( compareAndSetState ( 0 , 1 )) {
setExclusiveOwnerThread ( Thread . currentThread ());
return true ;
}
return false ;
}
protected boolean tryRelease ( int unused ) {
setExclusiveOwnerThread ( null );
setState ( 0 );
return true ;
}
public void lock () { acquire ( 1 ); }
public boolean tryLock () { return tryAcquire ( 1 ); }
public void unlock () { release ( 1 ); }
public boolean isLocked () { return isHeldExclusively (); }
void interruptIfStarted () {
Thread t ;
if ( getState () >= 0 && ( t = thread ) != null && ! t . isInterrupted ()) {
try {
t . interrupt ();
} catch ( SecurityException ignore ) {
}
}
}
} Worker 类实现了 Runnable 接口,需要重写 run 方法,而 Worker 的 run 方法本质上调用的是 ThreadPoolExecutor 类的 runWorker 方
法,在 runWorker 方法中,会首先调用 unlock 方法,该方法会将 state 置为 0 ,所以这个时候调用 shutDownNow 方法就会中断当前线
程,而这个时候已经进入了 runWork 方法,就不会在还没有执行 runWorker 方法的时候就中断线程。
注意:大家需要重点理解 Worker 类的实现
拒绝策略内部类
在线程池中,如果 workQueue 阻塞队列满了,并且没有空闲的线程池,此时,继续提交任务,需要采取一种策略来处理这个任务。
而线程池总共提供了四种策略,如下所示。
直接抛出异常,这也是默认的策略。实现类为 AbortPolicy 。
用调用者所在的线程来执行任务。实现类为 CallerRunsPolicy 。
丢弃队列中最靠前的任务并执行当前任务。实现类为 DiscardOldestPolicy 。
直接丢弃当前任务。实现类为 DiscardPolicy 。
在 ThreadPoolExecutor 类中提供了 4 个内部类来默认实现对应的策略,如下所示。
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy () { }
public void rejectedExecution ( Runnable r , ThreadPoolExecutor e ) {
if ( ! e . isShutdown ()) {
r . run ();
}
}
}
public static class AbortPolicy implements RejectedExecutionHandler {
public AbortPolicy () { }
public void rejectedExecution ( Runnable r , ThreadPoolExecutor e ) {
throw new RejectedExecutionException ( "Task " + r . toString () + " rejected from " +
e . toString ());
}
}
public static class DiscardPolicy implements RejectedExecutionHandler {
public DiscardPolicy () { }
public void rejectedExecution ( Runnable r , ThreadPoolExecutor e ) {
}
}
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy () { }
public void rejectedExecution ( Runnable r , ThreadPoolExecutor e ) {
if ( ! e . isShutdown ()) {
e . getQueue (). poll ();
e . execute ( r );
}
}
}
我们也可以通过实现 RejectedExecutionHandler 接口,并重写 RejectedExecutionHandler 接口的 rejectedExecution 方法来自定义拒
绝策略,在创建线程池时,调用 ThreadPoolExecutor 的构造方法,传入我们自己写的拒绝策略。
例如,自定义的拒绝策略如下所示。 public class CustomPolicy implements RejectedExecutionHandler {
public CustomPolicy () { }
public void rejectedExecution ( Runnable r , ThreadPoolExecutor e ) {
if ( ! e . isShutdown ()) {
System . out . println ( " 使用调用者所在的线程来执行任务 " )
r . run ();
}
}
}
使用自定义拒绝策略创建线程池。
new ThreadPoolExecutor ( 0 , Integer . MAX_VALUE ,
60L , TimeUnit . SECONDS ,
new SynchronousQueue < Runnable > (),
Executors . defaultThreadFactory (),
new CustomPolicy ());
通过 ThreadPoolExecutor 类的源码深度解析线程池执行任务的核心流程
核心逻辑概述
ThreadPoolExecutor 是 Java 线程池中最核心的类之一,它能够保证线程池按照正常的业务逻辑执行任务,并通过原子方式更新线程
池每个阶段的状态。
ThreadPoolExecutor 类中存在一个 workers 工作线程集合,用户可以向线程池中添加需要执行的任务, workers 集合中的工作线程可
以直接执行任务,或者从任务队列中获取任务后执行。 ThreadPoolExecutor 类中提供了整个线程池从创建到执行任务,再到消亡的
整个流程方法。本文,就结合 ThreadPoolExecutor 类的源码深度分析线程池执行任务的整体流程。
在 ThreadPoolExecutor 类中,线程池的逻辑主要体现在 execute(Runnable) 方法, addWorker(Runnable, boolean) 方法,
addWorkerFailed(Worker) 方法和拒绝策略上,接下来,我们就深入分析这几个核心方法。
execute(Runnable) 方法
execute(Runnable) 方法的作用是提交 Runnable 类型的任务到线程池中。我们先看下 execute(Runnable) 方法的源码,如下所示。
public void execute ( Runnable command ) {
// 如果提交的任务为空,则抛出空指针异常
if ( command == null )
throw new NullPointerException ();
// 获取线程池的状态和线程池中线程的数量
int c = ctl . get ();
// 线程池中的线程数量小于 corePoolSize 的值
if ( workerCountOf ( c ) < corePoolSize ) {
// 重新开启线程执行任务
if ( addWorker ( command , true ))
return ;
c = ctl . get ();
}
// 如果线程池处于 RUNNING 状态,则将任务添加到阻塞队列中
if ( isRunning ( c ) && workQueue . offer ( command )) {
// 再次获取线程池的状态和线程池中线程的数量,用于二次检查
int recheck = ctl . get ();
// 如果线程池没有未处于 RUNNING 状态,从队列中删除任务
if ( ! isRunning ( recheck ) && remove ( command ))
// 执行拒绝策略
reject ( command );
// 如果线程池为空,则向线程池中添加一个线程
else if ( workerCountOf ( recheck ) == 0 )
addWorker ( null , false );
}
// 任务队列已满,则新增 worker 线程,如果新增线程失败,则执行拒绝策略
else if ( ! addWorker ( command , false ))
reject ( command ); }
整个任务的执行流程,我们可以简化成下图所示。
接下来,我们拆解 execute(Runnable) 方法,具体分析 execute(Runnable) 方法的执行逻辑。
( 1 )线程池中的线程数是否小于 corePoolSize 核心线程数,如果小于 corePoolSize 核心线程数,则向 workers 工作线程集合中添加
一个核心线程执行任务。代码如下所示。
// 线程池中的线程数量小于 corePoolSize 的值
if ( workerCountOf ( c ) < corePoolSize ) {
// 重新开启线程执行任务
if ( addWorker ( command , true ))
return ;
c = ctl . get ();
}
( 2 )如果线程池中的线程数量大于 corePoolSize 核心线程数,则判断当前线程池是否处于 RUNNING 状态,如果处于 RUNNING 状态,则添加任务到待执行的任务队列中。注意:这里向任务队列添加任务时,需要判断线程池是否处于RUNNING 状态,只有线程池
处于 RUNNING 状态时,才能向任务队列添加新任务。否则,会执行拒绝策略。代码如下所示。
if ( isRunning ( c ) && workQueue . offer ( command ))
( 3 )向任务队列中添加任务成功,由于其他线程可能会修改线程池的状态,所以这里需要对线程池进行二次检查,如果当前线程池的状态不再是RUNNING 状态,则需要将添加的任务从任务队列中移除,执行后续的拒绝策略。如果当前线程池仍然处于 RUNNING 状态,则判断线程池是否为空,如果线程池中不存在任何线程,则新建一个线程添加到线程池中,如下所示。
// 再次获取线程池的状态和线程池中线程的数量,用于二次检查
int recheck = ctl . get ();
// 如果线程池没有未处于 RUNNING 状态,从队列中删除任务
if ( ! isRunning ( recheck ) && remove ( command ))
// 执行拒绝策略
reject ( command );
// 如果线程池为空,则向线程池中添加一个线程
else if ( workerCountOf ( recheck ) == 0 )
addWorker ( null , false ); ( 4 )如果在步骤( 3 )中向任务队列中添加任务失败,则尝试开启新的线程执行任务。此时,如果线程池中的线程数量已经大于线
程池中的最大线程数 maximumPoolSize ,则不能再启动新线程。此时,表示线程池中的任务队列已满,并且线程池中的线程已满,
需要执行拒绝策略,代码如下所示。
// 任务队列已满,则新增 worker 线程,如果新增线程失败,则执行拒绝策略
else if ( ! addWorker ( command , false ))
reject ( command );
这里,我们将 execute(Runnable) 方法拆解,结合流程图来理解线程池中任务的执行流程就比较简单了。可以这么说, execute(Runnable)方法的逻辑基本上就是一般线程池的执行逻辑,理解了 execute(Runnable) 方法,就基本理解了线程池的执行逻辑。
通过源码深度分析线程池中 Worker 线程的执行流程
Worker 类分析
Worker 类从类的结构上来看,继承了 AQS ( AbstractQueuedSynchronizer 类)并实现了 Runnable 接口。本质上, Worker 类既是一
个同步组件,也是一个执行任务的线程。接下来,我们看下 Worker 类的源码,如下所示。
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
private static final long serialVersionUID = 6138294804551838833L ;
// 执行任务的线程类
final Thread thread ;
// 初始化执行的任务,第一次执行的任务
Runnable firstTask ;
// 完成任务的计数
volatile long completedTasks ;
//Worker 类的构造方法,初始化任务并调用线程工厂创建执行任务的线程
Worker ( Runnable firstTask ) {
setState ( - 1 );
this . firstTask = firstTask ;
this . thread = getThreadFactory (). newThread ( this );
}
// 重写 Runnable 接口的 run() 方法
public void run () {
// 调用 ThreadPoolExecutor 类的 runWorker(Worker) 方法
runWorker ( this );
}
// 检测是否是否获取到锁
//state=0 表示未获取到锁
//state=1 表示已获取到锁
protected boolean isHeldExclusively () {
return getState () != 0 ;
}
// 使用 AQS 设置线程状态
protected boolean tryAcquire ( int unused ) {
if ( compareAndSetState ( 0 , 1 )) {
setExclusiveOwnerThread ( Thread . currentThread ());
return true ;
} 在 Worker 类的构造方法中,可以看出,首先将同步状态 state 设置为 -1 ,设置为 -1 是为了防止 runWorker 方法运行之前被中断。这是
因为如果其他线程调用线程池的 shutdownNow() 方法时,如果 Worker 类中的 state 状态的值大于 0 ,则会中断线程,如果 state 状态的
值为 -1 ,则不会中断线程。
Worker 类实现了 Runnable 接口,需要重写 run 方法,而 Worker 的 run 方法本质上调用的是 ThreadPoolExecutor 类的 runWorker 方
法,在 runWorker 方法中,会首先调用 unlock 方法,该方法会将 state 置为 0 ,所以这个时候调用 shutDownNow 方法就会中断当前线
程,而这个时候已经进入了 runWork 方法,就不会在还没有执行 runWorker 方法的时候就中断线程。
注意:大家需要重点理解 Worker 类的实现。
Worker 类中调用了 ThreadPoolExecutor 类的 runWorker(Worker) 方法。接下来,我们一起看下 ThreadPoolExecutor 类的
runWorker(Worker) 方法的实现。
runWorker(Worker) 方法
首先,我们看下 RunWorker(Worker) 方法的源码,如下所示。
return false ;
}
// 尝试释放锁
protected boolean tryRelease ( int unused ) {
setExclusiveOwnerThread ( null );
setState ( 0 );
return true ;
}
public void lock () { acquire ( 1 ); }
public boolean tryLock () { return tryAcquire ( 1 ); }
public void unlock () { release ( 1 ); }
public boolean isLocked () { return isHeldExclusively (); }
void interruptIfStarted () {
Thread t ;
if ( getState () >= 0 && ( t = thread ) != null && ! t . isInterrupted ()) {
try {
t . interrupt ();
} catch ( SecurityException ignore ) {
}
}
}
}
final void runWorker ( Worker w ) {
Thread wt = Thread . currentThread ();
Runnable task = w . firstTask ;
w . firstTask = null ;
// 释放锁,将 state 设置为 0, 允许中断任务的执行
w . unlock ();
boolean completedAbruptly = true ;
try {
// 如果任务不为空,或者从任务队列中获取的任务不为空,则执行 while 循环
while ( task != null || ( task = getTask ()) != null ) {
// 如果任务不为空,则获取 Worker 工作线程的独占锁
w . lock ();
// 如果线程已经停止,或者中断线程后线程终止并且没有成功中断线程
// 大家好好理解下这个逻辑
if (( runStateAtLeast ( ctl . get (), STOP ) ||
( Thread . interrupted () &&
runStateAtLeast ( ctl . get (), STOP ))) &&
! wt . isInterrupted ())
// 中断线程
wt . interrupt ();
try {
// 执行任务前执行的逻辑
beforeExecute ( wt , task );
Throwable thrown = null ;
try {
// 调用 Runable 接口的 run 方法执行任务
task . run ();
} catch ( RuntimeException x ) { thrown = x ; throw x ;
} catch ( Error x ) {
thrown = x ; throw x ;
} catch ( Throwable x ) {
thrown = x ; throw new Error ( x );
} finally {
// 执行任务后执行的逻辑
afterExecute ( task , thrown );
}
} finally {
// 任务执行完成后,将其设置为空
task = null ;
// 完成的任务数量加 1
w . completedTasks ++ ;
// 释放工作线程获得的锁
w . unlock ();
}
}
completedAbruptly = false ;
} finally {
// 执行退出 Worker 线程的逻辑
processWorkerExit ( w , completedAbruptly );
}
}
这里,我们拆解 runWorker(Worker) 方法。
( 1 )获取当前线程的句柄和工作线程中的任务,并将工作线程中的任务设置为空,执行 unlock 方法释放锁,将 state 状态设置为 0 ,
此时可以中断工作线程,代码如下所示。
Thread wt = Thread . currentThread ();
Runnable task = w . firstTask ;
w . firstTask = null ;
// 释放锁,将 state 设置为 0, 允许中断任务的执行
w . unlock ();
( 2 )在 while 循环中进行判断,如果任务不为空,或者从任务队列中获取的任务不为空,则执行 while 循环,否则,调用
processWorkerExit(Worker, boolean) 方法退出 Worker 工作线程。
while ( task != null || ( task = getTask ()) != null )
( 3 )如果满足 while 的循环条件,首先获取工作线程内部的独占锁,并执行一系列的逻辑判断来检测是否需要中断当前线程的执
行,代码如下所示。
// 如果任务不为空,则获取 Worker 工作线程的独占锁
w . lock ();
// 如果线程已经停止,或者中断线程后线程终止并且没有成功中断线程
// 大家好好理解下这个逻辑
if (( runStateAtLeast ( ctl . get (), STOP ) ||
( Thread . interrupted () &&
runStateAtLeast ( ctl . get (), STOP ))) &&
! wt . isInterrupted ())
// 中断线程
wt . interrupt ();
( 4 )调用执行任务前执行的逻辑,如下所示
// 执行任务前执行的逻辑
beforeExecute ( wt , task );
( 5 )调用 Runable 接口的 run 方法执行任务
// 调用 Runable 接口的 run 方法执行任务
task . run ();
( 6 )调用执行任务后执行的逻辑 // 执行任务后执行的逻辑
afterExecute ( task , thrown );
( 7 )将完成的任务设置为空,完成的任务数量加 1 并释放工作线程的锁。
// 任务执行完成后,将其设置为空
task = null ;
// 完成的任务数量加 1
w . completedTasks ++ ;
// 释放工作线程获得的锁
w . unlock ();
( 8 )退出 Worker 线程的执行,如下所示
// 执行退出 Worker 线程的逻辑
processWorkerExit ( w , completedAbruptly );
从代码分析上可以看到,当从 Worker 线程中获取的任务为空时,会调用 getTask() 方法从任务队列中获取任务,接下来,我们看下
getTask() 方法的实现。
getTask() 方法
我们先来看下 getTask() 方法的源代码,如下所示。
private Runnable getTask () {
// 轮询是否超时的标识
boolean timedOut = false ;
// 自旋 for 循环
for (;;) {
// 获取 ctl
int c = ctl . get ();
// 获取线程池的状态
int rs = runStateOf ( c );
// 检测任务队列是否在线程池停止或关闭的时候为空
// 也就是说任务队列是否在线程池未正常运行时为空
if ( rs >= SHUTDOWN && ( rs >= STOP || workQueue . isEmpty ())) {
// 减少 Worker 线程的数量
decrementWorkerCount ();
return null ;
}
// 获取线程池中线程的数量
int wc = workerCountOf ( c );
// 检测当前线程池中的线程数量是否大于 corePoolSize 的值或者是否正在等待执行任务
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize ;
// 如果线程池中的线程数量大于 corePoolSize
// 获取大于 corePoolSize 或者是否正在等待执行任务并且轮询超时
// 并且当前线程池中的线程数量大于 1 或者任务队列为空
if (( wc > maximumPoolSize || ( timed && timedOut ))
&& ( wc > 1 || workQueue . isEmpty ())) {
// 成功减少线程池中的工作线程数量
if ( compareAndDecrementWorkerCount ( c ))
return null ;
continue ;
}
try {
// 从任务队列中获取任务
Runnable r = timed ?
workQueue . poll ( keepAliveTime , TimeUnit . NANOSECONDS ) :
workQueue . take ();
// 任务不为空直接返回任务
if ( r != null )
return r ;
timedOut = true ;
} catch ( InterruptedException retry ) { timedOut = false ;
}
}
}
getTask() 方法的逻辑比较简单,大家看源码就可以了,我这里就不重复描述了。
接下来,我们看下在正式调用 Runnable 的 run() 方法前后,执行的 beforeExecute 方法和 afterExecute 方法。
beforeExecute(Thread, Runnable) 方法
beforeExecute(Thread, Runnable) 方法的源代码如下所示。
protected void beforeExecute ( Thread t , Runnable r ) { }
可以看到, beforeExecute(Thread, Runnable) 方法的方法体为空,我们可以创建 ThreadPoolExecutor 的子类来重写
beforeExecute(Thread, Runnable) 方法,使得线程池正式执行任务之前,执行我们自己定义的业务逻辑。
afterExecute(Runnable, Throwable) 方法
afterExecute(Runnable, Throwable) 方法的源代码如下所示。
protected void afterExecute ( Runnable r , Throwable t ) { }
可以看到, afterExecute(Runnable, Throwable) 方法的方法体同样为空,我们可以创建 ThreadPoolExecutor 的子类来重写
afterExecute(Runnable, Throwable) 方法,使得线程池在执行任务之后执行我们自己定义的业务逻辑。
接下来,就是退出工作线程的 processWorkerExit(Worker, boolean) 方法。
processWorkerExit(Worker, boolean) 方法
processWorkerExit(Worker, boolean) 方法的逻辑主要是执行退出 Worker 线程,并且对一些资源进行清理,源代码如下所示。
private void processWorkerExit ( Worker w , boolean completedAbruptly ) {
// 执行过程中出现了异常,突然中断
if ( completedAbruptly )
// 将工作线程的数量减 1
decrementWorkerCount ();
// 获取全局锁
final ReentrantLock mainLock = this . mainLock ;
mainLock . lock ();
try {
// 累加完成的任务数量
completedTaskCount += w . completedTasks ;
// 将完成的任务从 workers 集合中移除
workers . remove ( w );
} finally {
// 释放锁
mainLock . unlock ();
}
// 尝试终止工作线程的执行
tryTerminate ();
// 获取 ctl
int c = ctl . get ();
// 判断当前线程池的状态是否小于 STOP ( RUNNING 或者 SHUTDOWN )
if ( runStateLessThan ( c , STOP )) {
// 如果没有突然中断完成
if ( ! completedAbruptly ) {
// 如果 allowCoreThreadTimeOut 为 true ,为 min 赋值为 0 ,否则赋值为 corePoolSize
int min = allowCoreThreadTimeOut ? 0 : corePoolSize ;
// 如果 min 为 0 并且工作队列不为空
if ( min == 0 && ! workQueue . isEmpty ())
//min 的值设置为 1
min = 1 ;
// 如果线程池中的线程数量大于 min 的值
if ( workerCountOf ( c ) >= min )
// 返回,不再执行程序
return ;
} // 调用 addWorker 方法
addWorker ( null , false );
}
}
接下来,我们拆解 processWorkerExit(Worker, boolean) 方法。
( 1 )执行过程中出现了异常,突然中断执行,则将工作线程数量减 1 ,如下所示。
// 执行过程中出现了异常,突然中断
if ( completedAbruptly )
// 将工作线程的数量减 1
decrementWorkerCount ();
( 2 )获取锁累加完成的任务数量,并将完成的任务从 workers 集合中移除,并释放,如下所示。
// 获取全局锁
final ReentrantLock mainLock = this . mainLock ;
mainLock . lock ();
try {
// 累加完成的任务数量
completedTaskCount += w . completedTasks ;
// 将完成的任务从 workers 集合中移除
workers . remove ( w );
} finally {
// 释放锁
mainLock . unlock ();
}
( 3 )尝试终止工作线程的执行
// 尝试终止工作线程的执行
tryTerminate ();
( 4 )处判断当前线程池中的线程个数是否小于核心线程数,如果是,需要新增一个线程保证有足够的线程可以执行任务队列中的任
务或者提交的任务。
// 获取 ctl
int c = ctl . get ();
// 判断当前线程池的状态是否小于 STOP ( RUNNING 或者 SHUTDOWN )
if ( runStateLessThan ( c , STOP )) {
// 如果没有突然中断完成
if ( ! completedAbruptly ) {
// 如果 allowCoreThreadTimeOut 为 true ,为 min 赋值为 0 ,否则赋值为 corePoolSize
int min = allowCoreThreadTimeOut ? 0 : corePoolSize ;
// 如果 min 为 0 并且工作队列不为空
if ( min == 0 && ! workQueue . isEmpty ())
//min 的值设置为 1
min = 1 ;
// 如果线程池中的线程数量大于 min 的值
if ( workerCountOf ( c ) >= min )
// 返回,不再执行程序
return ;
}
// 调用 addWorker 方法
addWorker ( null , false );
}
接下来,我们看下 tryTerminate() 方法。
tryTerminate() 方法
tryTerminate() 方法的源代码如下所示。
final void tryTerminate () {
// 自旋 for 循环
for (;;) {
// 获取 ctl int c = ctl . get ();
// 如果线程池的状态为 RUNNING
// 或者状态大于 TIDYING
// 或者状态为 SHUTDOWN 并且任务队列为空
// 直接返回程序,不再执行后续逻辑
if ( isRunning ( c ) ||
runStateAtLeast ( c , TIDYING ) ||
( runStateOf ( c ) == SHUTDOWN && ! workQueue . isEmpty ()))
return ;
// 如果当前线程池中的线程数量不等于 0
if ( workerCountOf ( c ) != 0 ) {
// 中断线程的执行
interruptIdleWorkers ( ONLY_ONE );
return ;
}
// 获取线程池的全局锁
final ReentrantLock mainLock = this . mainLock ;
mainLock . lock ();
try {
// 通过 CAS 将线程池的状态设置为 TIDYING
if ( ctl . compareAndSet ( c , ctlOf ( TIDYING , 0 ))) {
try {
// 调用 terminated() 方法
terminated ();
} finally {
// 将线程池状态设置为 TERMINATED
ctl . set ( ctlOf ( TERMINATED , 0 ));
// 唤醒所有因为调用线程池的 awaitTermination 方法而被阻塞的线程
termination . signalAll ();
}
return ;
}
} finally {
// 释放锁
mainLock . unlock ();
}
}
}
( 1 )获取 ctl ,根据情况设置线程池状态或者中断线程的执行,并返回。
// 获取 ctl
int c = ctl . get ();
// 如果线程池的状态为 RUNNING
// 或者状态大于 TIDYING
// 或者状态为 SHUTDOWN 并且任务队列为空
// 直接返回程序,不再执行后续逻辑
if ( isRunning ( c ) ||
runStateAtLeast ( c , TIDYING ) ||
( runStateOf ( c ) == SHUTDOWN && ! workQueue . isEmpty ()))
return ;
// 如果当前线程池中的线程数量不等于 0
if ( workerCountOf ( c ) != 0 ) {
// 中断线程的执行
interruptIdleWorkers ( ONLY_ONE );
return ;
}
( 2 )获取全局锁,通过 CAS 设置线程池的状态,调用 terminated() 方法执行逻辑,最终将线程池的状态设置为 TERMINATED ,唤醒
所有因为调用线程池的 awaitTermination 方法而被阻塞的线程,最终释放锁,如下所示。
// 获取线程池的全局
final ReentrantLock mainLock = this . mainLock ;
mainLock . lock ();
try {
// 通过 CAS 将线程池的状态设置为 TIDYING
if ( ctl . compareAndSet ( c , ctlOf ( TIDYING , 0 ))) {
try {
// 调用 terminated() 方法 terminated ();
} finally {
// 将线程池状态设置为 TERMINATED
ctl . set ( ctlOf ( TERMINATED , 0 ));
// 唤醒所有因为调用线程池的 awaitTermination 方法而被阻塞的线程
termination . signalAll ();
}
return ;
}
} finally {
// 释放锁
mainLock . unlock ();
}
接下来,看下 terminated() 方法。
terminated() 方法
terminated() 方法的源代码如下所示。
protected void terminated () { }
可以看到, terminated() 方法的方法体为空,我们可以创建 ThreadPoolExecutor 的子类来重写 terminated() 方法,值得 Worker 线程
调用 tryTerminate() 方法时执行我们自己定义的 terminated() 方法的业务逻辑。
shutdown() 方法
当使用线程池的时候,调用了 shutdown() 方法后,线程池就不会再接受新的执行任务了。但是在调用 shutdown() 方法之前放入任务
队列中的任务还是要执行的。此方法是非阻塞方法,调用后会立即返回,并不会等待任务队列中的任务全部执行完毕后再返回。我
们看下 shutdown() 方法的源代码,如下所示。
public void shutdown () {
// 获取线程池的全局锁
final ReentrantLock mainLock = this . mainLock ;
mainLock . lock ();
try {
// 检查是否有关闭线程池的权限
checkShutdownAccess ();
// 将当前线程池的状态设置为 SHUTDOWN
advanceRunState ( SHUTDOWN );
// 中断 Worker 线程
interruptIdleWorkers ();
// 为 ScheduledThreadPoolExecutor 调用钩子函数
onShutdown (); // hook for
} finally {
// 释放线程池的全局锁
mainLock . unlock ();
}
// 尝试将状态变为 TERMINATED
tryTerminate ();
}
总体来说, shutdown() 方法的代码比较简单,首先检查了是否有权限来关闭线程池,如果有权限,则再次检测是否有中断工作线程
的权限,如果没有权限,则会抛出 SecurityException 异常,代码如下所示。
// 检查是否有关闭线程池的权限
checkShutdownAccess ();
// 将当前线程池的状态设置为 SHUTDOWN
advanceRunState ( SHUTDOWN );
// 中断 Worker 线程
interruptIdleWorkers (); 其中, checkShutdownAccess() 方法的实现代码如下所示。
private void checkShutdownAccess () {
SecurityManager security = System . getSecurityManager ();
if ( security != null ) {
security . checkPermission ( shutdownPerm );
final ReentrantLock mainLock = this . mainLock ;
mainLock . lock ();
try {
for ( Worker w : workers )
security . checkAccess ( w . thread );
} finally {
mainLock . unlock ();
}
}
}
对于 checkShutdownAccess() 方法的代码理解起来比较简单,就是检测是否具有关闭线程池的权限,期间使用了线程池的全局锁。
接下来,我们看 advanceRunState(int) 方法的源代码,如下所示。
private void advanceRunState ( int targetState ) {
for (;;) {
int c = ctl . get ();
if ( runStateAtLeast ( c , targetState ) ||
ctl . compareAndSet ( c , ctlOf ( targetState , workerCountOf ( c ))))
break ;
}
}
advanceRunState(int) 方法的整体逻辑就是:判断当前线程池的状态是否为指定的状态,在 shutdown() 方法中传递的状态是
SHUTDOWN ,如果是 SHUTDOWN ,则直接返回;如果不是 SHUTDOWN ,则将当前线程池的状态设置为 SHUTDOWN 。
接下来,我们看看 showdown() 方法调用的 interruptIdleWorkers() 方法,如下所示。
private void interruptIdleWorkers () {
interruptIdleWorkers ( false );
}
可以看到, interruptIdleWorkers() 方法调用的是 interruptIdleWorkers(boolean) 方法,继续看 interruptIdleWorkers(boolean) 方法
的源代码,如下所示。
private void interruptIdleWorkers ( boolean onlyOne ) {
final ReentrantLock mainLock = this . mainLock ;
mainLock . lock ();
try {
for ( Worker w : workers ) {
Thread t = w . thread ;
if ( ! t . isInterrupted () && w . tryLock ()) {
try {
t . interrupt ();
} catch ( SecurityException ignore ) {
} finally {
w . unlock ();
}
}
if ( onlyOne )
break ;
}
} finally {
mainLock . unlock ();
}
}
上述代码的总体逻辑为:获取线程池的全局锁,循环所有的工作线程,检测线程是否被中断,如果没有被中断,并且 Worker 线程获
得了锁,则执行线程的中断方法,并释放线程获取到的锁。此时如果 onlyOne 参数为 true ,则退出循环。否则,循环所有的工作线
程,执行相同的操作。最终,释放线程池的全局锁。
接下来,我们看下 shutdownNow() 方法。 shutdownNow() 方法
如果调用了线程池的 shutdownNow() 方法,则线程池不会再接受新的执行任务,也会将任务队列中存在的任务丢弃,正在执行的
Worker 线程也会被立即中断,同时,方法会立刻返回,此方法存在一个返回值,也就是当前任务队列中被丢弃的任务列表。
shutdownNow() 方法的源代码如下所示。
public List < Runnable > shutdownNow () {
List < Runnable > tasks ;
final ReentrantLock mainLock = this . mainLock ;
mainLock . lock ();
try {
// 检查是否有关闭权限
checkShutdownAccess ();
// 设置线程池的状态为 STOP
advanceRunState ( STOP );
// 中断所有的 Worker 线程
interruptWorkers ();
// 将任务队列中的任务移动到 tasks 集合中
tasks = drainQueue ();
} finally {
mainLock . unlock ();
}
/ 尝试将状态变为 TERMINATED
tryTerminate ();
// 返回 tasks 集合
return tasks ;
}
shutdownNow() 方法的源代码的总体逻辑与 shutdown() 方法基本相同,只是 shutdownNow() 方法将线程池的状态设置为 STOP ,中
断所有的 Worker 线程,并且将任务队列中的所有任务移动到 tasks 集合中并返回。
可以看到, shutdownNow() 方法中断所有的线程时,调用了 interruptWorkers() 方法,接下来,我们就看下 interruptWorkers() 方法
的源代码,如下所示。
private void interruptWorkers () {
final ReentrantLock mainLock = this . mainLock ;
mainLock . lock ();
try {
for ( Worker w : workers )
w . interruptIfStarted ();
} finally {
mainLock . unlock ();
}
}
interruptWorkers() 方法的逻辑比较简单,就是获得线程池的全局锁,循环所有的工作线程,依次中断线程,最后释放线程池的全局
锁。
在 interruptWorkers() 方法的内部,实际上调用的是 Worker 类的 interruptIfStarted() 方法来中断线程,我们看下 Worker 类的
interruptIfStarted() 方法的源代码,如下所示。
void interruptIfStarted () {
Thread t ;
if ( getState () >= 0 && ( t = thread ) != null && ! t . isInterrupted ()) {
try {
t . interrupt ();
} catch ( SecurityException ignore ) {
}
}
}
发现其本质上调用的还是 Thread 类的 interrupt() 方法来中断线程。
awaitTermination(long, TimeUnit) 方法
当线程池调用了 awaitTermination(long, TimeUnit) 方法后,会阻塞调用者所在的线程,直到线程池的状态修改为 TERMINATED 才返
回,或者达到了超时时间返回。接下来,我们看下 awaitTermination(long, TimeUnit) 方法的源代码,如下所示。 public boolean awaitTermination ( long timeout , TimeUnit unit )
throws InterruptedException {
// 获取距离超时时间剩余的时长
long nanos = unit . toNanos ( timeout );
// 获取 Worker 线程的的全局锁
final ReentrantLock mainLock = this . mainLock ;
// 加锁
mainLock . lock ();
try {
for (;;) {
// 当前线程池状态为 TERMINATED 状态,会返回 true
if ( runStateAtLeast ( ctl . get (), TERMINATED ))
return true ;
// 达到超时时间,已超时,则返回 false
if ( nanos <= 0 )
return false ;
// 重置距离超时时间的剩余时长
nanos = termination . awaitNanos ( nanos );
}
} finally {
// 释放锁
mainLock . unlock ();
}
}
上述代码的总体逻辑为:首先获取 Worker 线程的独占锁,后在循环判断当前线程池是否已经是 TERMINATED 状态,如果是则直接返
回 true ,否则检测是否已经超时,如果已经超时,则返回 false 。如果未超时,则重置距离超时时间的剩余时长。接下来,进入下一
轮循环,再次检测当前线程池是否已经是 TERMINATED 状态,如果是则直接返回 true ,否则检测是否已经超时,如果已经超时,则
返回 false 。如果未超时,则重置距离超时时间的剩余时长。以此循环,直到线程池的状态变为 TERMINATED 或者已经超时。
深入理解 ScheduledThreadPoolExecutor 与 Timer 的区别和简单示例
JDK 1.5 开始提供 ScheduledThreadPoolExecutor 类, ScheduledThreadPoolExecutor 类继承 ThreadPoolExecutor 类重用线程池实
现了任务的周期性调度功能。在 JDK 1.5 之前,实现任务的周期性调度主要使用的是 Timer 类和 TimerTask 类。本文,就简单介绍下
ScheduledThreadPoolExecutor 类与 Timer 类的区别, ScheduledThreadPoolExecutor 类相比于 Timer 类来说,究竟有哪些优势,
以及二者分别实现任务调度的简单示例。
二者的区别
线程角度
Timer 是单线程模式,如果某个 TimerTask 任务的执行时间比较久,会影响到其他任务的调度执行。
ScheduledThreadPoolExecutor 是多线程模式,并且重用线程池,某个 ScheduledFutureTask 任务执行的时间比较久,不会影响到其他任务的调度执行。
系统时间敏感度
Timer 调度是基于操作系统的绝对时间的,对操作系统的时间敏感,一旦操作系统的时间改变,则 Timer 的调度不再精确。
ScheduledThreadPoolExecutor 调度是基于相对时间的,不受操作系统时间改变的影响。
是否捕获异常
Timer 不会捕获 TimerTask 抛出的异常,加上 Timer 又是单线程的。一旦某个调度任务出现异常,则整个线程就会终止,其他需要调度的任务也不再执行。
ScheduledThreadPoolExecutor 基于线程池来实现调度功能,某个任务抛出异常后,其他任务仍能正常执行。
任务是否具备优先级
Timer 中执行的 TimerTask 任务整体上没有优先级的概念,只是按照系统的绝对时间来执行任务。
ScheduledThreadPoolExecutor 中执行的 ScheduledFutureTask 类实现了 java.lang.Comparable 接口和 java.util.concurrent.Delayed接口,这也就说明了 ScheduledFutureTask 类中实现了两个非常重要的方法,一个是 java.lang.Comparable接口的 compareTo 方法,一个是 java.util.concurrent.Delayed 接口的 getDelay 方法。在ScheduledFutureTask类中 compareTo 方法方法实现了任务的比较,距离下次执行的时间间隔短的任务会排在前面,也就是说,距离下次执行的时间间隔短的任务的优先级比较高。而getDelay 方法则能够返回距离下次任务执行的时间间隔。
是否支持对任务排序 Timer 不支持对任务的排序。
ScheduledThreadPoolExecutor 类中定义了一个静态内部类 DelayedWorkQueue , DelayedWorkQueue 类本质上是一个有序队列,为需要调度的每个任务按照距离下次执行时间间隔的大小来排序
能否获取返回的结果
Timer 中执行的 TimerTask 类只是实现了 java.lang.Runnable 接口,无法从 TimerTask 中获取返回的结果。
ScheduledThreadPoolExecutor 中执行的 ScheduledFutureTask 类继承了 FutureTask 类,能够通过 Future 来获取返回的结果。
通过以上对 ScheduledThreadPoolExecutor 类和 Timer 类的分析对比,相信在 JDK 1.5 之后,就没有使用 Timer 来实现定时任务调度
的必要了。
scheduleAtFixedRate 方法
scheduleWithFixedDelay 方法
Thread 类的源码剖析
Thread 类定义
Thread 在 java.lang 包下, Thread 类的定义如下所示。
public class Thread implements Runnable {
加载本地资源
打开 Thread 类后,首先,我们会看到在 Thread 类的最开始部分,定义了一个静态本地方法 registerNatives() ,这个方法主要用来注
册一些本地系统的资源。并在静态代码块中调用这个本地方法,如下所示。
// 定义 registerNatives() 本地方法注册系统资源
private static native void registerNatives ();
static {
// 在静态代码块中调用注册本地系统资源的方法
registerNatives ();
}
Thread 中的成员变量
Thread 类中的成员变量如下所示。
// 当前线程的名称
private volatile String name ;
// 线程的优先级
private int priority ;
private Thread threadQ ;
private long eetop ;
// 当前线程是否是单步线程
private boolean single_step ;
// 当前线程是否在后台运行
private boolean daemon = false ;
//Java 虚拟机的状态
private boolean stillborn = false ;
// 真正在线程中执行的任务
private Runnable target ;
// 当前线程所在的线程组
private ThreadGroup group ;
// 当前线程的类加载器
private ClassLoader contextClassLoader ;
// 访问控制上下文
private AccessControlContext inheritedAccessControlContext ;
// 为匿名线程生成名称的编号
private static int threadInitNumber ;
// 与此线程相关的 ThreadLocal, 这个 Map 维护的是 ThreadLocal 类
ThreadLocal . ThreadLocalMap threadLocals = null ;
// 与此线程相关的 ThreadLocal
ThreadLocal . ThreadLocalMap inheritableThreadLocals = null ;
// 当前线程请求的堆栈大小,如果未指定堆栈大小,则会交给 JVM 来处理
private long stackSize ;
// 线程终止后存在的 JVM 私有状态
private long nativeParkEventPointer ;
// 线程的 id
private long tid ;
// 用于生成线程 id
private static long threadSeqNumber ;
// 当前线程的状态,初始化为 0 ,代表当前线程还未启动
private volatile int threadStatus = 0 ;
// 由(私有) java.util.concurrent.locks.LockSupport.setBlocker 设置
// 使用 java.util.concurrent.locks.LockSupport.getBlocker 访问
volatile Object parkBlocker ;
//Interruptible 接口中定义了 interrupt 方法,用来中断指定的线程
private volatile Interruptible blocker ;
// 当前线程的内部锁
private final Object blockerLock = new Object (); // 线程拥有的最小优先级
public final static int MIN_PRIORITY = 1 ;
// 线程拥有的默认优先级
public final static int NORM_PRIORITY = 5 ;
// 线程拥有的最大优先级
public final static int MAX_PRIORITY = 10 ;
从 Thread 类的成员变量,我们可以看出, Thread 类本质上不是一个任务,它是一个实实在在的线程对象,在 Thread 类中拥有一个
Runnable 类型的成员变量 target ,而这个 target 成员变量就是需要在 Thread 线程对象中执行的任务。
线程的状态定义
在 Thread 类的内部,定义了一个枚举 State ,如下所示。
public enum State {
// 初始化状态
NEW ,
// 可运行状态,此时的可运行包括运行中的状态和就绪状态
RUNNABLE ,
// 线程阻塞状态
BLOCKED ,
// 等待状态
WAITING ,
// 超时等待状态
TIMED_WAITING ,
// 线程终止状态
TERMINATED ;
}
如何设计一个支撑高并发大流量的系统?
高并发架构相关概念
什么是并发?
并发是指并发的访问,也就是某个时间点,有多少个访问同时到来;
通常如果一个系统的日 PV 在千万以上,有可能是一个高并发的系统,这里需要注意的是:只是有可能是一个高并发的系统,不一定
是一个高并发的系统。
并发数和 QPS 是不同的概念,一般说 QPS 会说多少并发用户下 QPS ,当 QPS 相同时,并发用户数越大,网站并发处理能力越好。当并
发用户数过大时,会造成进程(线程)频繁切换,反正真正用于处理请求的时间变少,每秒能够处理的请求数反而变少,同时用户
的请求等待时间也会变大。 找到最佳线程数能够让 web 系统更稳定,效率更高。
并发数 = QPS* 平均响应时间
高并发具体关心什么?
QPS : 每秒请求或查询的数量,在互联网领域,指每秒响应请求数;
吞吐量: 单位时间内处理的请求量(通常由 QPS 与并发数决定);
响应时间: 从请求发出到收到响应花费的时间,例如一个系统处理一个 HTTP 请求需要 100ms ,这个 100ms 就是系统的响应时间;
PV : 综合浏览量,即页面浏览量或者点击量,一个访客在 24 小时内访问的页面数量;
UV : 独立访客 ,即一定时间范围内相同访客多次访问网站,只计算为一个独立的访客;
带宽: 计算带宽大小需要关注两个指标, 峰值流量和页面的平均大小 ;
日网站带宽可以使用下面的公式来粗略计算:
日网站带宽 = pv/ 统计时间(换算到秒) * 平均页面大小(单位 kB ) *8
峰值一般是平均值的倍数;
QPS 不等于并发连接数, QPS 是每秒 HTTP 请求数量,并发连接数是系统同时处理的请求数量;
峰值每秒请求数( QPS ) = ( 总 PV 数 * 80 %) / ( 6 小时秒数 * 20 % )
压力测试: 测试能承受的最大并发,测试最大承受的 QPS 值。
测试工具( ab ) : 目标是 URL ,可以创建多个访问线程对同一个 URL 进行访问( Nginx );
ab 的使用: 模拟并发请求 100 次( 100 个人),总共请求 5000 次(每个人请求 5000 次)
ab -c 100 -n 5000 待测试网站(内存和网络不超过最高限度的 75% )
QPS 达到 50 : 一般的服务器就可以应付; QPS 达到 100 : 假设关系型数据库的每次请求在 0.01 秒完成(理想),假设单页面只有一个 SQL 查询,那么 100QPS 意味着 1 秒中完
成 100 次请求,但此时我们不能保证数据库查询能完成 100 次;
方案:数据库缓存层、数据库的负载均衡;
QPS 达到 800 : 假设我们使用 百兆宽带,意味着网站出口的实际带宽是 8M 左右,假设每个页面是有 10k ,在这个并发的条件下,百
兆带宽已经被吃完;
方案: CDN 加速、负载均衡
QPS 达到 1000 : 假设使用 Redis 缓存数据库查询数据,每个页面对 Redis 请求远大于直接对 DB 的请求;
Redis 的悲观并发数在 5W 左右,但有可能之前内网带宽已经被吃光,表现出不稳定;
方案:静态 HTML 缓存
QPS 达到 2000 : 文件系统访问锁都成为了灾难;
方案:做业务分离,分布式存储;
高并发解决方案案例
流量优化: 防盗链处理(把一些恶意的请求拒之门外)
前端优化: 减少 HTTP 请求、添加异步请求、启用浏览器的缓存和文件压缩、 CDN 加速、建立独立的图片服务器;
服务端优化: 页面静态化处理、并发处理、队列处理;
数据库优化: 数据库的缓存、分库分表、分区操作、读写分离、负载均衡
Web 服务器优化: 负载均衡
高并发下的经验公式
通过 QPS 和 PV 计算部署服务器的台数
单台服务器每天 PV 计算
公式 1 :每天总 PV = QPS * 3600 * 6
公式 2 :每天总 PV = QPS * 3600 * 8
服务器计算
服务器数量 = ceil( 每天总 PV / 单台服务器每天总 PV )
峰值 QPS 和机器计算公式
原理: 每天 80% 的访问集中在 20% 的时间里,这 20% 时间叫做峰值时间
公式: ( 总 PV 数 * 80% ) / ( 每天秒数 * 20% ) = 峰值时间每秒请求数 (QPS)
机器: 峰值时间每秒 QPS / 单台机器的 QPS = 需要的机器。
高并发环境下构建缓存服务需要注意哪些问题?我和阿里 P9 聊了很久!
写在前面
周末,跟阿里的一个朋友(去年晋升为 P9 了)聊了很久,聊的内容几乎全是技术,当然了,两个技术男聊得最多的话题当然就
是技术了。从基础到架构,从算法到 AI ,无所不谈。中间又穿插着不少天马行空的想象,虽然现在看起来不太实际,但是随着
技术的进步,相信五年、十年之后都会实现的。
不知道是谁提起了在高并发环境下如何构建缓存服务,结果一路停不下来了!!
缓存特征
( 1 )命中率:命中数 /( 命中数 + 没有命中数 )
( 2 )最大元素(空间):代表缓存中可以存放的最大元素的数量,一旦缓存中元素的数量超过这个值,或者缓存数据所占的空间超
过了最大支持的空间,将会触发缓存清空策略。根据不同的场景,合理设置最大元素(空间)的值,在一定程度上可以提高缓存的
命中率,从而更有效的使用缓存。
( 3 )清空策略: FINO (先进先出)、 LFU (最少使用)、 LRU (最近最少使用)、过期时间、随机等。
FINO (先进先出):最先进入缓存的数据,在缓存空间不够或超出最大元素限制的情况下,会优先被清除掉,以腾出新的空间
来接收新的数据。这种策略的算法主要是比较缓存元素的创建时间,在数据实时性较高的场景下,可以选择这种策略,优先保
证最新策略可用。
LFU (最少使用):无论元素是否过期,根据元素的被使用次数来判断,清除使用次数最少的元素来释放空间。算法主要是比
较元素的命中次数,在保证高频数据有效的场景下,可以选择这种策略。
LRU (最近最少使用):无论元素是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素,释放空间。算
法主要是比较元素最近一次被获取的时间,在热点数据场景下,可以选择这种策略。
过期时间:根据过期时间判断,清理过期时间最长的元素,或者清理最近要过期的元素。
缓存命中率影响因素 ( 1 )业务场景和业务需求
缓存往往适合读多写少的场景。业务需求对实时性的要求,直接会影响到缓存的过期时间和更新策略。实时性要求越低,就越适合
缓存。在相同 Key 和相同请求数的情况下,缓存的时间越长,命中率就会越高。
( 2 )缓存的设计(粒度和策略)
通常情况下,缓存的粒度越小,命中率越高。缓存的更新和命中策略也会影响缓存的命中率,当数据发生变化时,直接更新缓存的
值会比移除缓存或使缓存过期的命中率更高。
( 3 )缓存容量和基础设施
缓存的容量有限,则容易引起缓存失效和被淘汰(目前多数的缓存框架或中间件都采用了 LRU 算法)。同时,缓存的技术选型也是至
关重要的,比如采用应用内置的本地缓存就比较容易出现单机瓶颈,而采用分布式缓存则毕竟容易扩展。所以需要做好系统容量规
划,并考虑是否可扩展。此外,不同的缓存框架或中间件,其效率和稳定性也是存在差异的。
( 4 )其他因素
当缓存节点发生故障时,需要避免缓存失效并最大程度降低影响,这种特殊情况也是架构师需要考虑的。业内比较典型的做法就是
通过一致性 Hash 算法,或者通过节点冗余的方式。
有些朋友可能会有这样的理解误区:既然业务需求对数据时效性要求很高,而缓存时间又会影响到缓存命中率,那么系统就别使用
缓存了。其实这忽略了一个重要因素 -- 并发。通常来讲,在相同缓存时间和 key 的情况下,并发越高,缓存的收益会越高,即便缓存
时间很短。
提高缓存命中率的方法
从架构师的角度,需要应用尽可能的通过缓存直接获取数据,并避免缓存失效。这也是比较考验架构师能力的,需要在业务需求,
缓存粒度,缓存策略,技术选型等各个方面去通盘考虑并做权衡。尽可能的聚焦在高频访问且时效性要求不高的热点业务上,通过
缓存预加载(预热)、增加存储容量、调整缓存粒度、更新缓存等手段来提高命中率。
对于时效性很高(或缓存空间有限),内容跨度很大(或访问很随机),并且访问量不高的应用来说缓存命中率可能长期很低,可
能预热后的缓存还没来得被访问就已经过期了。
缓存的分类和应用场景
( 1 )本地缓存:编程实现(成员变量、局部变量、静态变量)、 Guava Cache
( 2 )分布式缓存: Memcached 、 Redis
高并发场景下缓存常见问题
( 1 )缓存的一致性
更新数据库成功 —— 更新缓存失败
更新缓存成功 —— 更新数据库失败
更新数据库成功 —— 淘汰缓存失败
淘汰缓存成功 —— 更新数据库失败
( 2 )缓存并发
并发时请求缓存时已过期或者没有命中或者更新的情况下有大量的请求访问数据库。
解决办法 :在缓存更新或者过期的情况下 , 先尝试获取到 lock, 当更新完成后 , 尝试释放锁 , 其他的请求只需要牺牲一定的等待时间
( 3 )缓存穿透
在高并发的场景下 , 如果某一个 key 被高并发的访问没有被命中 , 出于对容错性的考虑会尝试从后端的数据库获取,从而导致大量的请
求访问了数据库 , 主要是当 key 对应的数据为空或者为 null 的情况下,这就导致数据库中并发的执行了很多不必要的查询操作。从而导
致了巨大的冲击和压力。
解决方法 :
缓存空对象:对查询结果为空的对象也进行缓存,如果是集合可以缓存一个空的集合,而不是 null, 如果是单个对象可以通过字段标
识来区分 , 需要保证缓存数据的时效性 ( 实现相对简单 ) ,适合命中不高但可能会频繁更新的数据。
单独过滤处理:对所有可能对应数据为空的 key 进行统一的存放 , 并在请求前做拦截 ( 实现相对复杂 ) ,适合命中不高更新不频繁的数据
( 4 )缓存颠簸问题
缓存的颠簸问题,有些地方可能被称为 “ 缓存抖动 ” ,可以看作是一种比 “ 雪崩 ” 更轻微的故障,但是也会在一段时间内对系统造成冲击
和性能影响。一般是由于缓存节点故障导致。业内推荐的做法是通过一致性 Hash 算法来解决。
( 5 )缓存雪崩现象
缓存雪崩就是指由于缓存的原因,导致大量请求到达后端数据库,从而导致数据库崩溃,整个系统崩溃,发生灾难。导致这种现象
的原因有很多种,上面提到的 “ 缓存并发 ” , “ 缓存穿透 ” , “ 缓存颠簸 ” 等问题,其实都可能会导致缓存雪崩现象发生。这些问题也可能
会被恶意攻击者所利用。还有一种情况,例如某个时间点内,系统预加载的缓存周期性集中失效了,也可能会导致雪崩。为了避免
这种周期性失效,可以通过设置不同的过期时间,来错开缓存过期,从而避免缓存集中失效。
从应用架构角度,我们可以通过限流、降级、熔断等手段来降低影响,也可以通过多级缓存来避免这种灾难。
此外,从整个研发体系流程的角度,应该加强压力测试,尽量模拟真实场景,尽早的暴露问题从而防范。 ( 6 )缓存无底洞现象
该问题由 facebook 的工作人员提出的, facebook 在 2010 年左右, memcached 节点就已经达 3000 个,缓存数千 G 内容。他们
发现了一个问题 ---memcached 连接频率,效率下降了,于是加 memcached 节点,添加了后,发现因为连接频率导致的问题,仍 然存在,并没有好转,称之为” 无底洞现象 ”