阅读本文需要了解的概念
原语
所谓原语,一般是指由若干条指令组成的程序段,用来实现某个特定功能,在执行过程中不可被中断。在操作系统中,某些被进程调用的操作,如队列操作、对信号量的操作、检查启动外设操作等,一旦开始执行,就不能被中断,否则就会出现操作错误,造成系统混乱。所以,这些操作都要用原语来实现 原语是操作系统核心(不是由进程,而是由一组程序模块组成)的一个组成部分,并且常驻内存,通常在管态下执行。原语一旦开始执行,就要连续执行完,不允许中断。
对操作系统来说,原语就是不可中断的一堆汇编指令。
对java程序来说,原语就是java native底层接口去调用的东西,java应用层面是不需要关心原语的。
串行
串行表示所有任务都一一按先后顺序进行。串行意味着必须先装完一车柴才能运送这车柴,只有运送到了,才能卸下这车柴,并且只有完成了这整个三个步骤,才能进行下一个步骤。串行是一次只能取得一个任务,并执行这个任务。
并行
并行意味着可以同时取得多个任务,并同时去执行所取得的这些任务。并行模式相当于将长长的一条队列,划分成了多条短队列,所以并行缩短了任务队列的长度。并行的效率从代码层次上强依赖于多进程多线程代码,从硬件角度上则依赖于多核 CPU。
并发
并发(concurrent)指的是多个程序可以同时运行的现象,更细化的是多进程可以同时运行或者多指令可以同时运行。但这不是重点,在描述并发的时候也不会去扣这种字眼是否精确,并发的重点在于它是一种现象, 并发描述的是多进程同时运行的现象。但实际上,对于单核心 CPU 来说,同一时刻能运行一个线程。所以,这里的"同时运行"表示的不是真的同一时刻有多个线程运行的现象,这是并行的概念,而是提供一种功能让用户看来多个程序同时运行起来了,但实际上这些程序中的进程不是一直霸占 CPU 的,而是执行一会停一会。要解决大并发问题,通常是将大任务分解成多个小任务, 由于操作系统对进程的调度是随机的,所以切分成多个小任务后,可能会从任一小任务处执行。这可能会出现一些现象:
• 可能出现一个小任务执行了多次,还没开始下个任务的情况。这时一般会采用队列或类似的数据结构来存放各个小任务的成果
• 可能出现还没准备好第一步就执行第二步的可能。这时,一般采用多路复用或异步的方式,比如只有准备好产生了事件通知才执行某个任务。
• 可以多进程/多线程的方式并行执行这些小任务。也可以单进程/单线程执行这些小任务,这时很可能要配合多路复用才能达到较高的效率
管程
管程(monitor)是保证了同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现).但是这样并不能保证进程以设计的顺序执行。
JVM 中同步是基于进入和退出管程(monitor)对象实现的,每个对象都会有一个管程(monitor)对象,管程(monitor)会随着 java 对象一同创建和销毁。执行线程首先要持有管程对象,然后才能执行方法,当方法完成之后会释放管程,方法在执行时候会持有管程,其他线程无法再获取同一个管程。
线程概念
进程与线程
进程(Process)是操作系统中程序运行和资源分配的基本单位,是操作系统结构的基础。 程序是指令、数据及其组织形式的描述,进程是程序的实体。在当代面向线程设计的计算机结构中,进程是线程的容器。
线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单元。一个进程中可以并发多个线程,每条线程并行执行不同的任务,一个进程至少有一个线程,多个线程共享进程的内存资源。
JVM在HotSpot的线程模型下,Java线程会一对一映射为内核线程。这意味着,在Java中每次创建以及回收线程都会去内核创建以及回收。这就有可能导致:创建和销毁线程所花费的时间和资源可能比处理的任务花费的时间和资源要更多。
守护线程
守护线程(daemon thread),是运行在后台服务于其他线程的线程,周期性地执行某种任务或等待处理某些发生的事件。
java程序启动后,至少有两个线程,一个main线程,一个GC的守护线程。
JMM(Java Memory Model)
JMM是JVM规范的一部分,JVM规范包括了JMM。
java内存模型是定义了线程和主内存之间的抽象关系,即 JMM 定义了 JVM 在计算机内存(RAM)中的工作方式,以及各个线程对各种变量的访问规则,即在虚拟机中将变量(线程共享的变量)存储到内存和从内存中取出变量这样底层细节。
每个线程创建时jvm会分配一个线程私有的工作内存(栈空间)。
所有共享变量都存储在主内存(堆空间+和方法区)。
线程操作共享变量过程是,复制一份到私有空间,在私有空间改完之后再写入主内存,不能直接操作主内存,并且线程私有空间对其他线程不可见。
JMM关于线程同步的规定
1.线程解锁前,必须把共享变量(非线程局部变量)写入主内存。
2.线程加锁前,必须读取主内存的最新值到自己的工作内存。
3.加锁解锁是同一把锁。
线程安全三要素
原子性
原子性是指一个操作不可被中断,要么全部执行成功要么全部执行失败。和关系型数据库事务的原子性概念相同。
Java原子操作是指符合原子性的操作,不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。
jvm原子操作指令
jvm中读写相关的原子操作指令:
lock :它把一个变量标识为一个线程独占的状态
unlock:它释放一个处理线程独占状态的变量,使释放后的变量能被其它线程锁定
read:它把一个变量的值从主内存中传输到线程的工作内存中,以便后面的load使用
load:将read命令传输的变量值放入工作内存中的变量副本
use:它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作
assign:它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
store:它把工作内存中一个变量的值传送给主内存中以便随后的write操作使用
write:它把store操作从工作内存中得到的变量的值放入主内存的变量中
jvm中读写相关的非原子操作指令:
iinc:自增或自减
java原子操作代码
- java中,对基本数据类型的常量赋值操作都是原子性操作。
int a = 1; //原子
int b = 2; //原子
int c = a; //非原子 (读a的值>赋值给c)
a++; //非原子 (读a的值>值+1>赋值给a)
b = b + 1; //非原子 (读b的值>值+1>赋值给b)
不过有个特例:
在32位的操作系统中,long和double不是原子操作,因为32位jdk在给64位的long变量赋值时,是两次操作,先给32位赋值,然后给剩下的32位赋值,当多个线程出现竞争的时候,会出现A线程赋值了一半(32位),B线程给另一半(32位)赋值了。
long a = 1L; //32位系统中非原子,64位系统中原子
double b = 1.0; //32位系统中非原子,64位系统中原子
- java中,所有引用的赋值操作都是原子操作(只是给引用赋值是原子操作,也就是
Object obj1=
是原子操作,new不是原子操作,但new是线程安全的):
Object obj1=new Object();//'='原子,'new'非原子
Object obj2=obj1;//原子
- java中,所有
java.util.concurrent.atomic
包下的类都是原子操作
java提供java.util.concurrent.atomic
包来支持将非原子操作转变成原子操作。
可见性
一个线程对主内存的修改可以及时的被其他线程观察到。
JMM中每一个线程都有自己的独立工作内存区,还有一个公共内存区(主内存)。线程执行时先把变量值从主内存拷贝到自己工作内存区中。可见性的目的就是保证一个线程改变了变量后,其他线程及时刷新自己的工作区内存获取最新值。
java中我们可以使用volatile、synchronized、Lock提供可见性的。当一个变量被volatile修饰时,对它的修改会立刻刷新到主存,并将其他线程缓存中的此变量清空,当其它线程需要读取该变量时,会去主内存中读取新值。而普通变量则不能保证每次都去主内存读取到最新值,会有延迟。
synchronized和Lock也能够保证可见性,线程在释放锁之前,会把变量值都刷回主存,且在持锁后首先从主内存同步所有工作内存变量,但是synchronized和Lock的开销都比volatile大。
有序性
被执行的代码必须按编码时的顺序逻辑去执行,避免指令重排。
java中我们可以通过lock/unlock, volatile等关键字防止指令重排序。
JMM是允许编译器和处理器对指令重排序的,但是规定了as-if-serial语义,即不管怎么重排序,程序的执行结果不能改变。比如下面的程序段:
int a = 1; //编译器在编译.java源文件为.class文件时,无法保证int a = 1在第一行,因为编译器会优化重排代码顺序。
int b = 2; //编译器在编译.java源文件为.class文件时,无法保证int b = 2在第二行,因为编译器会优化重排代码顺序。
int c = a + b; //编译器不管怎么优化,int c = a + b一定是第三行,JMM保证了重排序不会影响到单线程的执行结果,所以c无法参与重排,必须排在a、b后面。
但是多线程环境中线程交替执行,即使只是将a和b进行重排,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测,进而造成线程安全问题。
指令重排
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,一般分以下3种
源代码=>编译器优化的重排=>指令并行的重排=>内存系统的重排=>最终执行的指令
volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。
先了解一个概念,内存屏障( Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:
一是保证特定操作的执行顺序,
二是保证某些变量的内存可见性(利用该特性实现 volatile的内存可见性)。
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier
则会告诉编译器和CPU,不管什么指令都不能
和这条Memory Barrier
指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作
用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
对 Volatile变量进行写操作时,会在写操作后加入一条store屏障指令,将工作内存中的共享变量值刷新回到主内存。
对 Volatile变量进行读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。
New操作的线程安全问题的思考
new操作符不可能有两个线程能同时进入,多个线程进入会new出不同的对象,但构造方法里面的代码是不能保证线程安全的,例如在构造函数中将this交给别的线程,那么就会出现线程安全问题。
只要不在构造函数或构造代码块将this交出去,那么在new操作符完成之前,别的线程无论如何也是无法获取这个新对象的,必须等到new操作符操作结束,别的对象才能获得该对象。
总之,多个线程不可能new出同一个对象,既然不是同一个对象,就不存在线程安全问题。
线程状态
看这里:Java之线程状态
创建线程的方式
1 继承Thread类重写run方法。
2 实现Runnable接口实现run方法,并将Runnable接口实现类的实例作为Thread的构造参数。
3 实现Callable接口实现run方法,并将Callable接口实现实现类的实例作为和FutureTask的构造参数创建FutureTask对象,然后将FutureTask对象作为Thread的构造参数。这种方式可以通过FutureTask.get
获取执行的返回值,获取返回值时会阻塞。
Callable<String> call = new Callable<String>() {@Overridepublic String call() throws Exception {System.out.println("Running...");return "done";}};FutureTask<String> futureTask=new FutureTask<String>(call);Thread thread = new Thread(futureTask);thread.start();//futureTask.get():获取任务执行结果,如果任务还没完成则会阻塞等待直到任务执行完成。如果任务被取消则会抛出CancellationException异常,//如果任务执行过程发生异常则会抛出ExecutionException异常,如果阻塞等待过程中被中断则会抛出InterruptedException异常。boolean result = (boolean) futureTask.get();String result = futureTask.get();System.out.println(result);
4 使用线程池。
为什么要用多线程
使用多线程最主要的原因是提高CPU利用率,更快的完成任务。
锁概念
公平锁
排好队,不允许插队。
在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一
个,就占有锁,否则就会加入到等待队列中,队列按照FIFO(先进先出)的规则依次发放锁给线程。
new ReentrantLock(true)
就是一个公平锁
公平锁性能会比非公平锁低。
非公平锁
排队,但也允许插队,谁的权重高谁先拿到锁。
在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一
个,就占有锁,否则就会加入到等待队列中,加入队列时,权重高的排在最前面,如果排在后面的主动提升了权重(氪金了),则也可以重新排到前面去。
synchronized
和new ReentrantLock(默认就是非公平锁)
和new ReentrantLock(false)
都是非公平锁
非公平锁可能会导致饥饿问题和权重反转。
饥饿问题
一直有权重高的在插队,权重低的一直得不到执行。
自旋锁
自旋锁(spinlock),自旋其实就是死循环、递归的意思。是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
java.util.concurrent.atomic
包下的原子类很多都是使用的自旋锁,而不是Lock或synchronized。
读写锁
读锁,又称共享锁;写锁,又称排他锁。
将锁根据情况拆分成读锁和写锁两把锁,就成了读写锁。
读写锁允许多个线程同时读一个资源,不允许多个线程同时写一个资源。
读-读能共存,读-写不能共存,写写不能共存。
jdk自带的读写锁:ReentrantReadWriteLock
。
使用场景:
对共享资源有读和写的操作,且写操作没有读操作那么频繁。
在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源。
但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。
可重入锁
可重入锁又名递归锁
在一个线程内,当锁对象是同一个对象时,不用等锁释放即可再次获得锁,不会因为之前已经获取过还没释放而阻塞。
Java中 ReentrantLock和 synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
显式锁/隐式锁
显式锁:需要显式指定锁对象、调用获取锁和释放锁的代码,juc内的Lock相关锁都是显示锁。
隐式锁:可以不指定锁对象、不需要显式调用获取锁和释放锁的代码。synchronized就是隐式锁。
死锁
死锁是指两个或两个以上的线程互相持有对方所需要的资源,导致这些线程处于互相等待状态,都无法继续执行。
死锁不单单发生于多线程,多进程、软件系统都可能发生此种情况。
死锁产生的条件:
1.条件互斥:进程/线程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程/线程所占用。
2.请求和保持:当进程/线程因请求资源而阻塞时,对已获得的资源保持不放。
3.不剥夺:进程/线程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
4.环路等待:在发生死锁时,必然存在一个进程/线程——资源的环形链。
死锁一般产生的原因
系统资源不足
程序设计不当
防止死锁
1、加锁顺序(线程按照一定的顺序加锁)
当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。
如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。
2、加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
ReentrantLock类tryLock(long time, TimeUnit unit)方法可以尝试获取锁,如果超过指定时间仍未获取到锁,则放弃获取锁。
锁升级
在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。
线程池
线程池的实现原理
线程池使用阻塞队列,详情请查看本文后面的阻塞队列章节。
线程池的作用
- 控制线程数量
控制指定数量的线程运行,多余线程排队。 - 线程复用
线程是稀缺资源,大量的任务每次都new Thread是对资源的严重浪费,线程池可以重复利用已创建的线程,降低线程创建和销毁造成的资源消耗。 - 方便管理拓展
线程丢到一起可以方便统计和管理。
线程池提供了更多的功能,比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
线程池状态
线程池有5种状态:Running、ShutDown、Stop、Tidying、Terminated。
创建线程池的方式
一、自己new ThreadPoolExecutor。
二、使用Executors提供的静态方法。
2.1. Executors.newFixedThreadPool(int nThreads)
创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。
2.2. Executors.newCachedThreadPool()
创建一个可自动扩缩容的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。
2.3. Executors.newSingleThreadExecutor()
这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行。
2.4. Executors.newScheduledThreadPool(int corePoolSize)
创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。
2.5. Executors.newWorkStealingPool
jdk1.8 提供的线程池,底层使用的是 ForkJoinPool 实现,创建一个拥有多个
任务队列的线程池,可以减少连接数,创建当前可用 cpu 核数的线程来并行执行任务
public static ExecutorService newWorkStealingPool(int parallelism) {
/**
* parallelism:并行级别,通常默认为 JVM 可用的处理器个数
* factory:用于创建 ForkJoinPool 中使用的线程。
* handler:用于处理工作线程未处理的异常,默认为 null
* asyncMode:用于控制 WorkQueue 的工作模式:队列---反队
* ...
}
线程池7大核心参数
看这里:线程池七大参数的含义
线程池工作流程
1.如果核心线程corePoolSize
还有空闲线程,那么新任务将交给空闲线程。
2.如果核心线程都满负荷工作中,那么将添加任务到阻塞队列workQueue
。
3.如果阻塞队列满了,那么将使用最大线程数maximumPoolSize
来创建更多的线程,以接收新的任务。
4.如果阻塞队列满了,最大线程数的全部线程也都在工作中,那么将启用拒绝策略rejectedExecutionHandler
。
5.如果任务执行完了,没有新的任务提交了,将使用keepAliveTime
来销毁核心数之外的线程。
线程池常用方法
execute
ThreadPoolExecutor.execute在将来的某个时候执行给定的任务。任务可以在新线程中执行,也可以在现有的池线程中执行。如果任务不能提交执行,要么因为该执行器已经关闭,要么因为它的容量已经达到,任务将由当前RejectedExecutionHandler处理。
submit
ThreadPoolExecutor.submit提交一个Runnable任务执行,并返回一个表示该任务的future。成功完成后,Future的get方法将返回null。
shutdown
ThreadPoolExecutor.shutdown启动一个有序的关闭,之前提交的任务将被执行,但不会接受新的任务。如果已经关闭,调用不会产生额外的效果。
该方法不等待之前提交的任务完成执行。使用awaitTerminationto这样做。
shutdownNow
尝试停止所有正在执行的任务,停止等待任务的处理,并返回等待执行的任务列表。从该方法返回时,这些任务将从任务队列中取出(删除)。
该方法不等待主动执行的任务终止。使用awaitterminate完成此操作。
除了尽最大努力尝试停止正在积极执行的任务的处理外,没有任何保证。这个实现通过Thread.interrupt取消任务,所以任何无法响应中断的任务都可能永远不会终止。
schedule
ScheduledExecutorService.schedule在指定延迟时间后调用任务。
schedule(Runnable command,//任务long delay, //从现在开始推迟执行的时间TimeUnit unit//delay的单位
)
线程池的submit和execute的区别
submit里面其实就是调用的execute,只不过execute没有返回值,submit将run方法的返回值通过Future返回。
怎么设置线程池核心数
需要看我们的任务是CPU密集型还是IO密集型。
CPU密集型:
CPU密集的意思是该任务需要大量的运算,即需要长时间大量的CPU运行。
CPU密集型任务配置尽可能少的线程数量:
最佳线程池核心线程数=CPU核数+1
IO密集型:
IO密集型,即该任务需要大量的IO,会造成大量的阻塞。
在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。
所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
参考公式1:CPU核数/(1-阻塞系数)
阻塞系数在0.8~0.9之间
比如8核CPU:8/1-0.9=80个线程数
参考公式2:CPU核数*2
传统并发编程
synchronized
同步代码块,代码块内的代码需要获取锁对象才能执行,用于解决多线程中脏数据问题。
synchronized只有持有锁时才对其他线程有排他性,当wait释放锁时,其他线程就可以进入synchronized代码块内的,Lock.lock和Condition.await同理。
代码示例:
Object lock=new Object();
Thread a=new Thread(){public void run(){//a线程要干的活...synchronized(lock){//需要对共享变量进行操作,防止多个线程修改形成脏数据,使用synchronized加锁...//如果a线程需要别的线程配合干的活,别的线程干完了,a线程继续干活,这时候需要使用wait(),调用try {lock.wait();//一旦线程调用了wait,则必须等待其他线程释放锁并且调用notify,否则一直处于等待状态,即使别的线程执行完毕a线程也不会自动从等待中恢复执行。} catch (InterruptedException e) {e.printStackTrace();}}}
};
a.start();
Thread b=new Thread(){public void run(){//b线程要干的活...synchronized(lock){//需要对共享变量进行操作,防止多个线程修改形成脏数据,使用synchronized加锁...//b线程需要别的线程配合干的活,b线程干完了就通知其他线程执行。lock.notify();}}
};
b.start();
synchronized锁
任何对象都可以作为synchronized锁。synchronized定义在静态方法上,锁就是类的class对象,synchronized定义在实例方法上,锁就是调用该方法的实例对象。
synchronized的重入的实现机理
使用javap编译.class文件之后可以查看到synchronized相关字节码代码,以下内容皆源于此。
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
当执行 monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
一个monitorenter为什么对应两个monitorexit?
因为后面那个是为了保证异常情况下也能释放锁。
Object多线程相关方法
wait
Object.wait使线程处于等待状态(线程状态),除非其他线程释放锁并且调用notify,否则该线程一直处于等待状态,即使别的线程执行完毕该线程也不会自动从等待中恢复执行。用于解决多线程中线程交互问题。
notify
Object.notify通知其他等待状态的线程,变成可运行状态,重新参与锁竞争。用于解决多线程中线程交互问题。
notifyAll
Object.notifyAll和notify一样,区别在于notify每次只唤醒一个线程,如果有多个线程在等待状态,则随机唤醒一个,而notifyAll将唤醒所有等待状态的线程。
Thread常用方法
sleep
Thread.sleep让出当前线程运行权,使线程处于记时等待状态,不会释放锁。
yield
Thread.yield让出当前线程运行权,不改变线程状态,不会释放锁。
sleep和wait的区别
最重要的区别:sleep不会释放锁,wait会释放锁。
sleep和yield的区别
最重要的区别:sleep会改变线程状态(变为timed waiting状态),yield不会改变线程状态(仍然是runnable)。
setPriority
thread.setPriority,设置线程权重,数值越大权重越高.在非公平锁中,可以设置权重优化线程执行效率。
setDaemon
thread.setDaemon,设置线程为守护线程
setName
thread.setName,设置线程名称
isAlive
thread.isAlive,获取线程是否还活着。
只有New和Terminated状态才会返回false,其余都返回true。
volatile
volatile保证内存的可见性和禁止指令重排序。用于解决多线程中脏数据问题。比synchronized更轻量级(说人话就是效率更高,更节省资源)。
以下程序的t1线程会死循环出不来,System.out.println("我看不到")
不会执行,因为线程t1的flag是主内存flag的拷贝,而且是while循环开始时拷贝一份之后就不再拷贝,必须等while结束后才能重新从主内存拷贝,所以t1线程里的flag永远为true。解决办法就是在flag变量上加上volatileprivate static volatile boolean flag = false;
。
private static boolean flag = false;public static void main(String[] args) {Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {while (!flag) {}System.out.println("我看不到");}});t1.start();Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("我改了");flag = true;}});t2.start();}
但是volatile只能保证可见性(说人话就是,多个地方读没问题),不能保证原子性(说人话就是,多个地方写就有问题),以下程序如果反复多次运行,会发现几乎每次打印结果都少于10000。
public static volatile int inc = 0;public static void main(String[] args) throws Exception {for (int i = 0; i < 100; i++) {new Thread() {public void run() {for (int j = 0; j < 100; j++) {inc=inc+1;}};}.start();}while (Thread.activeCount() > 1) {//如果有main线程之外的线程在运行,就一直循环,保证后面的打印在最后执行Thread.yield();}System.out.println(inc);}
因为,inc=inc+1
操作并不是原子的操作。要想解决这个问题,不能用volatile,得用java.util.concurrent.atomic
包下的原子类。下面代码可以保证每次都打印10000。
public static AtomicInteger inc = new AtomicInteger(0);public static void main(String[] args) throws Exception {for (int i = 0; i < 100; i++) {new Thread() {public void run() {for (int j = 0; j < 100; j++) {inc.incrementAndGet();}};}.start();}while (Thread.activeCount() > 1) {//如果有main线程之外的线程在运行,就一直循环,保证后面的打印在最后执行Thread.yield();}System.out.println(inc);}
当然用synchronized也能解决问题,但没必要。
虚假唤醒
虚假唤醒就是在多线程执行过程中,线程间的通信未按照我们幻想的顺序唤醒,故出现数据不一致等不符合我们预期的结果。而出现虚假唤醒的问题就是在if语句上,将if换成while就行。
wait规范
Object.wait、Condition.await、LockSupport.park都应该在循环中使用。
JDK官方文档给出代码建议是这样的:
/*** 建议的等待方法是在调用等待的while循环中检查等待的条件,如下面的示例所示。除此之外,这种方法还避免了可能由虚假唤醒引起的问题。*/
synchronized (obj) {while (<条件不成立> and <如果没有超时>) {long timeoutMillis = ... ; // 重新计算超时值int nanos = ... ;obj.wait(timeoutMillis, nanos);}... // 执行适合于条件或超时的操作
}
模拟问题
反复运行以下代码就会发现问题,明明有判断number == 0就wait
却还是会产生负数:
public class Test {public static void main(String[] args) {final Work work = new Work();new Thread("A") {public void run() {for (int i = 0; i < 10; i++) {work.producer();}}}.start();new Thread("B") {public void run() {for (int i = 0; i < 10; i++) {work.consumer();}}}.start();new Thread("C") {public void run() {for (int i = 0; i < 10; i++) {work.producer();}}}.start();new Thread("D") {public void run() {for (int i = 0; i < 10; i++) {work.consumer();}}}.start();}
}class Work{private int number = 0;private Lock lock = new ReentrantLock();private Condition condition = lock.newCondition();public void producer(){lock.lock();try {// 1判断if (number != 0) {//这里不能用if,if会产生虚假唤醒问题// 等待,不能生产condition.await();}// 2干活number++;System.out.println(Thread.currentThread().getName() + "\t" + number);// 3通知唤醒condition.signalAll();} catch (Exception e) {e.printStackTrace();} finally {lock.unlock();}}public void consumer(){lock.lock();try {// 1判断if (number == 0) {//这里不能用if,if会产生虚假唤醒问题// 等待,不能生产condition.await();}// 2干活number--;System.out.println(Thread.currentThread().getName() + "\t" + number);// 3通知唤醒condition.signalAll();} catch (Exception e) {e.printStackTrace();} finally {lock.unlock();}}
}
打印:
A 1
B 0
C 1
B 0
A 1
C 2
D 1
D 0
B -1
B -2
B -3
B -4
B -5
B -6
B -7
B -8
A -7
C -6
D -7
D -8
D -9
D -10
D -11
A -10
C -9
D -10
D -11
D -12
A -11
C -10
A -9
C -8
A -7
C -6
A -5
C -4
A -3
C -2
A -1
C 0
分析问题
我们可以来分析一下为什么会出现负数B -1
(其实C 2
也有问题,和B -1
产生的原因一样):
当D消费了一个产品,使用了signalAll函数,把其他三个线程(ABC)都唤醒了,但B是消费者调用的是consumer函数,在被D唤醒之前,B被卡在await()函数,唤醒后因为是在if语句块中,那么B不再需要再判断number是否等于0,就直接往下执行number--
的语句,因此造成了虚假唤醒的问题,解决的办法很简单,只需要把if语句换成while语句即可,让他在被唤醒之后,再次判断number的数量再决定能不能执行number–的操作。
JUC并发编程需要了解的概念
CAS
CAS是Compare and Swap的缩写,意思就是比较并交换。是用来保证原子性,支持多线程中修改共享数据的问题。归根结底还是为了解决多线程中脏数据问题。
设计思想就是:
如果线程中修改主内存时,预先估计一个值,如果这个值和主内存的值不匹配,那么就不修改,并重新从主内存读取最新值,并将这个最新值作为预先估计的值,再次和主内存的值进行比较,如果和主内存的值相同,则修改。
java的juc包中很多线程同步相关的类都使用了cas设计思想,java的cas主要依赖于Unsafe提供的native方法来实现,Unsafe只是调用了底层c++的Atomic,c++的Atomic也是依赖于汇编层面的原语CMPXCHG指令
。详情:C++ atomic详解。自旋cas实现的基本思路就是java层面循环调用c++的Atomic操作直到成功为止。
下面是java原子类的CompareAndSwap
public final int updateAndGet(IntUnaryOperator updateFunction) {int prev, next;do {prev = get();next = updateFunction.applyAsInt(prev);} while (!compareAndSet(prev, next));//循环获取值并比较return next;}public final int getAndUpdate(IntUnaryOperator updateFunction) {int prev, next;do {prev = get();next = updateFunction.applyAsInt(prev);} while (!compareAndSet(prev, next));//循环获取值并比较return prev;}public final boolean compareAndSet(int expect, int update) {return unsafe.compareAndSwapInt(this, valueOffset, expect, update);//这里就是调用下面c++的Unsafe_CompareAndSwapInt了}
下面代码是java本地方法(c++)的CompareAndSwap
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))UnsafeWrapper("Unsafe_CompareAndSwapInt");oop p= JNIHandles::resolve(obj);jint* addr =(jint * index_oop_from_field_offset_long(p, offset);return (jint)(Atomic:: cmpchg(x, addr, e))==e;//Atomic:: cmpchg,原子的调用汇编指令'CMPXCHG'
UNSAFE_END
cas属于一种乐观锁,和根据版本号修改数据库数据的乐观锁是一样的。
cas缺点:
1.只是解决了原子性,没有解决可见性,需要配合volatile一起使用。(所以java.util.concurrent.atomic
包的类里使用了volatile
)。
public class AtomicInteger extends Number implements java.io.Serializable {private static final Unsafe unsafe = Unsafe.getUnsafe();//unsafe提供原子性private static final long valueOffset;private volatile int value;//volatile提供可见性public AtomicInteger(int initialValue) {value = initialValue;}...
}
2.如果比较不一致就需要循环查询最新值,这会耗费系统资源。
3.会引发ABA问题
ABA问题
主内存有个变量值为A,有两个线程a和b同时读取了主内存的A到自己内存空间,此后a线程修改主内存为B,然后又将主内存修改为A,此时如果b线程去修改主内存,就无法发觉变量值A已经被改过了,无法意识到此A非彼A。这个情况应该是要避免的。
如果不关心过程,只关心开头A和结尾A对得上,那么ABA问题其实可以忽略。
但是有的业务,会要求一个线程操作过程不能被其他线程修改,这时候就需要解决ABA问题。
ABA问题的解决
加版本号。
jdk中提供了带版本号的原子类型AtomicStampedReference
。
JUC并发编程
juc是java.util.concurrent
包的简称,传统的synchronized并发编程性能太低,所以JDK1.5提供java.util.concurrent并发包,提供了一系列多线程并发编程相关的API和工具,用以替代传统的synchronized并发编程。
atomic
java.util.concurrent.atomic
包的相关类,提供原子操作相关的API和工具。
AtomicInteger
//以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
int addAndGet(int delta)//如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。
boolean compareAndSet(int expect, int update)//以原子方式将当前值加1,注意,这里返回的是自增之前的值。
int getAndIncrement()//最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还算可以读到旧的值。
void lazySet(int newValue)//以原子方式设置为newValue的值,并返回旧值。
int getAndSet(int newValue)
AtomicLong
原子长整型,使用同AtomicInteger。
AtomicBoolean
原子布尔值,使用同AtomicInteger。
AtomicDouble
java没有提供AtomicDouble,如果需要可以使用谷歌的guava工具库中有AtomicDouble。
<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId></dependency>
AtomicReference
原子引用类,使用和AtomicInteger差不多,但参数都是对象引用。
AtomicStampedReference
带版本号的引用原子类。原子引用类,使用和AtomicReference差不多,多了一个旧版本号参数而已(用来比较)。
其他原子类
AtomicIntegerArray,原子更新整型数组里的元素。
AtomicLongArray,原子更新长整型数组里的元素。
AtomicReferenceArray,原子更新引用类型数组里的元素。
同步与锁(synchronized)
Lock
juc包下的所有锁对象的超级接口。
最核心的方法就两个:
void lock();//加锁
void unlock();//解锁
使用示例:
public void m() {lock.lock();try {// ...} finally {lock.unlock();}}
ReentrantLock
re=再次,entrant=进入,翻译过来就是可重入锁。
是代替synchronized的实现,是比synchronized更加轻量级的锁。
示例代码:
ReentrantLock lock = new ReentrantLock();// lockpublic void m1() {lock.lock();try {// ... method body} finally {lock.unlock();}}// tryLockpublic void m2() {if (lock.tryLock()) {try {// 你的业务代码} finally {lock.unlock();}}else{// 你的业务代码}}// tryLock timepublic void m3() {try{if (lock.tryLock(1, TimeUnit.MINUTES)) {try {// 你的业务代码} finally {lock.unlock();}}else{// 你的业务代码}}catch(Exception e){//当前线程被中断(interrupted)时tryLock(time)会抛出异常。// 你的业务代码}}// tryLock和tryLock timepublic void m5() {try{//强行插入一个公平的锁if (lock.tryLock()||lock.tryLock(1, TimeUnit.MINUTES)) {try {// 你的业务代码} finally {lock.unlock();}}else{// 你的业务代码}}catch(Exception e){//当前线程被中断(interrupted)时tryLock(time)会抛出异常。// 你的业务代码}}
ReentrantLock和synchronized比较
-
原始构成不同
synchronized是关键字属于JVM层面,依靠monitorenter和monitorexit指令还完成加锁和解锁。
(monitorenter和monitorexit底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象,具体可以查看Monitor简介)
Lock是具体类是api层面的锁。 -
使用方法不同
synchronized不需要用户去手动释放锁,当 synchronized代码执行完后系统会自动让线程释放对锁的占用。
ReentrantLock则需要用户去手动释放锁,若没有主动释放锁,就有可能导致出现死锁现象。
需要lock()和unlock()方法配合try/finally语句块来完成。 -
获取锁时阻塞是否可中断
synchronized不可中断,除非抛出异常或者正常运行完成。
ReentrantLock可用两种方式中断
1.设置超时方法 tryLock( long timeout, TimeUnit unit)。
2.lockInterruptibly()放代码块中,调用interrupt()方法可中断。 -
获取锁是否公平
synchronized权重锁(非公平锁),可以通过Thread.setPriority(数值越大权重越高)
设置线程权重。
ReentrantLock两者都可以,默认不公平锁,构造方法可以传入boolean值,true为公平锁, false为非公平锁。 -
唤醒条件不同
synchronized只能调用Object的notify随机唤醒一个或者notifyAll唤醒全部。
ReentrantLock支持多条件唤醒(一个Lock可以多次调用newCondition获取不同的钥匙),可以做到精确唤醒指定线程。
在实际项目中,使用synchronized还是ReentrantLock是根据业务要求来的,如果确实一定要获取锁的情况下才能继续操作,那么可以使用synchronized,如果获取不到锁就返回提示这种业务,那就需要使用ReentrantLock。
ReentrantReadWriteLock
ReentrantReadWriteLock是jdk对读写锁的一个实现。
ReentrantReadWriteLock 实现了 ReadWriteLock 接口,ReadWriteLock 接口定义了获取读锁和写锁的规范,具体需要实现类去实现。
同时其还实现了Serializable接口,表示可以进行序列化,在源代码中可以看到ReentrantReadWriteLock 实现了自己的序列化逻辑。
使用示例:
public void read() {lock.readLock().lock();try {// 读取使用共享资源// ...} finally {lock.readLock().unlock();}}public void write() {lock.writeLock().lock();try {// 修改共享资源// ...} finally {lock.writeLock().unlock();}}
使用建议:
在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。
在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。
原因: 当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。
锁降级:
ReentrantReadWriteLock能够支持写锁降级成为读锁。
阻塞与唤醒(wait/notify)
Condition
Condition是一个接口,提供和wait/notify功能,Condition使用时需要依赖于Lock。用于解决多线程中线程交互问题。
Condition内部调用的就是LockSupport,是对LockSupport的加强。
Condition是线程等待和唤醒机制,是wait和notify的改良加强版。
await=wait,signal=notify
jdk自带的实现有:
AbstractQueuedLongSynchronizer.ConditionObject
AbstractQueuedSynchronizer.ConditionObject
CountDownLatch
表面意思为倒数的门栓,实际是一种特殊的阻塞工具类,和Condition有点像,但不需要和Lock配合也可单独使用。
CountDownLatch允许一个或者多个线程去等待其他线程完成操作。
CountDownLatch内部维护一个倒数的计数器,初始值是线程的数量,每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,方可继续执行后续代码,否则一直等待。
await:
调用await方法的线程会被挂起,它会等待直到count值为0才续执行。
多个线程执行await()方法,那么这几个线程都将处于等待状态,并且以共享模式享有同一个锁。
await(long timeout, TimeUnit unit):
等待一定的时间后 count值还没变为0的话,强行执行。
countDown:
将count值减1。countDown方法并没有规定一个线程只能调用一次,当同一个线程调用多次countDown()方法时,每次都会使计数器减一。
CyclicBarrier
和CountDownLatch类似,也是一种特殊的阻塞工具类,只是将减法变成加法,加到目标值,调用await方法的线程才会执行。
Semaphore
又被称为信号灯,一种特殊的阻塞工具类,在多线程环境下用于协调各个线程正确、合理的使用资源。
Semaphore内部维护了一个许可证列表,Semaphore允许一个线程获取和释放多个许可证。
acquire:获取1个许可证
acquire(int):获取n个许可证
如果许可证已分配完了,那么线程将进入等待状态,直到其他线程释放许可证才有机会再获取许可。
release:释放1个许可证
release(int):释放n个许可证
线程释放一个许可证,许可证将被归还给Semaphore,
阻塞与唤醒底层支持(wait/notify)
AQS
是AbstractQueuedSynchronizer:抽象的队列同步器,线程获取不到锁时,把它们放入队列中来排队获取锁。
是用来构建锁或者其它同步器组件的重量级基础框架及整个juc体系的基石,
通过内置的FIFO(先进先出的英文缩写)队列来完成资源获取线程的排队工作,并通过一个int类型变量
表示持有锁的状态。
LockSupport
lock support是什么
LockSupport是一个线程阻塞工具类,内部调用的是操作系统级别的native的阻塞原语。用于解决多线程中线程交互问题。
LockSupport是线程等待和唤醒机制,是wait和notify的改良加强版。
park=wait,unpark=notify
unpark可以在park执行之前被执行,如果unpark在park之前执行,则park被忽略。
使用场景
不需要lock的复杂功能,只需要wait和notify时使用。
一般来说,我们只需要调用Lock和Condition就够了,不需要用到LockSupport。
底层实现
LockSupport的底层是调用Unsafe中的native方法实现的。
LockSupport内部维护了一个permit许可标志,permit只能为0或者1,为0表示不阻塞,为1表示阻塞,unpark就是将permit变成1
park和unpark方法实现阻塞线程和解除线程阻塞的。
LockSupport和每个使用它的线程都有一个许可( permit)关联。 permit相当于1,0的开关,默认是0,
调用一次 unpark就加1变成1,调用一次park会消费 permit,也就是将1变成0,同时park立即返回。
如再次调用park会变成阻塞(因为 permit)为零了会阻塞在这里,一直到 permit变为1),这时调用 unpark会把 permit置为1。
每个线程都有一个相关的 permit, permiti最多只有一个,重复调用 unpark也不会积累凭证。
形象的理解
线程阻塞需要消耗凭证( permit),这个凭证最多只有1个。
当调用park方法时
*如果有凭证,则会直接消耗掉这个凭证然后正常退出;
*如果无凭证,就必须阻塞等待凭证可用
而 unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效。
为什么可以先唤醒线程后阻塞线程?
因为 unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。
为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
因为凭证的数量最多为1,连续调用两次 unpark和调用一次 unpark效果一样,只会增加一个凭证;
而调用两次park却需要消费两个凭证,证不够,不能放行。
Fork/Join
看这里:Java Fork/Join 并发编程
CompletableFuture
看这里:Java CompletableFuture 并发编程
多线程下正确使用集合
List线程安全问题
如下代码list中可能会出现null值,并且数量也可能少于1000。
public static void main(String[] args) {List<String> list = new ArrayList<>();for (int i = 1; i <= 1000; i++) {new Thread() {public void run() {list.add(new Random().nextInt(1000)+"");};}.start();}while (Thread.activeCount() > 1) {// 如果有main线程之外的线程在运行,就一直循环,保证后面的打印在最后执行Thread.yield();}System.out.println(list.size());System.out.println(list);}
打印:
998
[null, null, 148, 890, 423, 771, 882, 521, 830,......
换成下面代码,则会报java.lang.ArrayIndexOutOfBoundsException
public static void main(String[] args) {List<String> list = new ArrayList<>();for (int i = 1; i <= 1000; i++) {new Thread() {public void run() {list.add(new Random().nextInt(1000)+"");System.out.println(list.size());};}.start();}}
换成下面代码,则会报java.util.ConcurrentModificationException
public static void main(String[] args) {List<String> list = new ArrayList<>();for (int i = 1; i <= 1000; i++) {new Thread() {public void run() {list.add(new Random().nextInt(1000)+"");System.out.println(list);};}.start();}}
原因:
ArrayList不是线程安全的,其提供的各种方法包括add方法都会在多线程环境下出现脏数据问题。
解决方案:
方案1
Vector代替ArrayList,Vector其实就是使用synchronized。不过Vector已经过时了,如里面提供的迭代器是老旧版本的迭代器,非常不推荐使用。
方案2
Collections.synchronizedList包裹ArrayList,其实也是使用synchronized。
方案3
CopyonwriteArrayList代替ArrayList,其实使用的是ReentrantLock,并且使用了读写分离思想,读时不加锁,修改才加锁。
CopyonwriteArrayList源码:
public E get(int index) {//不加锁return get(getArray(), index);}public E set(int index, E element) {//加锁final ReentrantLock lock = this.lock;lock.lock();try {Object[] elements = getArray();E oldValue = get(elements, index);if (oldValue != element) {int len = elements.length;Object[] newElements = Arrays.copyOf(elements, len);newElements[index] = element;setArray(newElements);} else {setArray(elements);}return oldValue;} finally {lock.unlock();}}
Set线程安全问题
和List差不多
1.使用Collections.synchronizedSet
1.使用java.util.concurrent.CopyOnWriteArraySet
Map线程安全问题
同ArrayList
1.使用Hashtable
2.使用Collections.synchronizedMap
3.使用java.util.concurrent.ConcurrentHashMap
阻塞队列
阻塞队列(BlockingQueue),首先它是一个队列(Queue),而一个阻塞队列在数据结构中所起的作用大致如下图所示:
当阻塞队列是空时,从队列中获取元素的操作将会被阻塞。也就是说试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。
同样当阻塞队列是满时,往队列里添加元素的操作将会被阻塞。试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程从列中移除一个或者多个元素或者完全清空队列后使队列重新变得空闲起来后才能继续添加。
为什么需要阻塞队列
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切 BlockingQueue都给你一手包办了
在 concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而
这会给我们的程序带来不小的复杂度。
常用的阻塞队列
ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为 Integer.MAx_ VALUE)阻塞队列。
PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
LinkedTransferQueue:由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:由链表结构组成的双向阻塞队列
阻塞队列常用方法
方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time, unit) |
移除 | remove() | poll() | take() | poll(time,unit) |
检查 | element() | peek() | 不可用 | 不可用 |
结果 | 操作 |
---|---|
抛出异常 | 当阻塞队列满时,再往队列里add插入元素会抛 lllegalState Exception: Queue full。 当阻塞队列空时,再往队列里 remove移除元素会抛 NoSuchElementException |
特殊值 | 插入方法,成功ture失败 false移除方法,成功返回出队列的元素,队列里面没有就返回null。 |
一直阻塞 | 当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产线程直到put数据or响应中断退出。 当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用。 |
超时退出 | 当阻塞队列满时,队列会阻塞生产者线程一定时间,超过后限时后生产者线程会退出 |
阻塞队列使用场景
生产者消费者模式
消息中间件(也是生产者消费者模式)
线程池
连接池
ThreadLocal
看这里:ThreadLocal详解
线上定位问题线程
看这里:Java定位问题线程