目录
一、线程状态
二、线程中止
三、CPU缓存和内存屏障
1、CPU性能优化手段-缓存
2、CPU性能优化手段-运行时指令重排
3、内存屏障
四、线程通信
五、线程封闭
六、线程池原理
1、为什么要用线程池?
2、线程池原理 - 概念
3、线程池API - 接口定义和实现类
4、创建线程池的方法
5、线程池原理 - 任务execute执行过程
6、线程数量
状态定义:java.lang.Thread.State内部枚举类 1、NEW:已创建但尚未启动运行 2、RUNNABLE:线程可运行,等待CPU调度。代表两种状态:正在执行,或可被执行等待调度。 3、BLOCKED:线程阻塞等待监视器锁定。处于synchronized同步代码块或方法中被阻塞,等待资源锁。 4、WAITING:等待状态。等待被唤醒。调用Object.wait、Thread.join、LockSupport.park会进入等待状态,不带超时,线程一直处于等待状态,依赖其他线程唤醒。 5、TIMED_WAITING:可指定等待时间的等待状态。调用Thread.sleep、Object.wait、Thread.join、LockSupport.parkNanos、LockSupport.parkUntil进入等待状态,可指定等待超时时间,超出后会抛出异常或线程继续执行。 6、TERMINATED:线程终止状态。线程正常执行完成或出现异常。
不正确的线程中止-Stop Stop:中止线程,并且清除监控器锁的信息,但是可能导致线程安全问题,JDK不建议用。 Destroy:JDK未实现该方法。 正确的线程中止-interrupt 线程处于等待状态时被阻塞,那么interrupt会生效,该线程的中断状态将被清除,抛出InterruptedException异常。 如果目标线程被IO或NIO中的Channel所阻塞,同样,IO操作会被中断或者返回特殊异常值,达到终止线程的目的。 如果以上条件都不满足,则会设置此线程的中断状态。 正确的线程中止-标志位 代码逻辑中,增加一个判断,用来控制线程执行的中止。循环代码中,根据条件判断停止循环,线程完成执行。
Stop强制终止线程,可能导致数据不一致,interrupt终止线程抛出异常,可捕获进行异常处理。
例如:CPU高速缓存。尽可能的避免处理器访问主内存的时间开销,处理器大多会利用缓存(cache)以提高性能。
1> CPU多级缓存(三级缓存)L1 cache(一级缓存)是CPU第一层高速缓存,分为数据缓存和指令缓存。一般服务器CPU的L1缓存容量通常在32-4096KB。 L2 由于L1级高速缓存容量的限制,为了再次提高CPU的运算速率,在CPU外部放置一高速存储器,即二级缓存。 L3 现在的都是内置的。L3的存在也是基于L1、L2的内存大小限制,L3缓存容量更大些。L3缓存的应用可以进一步降低内存延迟,同时提升大数据量计算时处理器的性能。具有较大L3缓存的处理器提供更有效的文件系统缓存行为及较短消息和处理器队列长度。一般是多核共享一个L3缓存。
CPU在读取数据时,先在L1中寻找,再从L2寻找,再从L3寻找,然后是内存,再后是外存储器(硬盘之类)。
2> 缓存同步协议为了解决多CPU读取同样数据缓存,进行不同运算后,写入主内存冲突,多数CPU厂商约定实现缓存一致性协议:MESI协议:规定每条缓存有个状态位,定义了下面四个状态。 修改态(Modified):此cache行已被修改过(脏行),内容已不同于主内存,为此cache专有; 专有态(Exclusive):此cache行内容同于主内存,但不出现于其他cache中; 共享态(Shared):此cache行内容同于主内存,但也出现于其他cache中; 无效态(Invalid):此cache行内容无效(空行)。
多处理器时,单个CPU对缓存中数据进行了改动,需要通知给其他CPU。 意味着,CPU除了要控制自己的读写操作,还要监听其他CPU发出的通知,从而保证最终一致。
指令重排的场景:当CPU写缓存时发现缓存区块正被其他CPU占用,为了提高CPU处理性能,可能将后面的读缓存命令优先执行。
需要遵守as-if-serial语义: 即不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器,runtime和处理器都必须遵守as-if-serial语义。不会对存在数据依赖关系的操作做重排序。
两个问题
1、CPU高速缓存下的问题 缓存中的数据与主内存中的数据不是实时同步的,各CPU(或CPU核心)间缓存的数据也不是实时同步的。在同一个时间点,各CPU所看到同一内存地址的数据的值可能是不一致的。
2、CPU执行指令重排序优化下的问题 遵守as-if-serial语义仅保证了在单CPU自己执行的情况下结果正确。多核线程中,指令逻辑无法分辨因果关联,可能出现乱序执行,导致程序运行结果错误。
处理器为了解决以上两个问题,提出了两个内存屏障指令(Memory Barrier):
写内存屏障(Store Memory Barrier):在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。强制写入主内存,这种显示调用,CPU就不会因为性能考虑而去对指令重排。读内存屏障(Load Memory Barrier):在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制重新从主内存加载数据。强制读取主内存内容,让CPU缓存与主内存保持一致,避免了缓存导致的一致性问题。
涉及到线程通信的类型: 文件共享、网络共享、共享变量、jdk提供的线程协调API(suspend/resume、wait/notify、park/unpark)
1、文件共享:线程1-->写入数据-->文件系统<--读取数据<--线程2 2、网络共享 3、变量共享:线程1-->写入数据-->内存<--读取数据<--线程2 4、线程协作-JDK API:多线程协作的典型场景:生产者-消费者模型。(线程阻塞、线程唤醒)
API - 被弃用的suspend/resume Thread.suspend()、Thread.resume()。 作用:调用suspend挂起目标线程,调用resume恢复线程执行。 被弃用是因为容易写出死锁的代码(在同步代码中,或者当suspend比resume后执行时),所以用wait/notify和park/unpark机制进行替代。
API - wait/notify机制 Object.wait()、Object.notify()。 wait方法导致当前线程等待,加入到该对象的等待集合中,并且放弃当前持有的对象锁。 notify/notifyAll方法唤醒一个或所有正在等待这个对象锁的线程。 这些方法只能由同一对象锁的持有者线程调用,也就是写在同步块里面,否则会抛出IllegalMonitorStateException异常。 虽然wait会自动解锁,但仍对执行顺序有要求,如果wait在notify之后调用,线程将会永远处于WAITING状态,尽管并不持有锁。
API - park/unpark机制 LockSupport.park()、LockSupport.unpark(thread)。 线程调用park则等待“许可”,unpark方法为指定线程提供“许可(permit)”。 不要求park和unpark方法的执行顺序。 多次调用unpark后,再调用park,线程会直接运行。只要调用park最终能拿到“许可”,就能继续执行。 连续多次调用park方法,第一次的调用拿到“许可”直接运行,后续的park调用会进入等待。 park挂起线程后不会像wait一样释放锁,在同步代码中有发生死锁的可能(等待、争抢锁)。
伪唤醒 代码中使用if语句来做条件判断是错误的,官方建议应该在循环中检查条件判断。因为处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查条件判断,程序就会在没有满足条件的情况下退出。 伪唤醒是指线程并非因为notify、notifyAll、unpark等api调用而唤醒,是更底层原因导致的。
线程封闭:数据都被封闭在各自的线程之中,就不需要同步,避免多线程访问共享可变数据时线程间数据同步的问题。 具体体现:ThreadLocal、局部变量
ThreadLocal 是Java里一种特殊的变量。是一个线程级别的变量,每个线程都有一个自己独立的ThreadLocal变量。彻底消除了多线程之间的竞争,在并发模式下是绝对安全的变量。 用法:ThreadLocal<T> var = new ThreadLocal<T>(); 会自动在每一个线程上创建一个T的副本,副本之间彼此独立,互不影响。可以用ThreadLocal存储一些参数,以便在线程中多个方法中使用,用来代替方法传参。
栈封闭 局部变量是封闭在线程中。位于执行线程的栈中,其他线程无法访问这个栈。
a.线程在Java中是一个对象,更是操作系统的资源,线程创建、销毁需要时间,如果创建时间+销毁时间>执行任务时间就很不合算。 b.Java对象占用堆内存,操作系统线程占用系统内存。根据JVM规范,一个线程默认最大栈大小1M,这个栈空间是需要从系统内存中分配的,线程过多会消耗很多内存。 c.操作系统需要频繁切换线程上下文,影响性能。 线程池的推出,就是为了方便的控制线程数量。
a.线程池管理器:用于创建并管理线程池,包括创建线程池,销毁线程池,添加新任务。 b.工作线程:线程池中的线程,在没有任务时处于等待状态,可以循环的执行任务。 c.任务接口:每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等。runnable、callable。 d.任务队列:用于存放没有处理的任务,提供一种缓冲机制。
Java通过Executors工厂类提供了四种方式创建线程池:
newFixedThreadPool(int nThreads):创建一个固定大小、任务队列容量无界的线程池。核心线程数=最大线程数。
newCachedThreadPool():创建一个大小无界的缓冲线程池。它的任务队列是一个同步队列。任务加入到池中,如果池中有空闲线程,则用空闲线程执行,否则创建新线程执行。池中的线程空闲超过60秒,将被销毁释放。线程数随任务的多少变化。适用于执行耗时较小的异步任务。池的核心线程数=0,最大线程数=Interger.MAX_VALUE。
newSingleThreadExecutor():只有一个线程来执行无界任务队列的单一线程池。该线程池确保任务按加入的顺序一个一个一次执行。当唯一的线程因任务异常中止时,将创建一个新的线程来继续执行后续的任务。与newFixedThreadPool(1)的区别在于,单一线程池的池大小在newSingleThreadExecutor方法中硬编码,不能再改变。
newScheduledThreadPool(int corePoolSize):能定时执行任务的线程池。该池的核心线程池由参数指定,最大线程数=Interger.MAX_VALUE。
推荐的线程池创建方式: ExecutorService executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, timeUnit, blockingQueue);
ThreadPoolExecutor提供了四个构造方法,参数的含义分别为: corePoolSize:核心线程池大小 maximumPoolSize:最大线程池大小 keepAliveTime:线程最大空闲时间 unit:时间单位。线程最大空闲时间的单位 workQueue:线程等待队列 ThreadFactory:线程创建工厂 handler:拒绝策略
查看ThreadPoolExecutor.execute()方法源码可知线程池任务执行过程: a.是否达到核心线程数?没达到,创建一个工作线程来执行任务。 b.达到核心线程数,判断工作队列是否已满?没满,将新提交的任务存储在工作队列里。 c.工作队列已满,判断是否达到最大线程数?没达到,创建一个新的工作线程来执行任务。 d.达到最大线程数,执行拒绝策略来处理这个任务。
如何确定合适数量的线程?
计算型任务:CPU数量的1-2倍 IO型任务:对比计算型任务,需要多一些线程,根据具体的IO阻塞时长进行考量决定。比如Tomcat中默认的最大线程数为200。 也可考虑根据需要在一个最小数量和最大数量间自动增减线程数。如CachedThreadPool。