程序员究竟要不要读源码?
很多人觉得读源码比较枯燥,确实,读源码是要比看那些表面教你如何使用的文章要枯燥的多,也比不上刷抖音和微博来的轻松愉 快。但是,读源码是一名程序员突破自我瓶颈,获得高薪和升职加薪的一个有效途径。通过阅读优秀的开源框架的源码,我们能够领略到框架作者设计框架的思维和思路,从中学习优秀的架构设计和代码设计。这些都是在那些只告诉你如何使用的文章中所学不
到的,就更别提是刷抖音和微博了。当你只停留在业务层面的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 (;;) {
//获取ctlint 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 节点,添加了后,发现因为连接频率导致的问题,仍 然存在,并没有好转,称之为”无底洞现象”





本文深入剖析了Java线程池的实现原理,包括ThreadPoolExecutor类的构造方法、核心线程数、最大线程数、任务队列、线程池状态、线程执行流程等。通过分析源码,展示了如何创建线程池、如何确保线程安全、以及线程池的生命周期管理。同时,讨论了如何通过设置线程池参数来优化系统资源使用,避免资源浪费。此外,文章还介绍了SimpleDateFormat类在高并发下的线程安全问题,以及如何解决。最后,分析了线程池的shutdown()和shutdownNow()方法,以及如何应对高并发下的缓存服务问题,如缓存穿透、雪崩现象等。
1万+

被折叠的 条评论
为什么被折叠?



