导图社区 多线程
多线程、对任意单个使用 volatile 修饰的变量的读 / 写是具有原子性,但类似于 flag = !flag 这种复合操作不具有原子性。简单地说就是,单纯的赋值操作是原子性的。
编辑于2022-11-10 21:15:35时间管理-读书笔记,通过学习和应用这些方法,读者可以更加高效地利用时间,重新掌控时间和工作量,实现更高效的工作和生活。
本书是法兰教授的最新作品之一,主要阐明了设计史的来源、设计史现在的状况以及设计史的未来发展可能等三个基本问题。通过对设计史学科理论与方法的讨论,本书旨在促进读者对什么是设计史以及如何写作一部好的设计史等问题的深入认识与反思。
《计算机组成原理》涵盖了计算机系统的基本组成、数据的表示与运算、存储系统、指令系统、中央处理器(CPU)、输入输出(I/O)系统以及外部设备等关键内容。通过这门课程的学习,学生可以深入了解计算机硬件系统的各个组成部分及其相互之间的连接方式,掌握计算机的基本工作原理。
社区模板帮助中心,点此进入>>
时间管理-读书笔记,通过学习和应用这些方法,读者可以更加高效地利用时间,重新掌控时间和工作量,实现更高效的工作和生活。
本书是法兰教授的最新作品之一,主要阐明了设计史的来源、设计史现在的状况以及设计史的未来发展可能等三个基本问题。通过对设计史学科理论与方法的讨论,本书旨在促进读者对什么是设计史以及如何写作一部好的设计史等问题的深入认识与反思。
《计算机组成原理》涵盖了计算机系统的基本组成、数据的表示与运算、存储系统、指令系统、中央处理器(CPU)、输入输出(I/O)系统以及外部设备等关键内容。通过这门课程的学习,学生可以深入了解计算机硬件系统的各个组成部分及其相互之间的连接方式,掌握计算机的基本工作原理。
多线程
Java有哪些锁?
Java主流锁
乐观锁 & 悲观锁
乐观锁
乐观锁认为自己在使用数据的时候不会有别的线程修改数据,所以不会加锁,只是在更新数据的时候去判断有没有别的线程在这期间更新了这个数据,如果没有,则当前线程将自己的修改的数据成功写入;如果数据被别的线程更新了,则根据不同的实现方式执行不同的操作(报错或者重试);
乐观锁最常用的是CAS算法;
CAS算法非常高效,但也存在一些问题
ABA问题
cas需要再操作值的时候检查内存值是否发生变量,如果没有发生变化才会更新内存值;但如果内存值原来是A,后来被修改为B,然后又被修改成A,那么cas进行检查会认为没有变化,实际是有变化的;
解决思路:使用自增的版本号作比较;
循环时间长开销大
cas操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销;
只能保证一个共享变量的原子操作
对一个共享变量执行操作时,cas能够保证原子操作,但是对多个共享变量操作时,cas是无法保证操作的原子性的;
悲观锁
对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候,一定会有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改;(synchronized 、Lock的实现类 都是悲观锁)
应用场景
乐观锁:适合读操作多的场景,不加锁能使其读操作的性能大幅提升;
悲观锁:适合写操作多的场景,先加锁可以保证写操作是数据正确;
上图
自旋锁 & 适应性自旋锁
自旋锁
阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间,如果同步代码块中内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长;
自旋锁:当前线程在尝试获取锁的过程中,如果没有获取成功,线程不会挂起,而是通过自旋(循环)再次尝试获取;避免了切换线程的开销;
上图
自旋锁的缺点
自旋等待虽然避免了线程切换的开销,但它要占用处理器时间;如果锁被占用的时间很长,那么自旋的线程会白浪费了处理器资源;
所以自旋锁等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用 -XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程;
适应性自旋锁
自旋的时间(次数)不是固定的,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的;
如果同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间;
如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源;
无锁 & 偏向锁 & 轻量级锁 & 重量级锁
这四种锁是指锁的状态,专门针对synchronized的。
无锁
无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功;
无锁的特点是修改操作在循环内进行,线程会不断的尝试修改共享资源,直到修改成功;
CAS原理及应用即无锁的实现;
偏向锁
指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,以降低获取锁的代价;
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁;
轻量级锁
是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提供性能;
重量级锁
指当锁为轻量级锁的时候,若当前只有一个线程等待,则该线程通过自旋进行等待;但是当自旋超过一定的次数,或者一个线程持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁;
重量级锁会让其他申请的线程进入阻塞,性能降低;
公平锁 & 非公平锁
公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁,线程之间进入队列中排队,队列中的第一个线程才能获得锁;
优点:等待锁的线程不会饿死;
缺点:整体吞吐效率相对非公平锁要低,等待队列中除第一个线程外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大;
非公平锁
多个线程申请锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。如果获取锁的时候,锁刚好可用那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请的线程现获取锁的场景;
优点:可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程;
缺点:处于等待队列中的线程可能会饿死,或者等待很久才会获得锁;
可重入锁 & 非可重入锁
可重入锁
又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提是锁对象是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞;(ReentrantLock和synchronized都是可重入锁)
非可重入锁
同一个锁对象(同一个对象或者class)只能被一个线程获取一次,当前持有锁的线程未释放锁之前,该锁不可再获取;
非可重入锁在重复调用同步资源(重新获取同一对象锁)时会出现死锁;
ReentrantLock 和 NonReentrantLock实现原理
ReentrantLock 和 NonReentrantLock 都继承父类AQS,其父类AQS中维护了一个同步状态status来计数重入次数,status初始值为0;
当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status==0表示没有其他线程在执行同步代码,则把status置为1,当前线程开始执行;如果status!=0,则判断当前线程是否获取到这个锁的线程,如果是的话执行status+1,且当前线程可以再次获取锁;而可重入锁是直接去获取并尝试更新当前status的值,如果status!=0的话会导致其获取锁失败,当前线程阻塞;
释放锁时,可重入锁同样先获取当前的status的值,当前线程是持有锁的线程的前提下,如果status-1=0,则表示当前线程所有重复获取锁的操作都已经执行完毕,让后该线程才会真正释放锁;而非可重入锁则是在确定当前线程是持有锁的线程之后,直接将status置为0,将锁释放;
独享锁 & 共享锁
独享锁
也叫排他锁,是指该锁一次只能被一个线程所持有;如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程可以对数据进行读和写操作;
共享锁
共享锁是指该锁可被多个线程所持有;如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁;获得共享锁的线程只能对数据进行读操作,不能写;
ThreadLocal
ThreadLocal 是什么?
ThreadLocal是线程局部变量,为解决多线程并发问题提供了一种新的思路,当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal用法?
void set(T value)
public T get()
public void remove()
protected T initialValue()
实现原理?
ThreadLocal的变量是放在当前线程的ThreadLocalMap中,并不是存在ThreadLocal上;
ThreadLocal可以理解为是ThreadLocalMap的封装,传递了变量值;
每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为key,Object对象为Value的键值对;
ThreadLocal使用场景?
1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束;
2、线程间数据隔离;
3、进行事务操作,用于存储线程事务信息;
4、数据库连接,Session会话管理;
ThreadLocal和线程同步机制的比较?
在同步机制中,通过对象锁机制保证同一时间只有一个线程访问变量,这时该变量是多个线程共享的;
ThreadLocal从另一个角度解决线程的并发访问,为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突;
总结
同步机制采用了“以时间换空间”的方式,仅提供一份变量,让不同的线程排队访问;
ThreadLocal采用了“以空间换时间”的方式,为每一个线程提供一份变量,因此可以同时访问而互补影响;
ThreadLocal内存泄露问题?
ThreadLocal是一个实例变量,作为ThreadLocalMap的key,这里因为key被包装成弱引用,弱引用很不稳定,很容易被JVM回收,一旦被回收了,原本存在ThreadLocalMap的key-value,就变成了null-value,key消失了,value就找不到了,从而造成内存泄露。
解决办法
使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况;
JDK建议ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在了;
ThreadLocal存在内存泄露问题,那为啥要用弱引用,而不用强引用呢?
设置为弱引用的key能预防大多数内存泄漏的情况;
如果key 使用强引用,GC回收ThreadLocal的时候,由于ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏;
如果key为弱引用,GC回收ThreadLocal的时候,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set、get、remove的时候会被清除;
Java中的4种引用类型(强、弱、软、虚)
强引用(Strong Reference)
Strong Reference这个类型并不存在,默认的对象都是强引用类型,因为有后来的新引用所衬托,所以才起了个名字叫“强引用”;
除非是JVM垃圾回收器GC可达性分析结果为不可达,否则这类对象不会被垃圾回收器回收,即使JVM发生OOM也不会回收;
软引用(Soft Reference)
是一种比强引用生命周期稍弱的一种引用类型。
在JVM内存充足的情况下,软引用不会被垃圾回收器回收,只有在JVM内存不足的情况下,才会被垃圾回收器回收。
使用示例
弱引用(Weak Reference)
一种比软引用生命周期更短的引用;
它的生命周期很短,不论当前内存是否充足,都只能存活到下一次垃圾收集之前;
示例
虚引用(PhantomReference)
虚引用与前面的几种都不一样,这种引用类型不会影响对象的生命周期,所持有的引用就跟没持有一样,随时都能被GC回收;
在使用虚引用时,必须和引用队列关联使用;
在对象的垃圾回收过程中,如果GC发现一个对象还存在虚引用,则会把这个虚引用加入到与之关联的引用队列中;
程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收;
如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象内存被回收之前采取必要的行动防止被回收;
虚引用主要用来跟踪对象被垃圾回收器回收的活动;
示例
ThreadLocal类为什么要建议使用 private static 修饰?
private 修饰语ThreadLocal 本身没有关系,是否使用private修饰是一个普遍的问题而不是针对ThreadLocal的,主要是在安全方面进行考虑;
ThreadLocal采用 static修饰,作为某个类的静态变量,随着类的创建而创建,该类的所有实例共用该变量,避免重复创建ThreadLocal所关联的对象导致的内存浪费;简单说就是 防止无意义的多实例;
static修饰ThreadLocal变量,使该变量的生命周期延长,可以避免由于key被回收而导致的内存泄露问题;但使用完ThreadLocal,必须手动调用remove方法,否则会导致Entry内存泄露;
ThreadLocal,Thread,ThreadLocalMap,Entry<k,v>之间的关系?
每一个Thread中维护了一个ThreadLocalMap成员变量,所以ThreadLocalMap的生命周期跟Thread(当前线程)是一致的;
ThreadLocalMap类是ThreadLocal类的静态内部类;
Entry<k, v>是ThreadLocalMap的静态内部类;key 是ThreadLocal(声明为弱引用),value是Object,也就是我们要存的值;
线程死锁,解除线程死锁有哪几种方式?
线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
举例:线程A持有资源1,线程B持有资源2,它们同时想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态;
解决死锁的策略
死锁预防
破坏占有和等待条件
破坏不可剥夺条件
破坏循环等待条件
死锁避免
死锁检查
死锁解除
线程池
线程池的作用/好处?
降低资源消耗
通过重复利用已创建的线程,降低创建和销毁造成的消耗;
提高响应速度
当任务到达时,任务可以不需要等待线程创建就能立即执行;
提高线程的可管理性
线程是稀缺资源,如果无限的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控;
提供更多更强大的功能
线程池具备可扩展性,允许开发人员向其中增加更多的功能;
比如延时定时线程池 ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行;
线程池解决的问题是什么?
解决的核心问题是资源管理问题。在并发环境下,系统不能确定在任意时刻中,有多少任务需要执行,有多少资源需要投入。这种不确定性将带来以下若干问题:
频繁申请、销毁和调度资源,将带来额外的销毁可能会非常巨大;
对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险;
系统无法合理管理内部的资源分布,会降低系统的稳定性;
线程池有哪些参数?
corePoolSize(核心线程数)
定义了最小可以同时运行的线程数量;
maximumPoolSize(最大线程数)
当线程数>=corePoolSize,且任务队列已满时,线程池会创建新线程来处理任务;
当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常。
workQueue(任务队列容量)
当新任务来的时候,会先判断当前的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中;
keepAliveTime(线程空闲时间)
当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是等待,直到等待时间超过了keepAliveTime才会被回收销毁;
handler(饱和策略)
阻塞队列有几种?
用来保存等待被执行的任务的阻塞队列,且任务必须实现Runnable接口,在JDK中提供了如下几种阻塞队列;
ArrayBlockingQueue(有界队列)
基于数组结构的有界队列,按FIFO排序任务;
LinkedBlockingQueue(有/无界队列,传参就是有界,不传就是无界)
基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常高于ArrayBlockingQueue;
SynchronousQueue(同步移交队列)
一个线程调用put方法插入值,另一个线程调用take方法删除值;
一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常高于LinkedBlockingQueue;
PriorityBlockingQueue(优先队列)
具有优先级的无界阻塞队列;
拒绝策略有几种?各种策略的使用场景?
AbortPolicy(终止策略)
抛出RejectedExecutionException来拒绝新来的任务;
使用场景:这个没有特殊的场景,但是要正确处理抛出的异常,因为他会打断当前执行流程;
CallerRunsPolicy(调用者运行策略)
立即处理,由调用者线程处理该任务;
使用场景:一般在不允许失败的、对性能要求不高的、并发量较小的场景下使用;
DiscardPolicy(丢弃策略)
不处理新任务,直接丢掉,不会抛出异常;
使用场景:提交的任务无关要紧,可以使用它;
DiscardOldestPolicy(弃老策略)
丢弃最老(早进入队列)的未处理的任务,即排在队列最前面将要被执行的任务;然后重新提交被拒绝的任务;
使用场景:这个策略还是会丢弃任务,丢弃时也是毫无声息,但是特点是丢弃的是最老的未执行的任务,而且是待执行优先级较高的任务。基于这个特性,能想到的场景就是,发布消息和修改消息,当消息发布出去后,还未执行,此时更新的消息又来了,这个时候未执行的消息的版本比现在提交的消息版本要低就可以被丢弃了。
实现RejectedExecutionHandler接口,可自定义处理器
线程池有几种?
Executors.newCacheThreadPool():可缓存线程池(推荐使用)
没有核心线程数,直接向SynchronousQueue队列中提交任务;
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE),这样可灵活的往线程池中添加线程。
如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统OOM。
Executors.newFixedThreadPool(int n):固定数量的线程池
使用LinkedBlockingQueue无界队列;
创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
Executors.newScheduledThreadPool(int n):定长线程池
核心线程数和最大线程数都有,采用DelayedWorkQueue队列;
支持定时及周期性任务执行;
Executors.newSingleThreadExecutor():单个线程的线程池
使用LinkedBlockingQueue无界队列;
创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
如果这个线程异常结束,会有另一个取代它,保证顺序执行。
单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
创建线程池的方式?
使用 Executors 工具类的方式;
使用 ThreadPoolExecutor 方式;(推荐)
为什么不建议直接使用Executors创建线程池?
不直接使用工具类的目的是可以让我们知道线程池的运行规则,避免使用工具类的包装而不够直观内部机制而导致潜在的问题;
例如:使用Executors的 FixedThreadPoll 和 SingleThreadPool 创建的线程池,任务队列长度为Integer的最大值,这样可能会堆积大量的请求导致OOM(Out of Memory 内存用完了);
所以推荐通过明确的构造参数创建线程池,这样相当于时刻提醒自己线程池的特性是什么;
线程池的工作原理/工作过程?
提交任务后,线程池中线程数小于核心线程数,则创建线程处理任务;
当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列;
当线程数大于等于核心线程数,且任务队列已满;若线程数小于最大线程数,则创建线程处理队列的任务;若线程数等于最大线程数,执行饱和策略,默认会抛出RejectedExecutionException异常;
线程池核心设计与实现?
线程池总体设计
核心实现类ThreadPoolExecutor,继承关系及职责
继承关系UML图
ThreadPoolExecutor实现顶层接口Executor,顶层接口Executor提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,只需提供Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行部分。
ExecutorService接口增加了一些能力:
扩充执行任务的能力,补充可以为一个或一批异步任务生成Future的方法;
提供管控线程池的方法,比如停止线程池的运行;
AbstractExecutorService则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。
ThreadPoolExecutor实现最复杂的运行部分,一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。
ThreadPoolExecutor运行机制
运行流程
线程池内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程;
线程池的运行主要分成两部分:任务管理、线程管理;
任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:1、直接申请线程执行该任务;2、缓冲到队列中等待线程执行;3、拒绝该任务;
线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。
生命周期管理
线程池运行的状态,不是用户显式设置的,而是伴随着线程池的运行,由内部来维护;线程池内部使用一个变量维护两个值:运行状态(runState)和线程数量(workerCount)。
ctl 这个AtomicInteger类型,是对线程池的运行状态和线程池中有效线程的数量进行控制的一个字段,高3位保存runState,低29位保存workerCount,两个变量互不干扰;
用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。
源码中经常出现要同时判断线程池运行状态喝线程数量的情况;线程池提供了相应的方法供用户获得线程池当前的运行状态、线程个数;这里使用的是位运算,速度很快;
ThreadPoolExecutor的5种运行状态
Running:能接受新提交的任务,并且也能处理阻塞队列中的任务;
Shutdown:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务;
Stop:不能接受新任务,也不能处理队列中的任务,会中断正在处理的任务的线程;
Tidying:所有的任务都已终止了,workerCount(有效线程数)为0;
Terminated:在terminated()方法执行完后进入该状态;
生命周期转换图
任务执行机制
任务调度
任务调度是线程池的主要入口,当用户提交了一个任务,接下来这个任务将如何执行都是有这个阶段决定的;
所有的任务调度都是有execute()方法完成的,这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,或是直接拒绝该任务;执行过程如下:
首先检测线程池运行状态,如果不是Running,则直接拒绝,线程池要保证在Running的状态下执行任务;
如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务;
如果workerCount > corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中;
如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务;
如果workerCount>=maximumPoolSize,并且线程池内的阻塞队列已满,则根据拒绝策略来处理该任务,默认的处理方式是直接抛异常。
处理流程图
任务缓冲
任务缓冲模块是线程池能够管理任务的核心部分;阻塞队列缓存任务,工作线程从阻塞队列中获取任务;
阻塞队列是一个支持两个附加操作的队列;一是队列为空时,获取元素的线程会等待队列变为非空;二是队列满时,存储元素的线程会等待队列可用;
任务申请
任务的执行有两种可能:一是任务直接由新创建的线程执行;二是线程从任务队列中获取任务然后执行,执行完任务的空闲线程会再次去从队列中申请任务再去执行;第一种情况仅出现在线程初始创建的时候,第二种是线程获取任务绝大多数的情况;
获取任务流程图
任务拒绝
任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池;
Worker线程管理
Worker线程
执行任务
Worker工作线程,实现了Runnable接口,并持有一个线程thread,一个初始化的任务firstTask;
thread是在调用构造方法时,通过ThreadFactory来创建的线程,可以用来执行任务;
firstTask用来保存传入的第一个任务,这个任务可以为null;如果这个值不为空,则线程就会在启动初期立即执行这个任务,对应核心线程创建时的情况;如果这个值是null,那么久需要创建一个线程去执行任务队列表(workQueue)中的任务,也就是非核心线程的创建;
流程图
回收线程
线程池管理线程的生命周期,在线程长时间不运行的时候进行回收;线程池使用一张Hash表去持有线程的引用,通过添加、移除引用来控制线程的生命周期;
Worker通过继承AQS,实现独占锁的功能,没有使用可重入锁ReentrantLock,而是使用AQS,为的就是实现不可重入的特性去反映线程现在的执行状态;
lock方法一旦获取了独占锁,表示当前线程正在执行任务中;
如果正在执行任务,则不应该中断线程;
如果该线程现在不少独占锁的状态,也就是空闲的状态,这时可以对该线程进行中断;
线程池在执行shutdown方法或tryTerminate方法时,会调用interruptIdleWorkers方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是否是空闲状态,如果是空闲状态则可以安全回收;
回收过程图
Worker线程增加
增加线程是通过线程池的addWorker方法,该方法有两个参数:firstTask、core; firstTask 参数用于指定新增的线程执行的第一个任务,可以为空; core参数为true表示在新增线程时会判断当前活动线程是否少于corePoolSize,false表示新增线程前需要判断当前活动线程数是少于maximumPoolSize;
新增线程流程图
Worker线程回收
线程池中线程的销毁依赖JVM自动的回收,线程池做的工作是根据当前线程池的状态维护一定数量的线程引用,防止这部分线程被JVM回收,当线程池决定哪些线程需要回收时,只需要将其引用消除即可;
Worker线程被创建出来后,就会不断地进行轮询,然后获取任务去执行,核心线程可以无限等待获取任务,非核心线程要限时获取任务;当Worker无法获取到任务,也就是获取的任务为空时,循环会结束,Worker会主动消除自身在线程池内的引用;
Worker线程执行任务
在Worker类中的run方法调用了runWorker方法来执行任务,runWorker方法的执行过程如下:
while循环不断地通过getTask()方法获取任务;
getTask()方法从阻塞队列中取任务;
如果线程池正在停止,那么要保证当前线程是中断状态,否则要保证当前线程不是中断状态;
执行任务;
如果getTask结果为null,则跳出循环,执行processWorkerExit()方法,销毁线程;
执行流程
interrupt、interrupted、isInterrupted 方法的区别?如何停止一个正在运行的线程?
interrupt():用于中断线程,调用该方法的线程的状态将被置为“中断”状态;线程中断仅仅是置线程的中断状态位,不会停止线程。
interrupted():调用的是currentThread().isInterrupted(true)方法,检测当前线程是否已经中断,是则返回true,否则false,并清除中断状态;
isInterrupted():调用的是isInterrupted(false)方法,检测线程是否已经中断,是则返回true,否则false;中断状态不受该方法的影响;
java中有3种方法可以终止正在运行的线程
使用 stop() 方法强行终止(不推荐,该方法为过期作废的方法);
使用 interrupt() 方法;
使用退出标志的方式;
AQS原理?
ReentrantLock的实现方式?
锁的获取过程
通过CAS操作来修改state状态,表示争抢锁的操作,如果能够获取到锁,设置当前获得锁状态的线程。compareAndSetState(0,1);
如果没有获取到锁,尝试去获取锁,acquire(1)
通过tryAcquire尝试获取独占锁,如果成功返回true,失败返回false。如果是同一个线程来获得锁,则直接增加冲入次数,并返回true;
如果tryAcquire失败,则会通过addWaiter方法将当前线程封装成Node,添加到AQS队列尾部;
acquireQueued,将Node作为参数,通过自旋去尝试获取锁。
如果获取锁失败,则挂起线程。
锁的释放过程
Java怎么实现多线程?
继承Thread类并重写run方法;
实现Runnable接口并重写run方法;
实现Callable接口并重写call方法;
Java线程通信的方式?
目的:为了更好的协作,线程无论是交替执行,还是接力执行,都需要进行通信告知。
volatile方式
告知程序任何对变量的读取需要从主内存中获取,写必须同步刷新回主内存,保证所有线程对变量访问的可见性;
volatile两大特性
可见性:该特性可以让线程之间进行通信
所有volatile修饰的变量,一旦被某个线程更改,必须立即刷新到主内存;
所有volatile修饰的变量,使用之前必须重新读取主内存的值;
有序性:禁止指令重排序;
synchronized方式
确保多个线程在同一时刻只能有一个处于方法或同步块中,保证线程对变量访问的原子性、可见性和有序性;
wait()/notify()方式
在一个线程内调用该线程锁对象的wait()方法,线程将进入等待队列进行等待,直到被通知或者被唤醒。
为什么要必须获取锁?
因为调用wait()方法时,必须要先释放锁,如果没有持有锁,将会抛出异常。
join()方式
一个线程执行了某个线程的join()方法,这个线程就会阻塞等待执行join方法的线程终止,这里涉及等待/通知机制;
join底层通过wait实现,线程终止时会调用自身的notifyAll方法,通知所有等待在该线程对象上的线程;
管道IO方式
管道通信机制,类似消息通信机制;
管道IO流用于线程间数据传输,媒介为内存;
PipedOutputStream 和 PipedWriter 是输出流,相当于生产者;
Pipe的InputStream 和 PipedReader 是输入流,相当于消费者;
管道流使用一个默认大小为1kb的循环缓冲数组;
输入流从缓冲数组读取数据,输出流往缓冲数组写数据;
当数组已满时,输出流所在线程阻塞;当数组为空时,输入流所在线程阻塞;
ThreadLocal方式
ThreadLocal是线程共享变量,但它可以为每个线程创建单独的副本,副本值是线程私有的,互相之间不影响;
多线程下如何保证线程安全?
导致线程不安全的原因有3个
原子性:一个或多个操作在CPU执行的过程中被中断;
JDK里面提供了很多atomic类,比如:AtomicInteger,AtomicLong,AtomicBoolean等,这些类本身可以通过CAS来保证操作的原子性;
另外Java也提供了各种锁机制,来保证锁内的代码在同一时刻只能有一个线程执行;
可见性:一个线程对共享变量的修改,另一个线程不能立刻看到;
可以通过synchronized关键字加锁来解决;
java还提供了一种轻量级锁,即:volatile关键字,要优于synchronized的性能;volatile一般用于对变量的写操作不依赖于当前值的场景;
有序性:程序执行的顺序没有按照代码的先后顺序执行;
可以通过synchronized关键字定义同步代码块或者同步方法保障有序性;
也可以通过Lock接口保障有序性;Lock需要手动加锁和释放锁;
写一个死锁的例子?
栗子
讲一下volatile关键字的作用?
作用
保证了不同线程对该变量操作的内存可见性;
禁止指令重排序;
可见性实现原理
当写一个volatile变量时,JMM(Java Memory Model)将本地内存更改的变量写回到主内存中;
当取一个volatile变量时,JMM将使线程对应的本地内存失效,然后线程将从主内存读取共享变量;
volatile可以保证线程可见性且提供了一定的有序性,但无法保证原子性。在JVM底层是基于内存屏障实现的。
对任意单个使用 volatile 修饰的变量的读 / 写是具有原子性,但类似于 flag = !flag 这种复合操作不具有原子性。简单地说就是,单纯的赋值操作是原子性的。
对于非volatile变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的本地内存中;
synchronized作用,讲一讲底层实现?
synchronized 关键字解决的是多个线程之间访问资源的同步性,调用操作系统内核态做同步,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
主要的三种使用方式
修饰实例方法
作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁;
修饰静态方法
相当于给当前类加锁,会作用于类的所有对象实例。
修饰代码块
指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
synchronized底层原理属于JVM层面
synchronized同步语句块的情况
使用的是monitorenter 和 monitorexit 指令,其中monitorenter 指令指向同步代码块的开始位置,monitorexit则指明同步代码块的结束位置。
synchronized修饰方法的情况
JVM通过ACC_SYNCHRONIZED访问标识来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
ReentrantLock 和 synchronized 的区别?
底层实现
synchronized 是JVM层面的锁,通过monitor对象来完成(monitorenter与monitorexit),对象只有在同步块或同步方法中才能调用wait/notify方法;
ReentrantLock是从jdk1.5开始提供的API层面的锁;
是否可手动释放
synchronized不需要用户手动释放锁,代码执行完后系统会自动让线程释放对锁的占用;
ReentrantLock 需要用户手动释放锁,如果没有手动释放锁,就可能导致死锁现象;一般通过lock()和unlock()方法配合try/finally语句块来完成;
是否可中断
synchronized是不可中断类型的锁,除非加锁的代码块出现异常;
ReentrantLock 则可以中断,可通过trylock(long timeout, TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断;
是否公平锁
synchronized为非公平锁;
ReentrantLock默认为非公平锁,可以通过构造函数设置为公平锁;
锁是否可绑定条件Condition
synchronized不能绑定;
ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒;synchronized通过Object类的wait、notify、notifyAll方法要么随机唤醒一个线程,要么唤醒全部线程;
锁的对象
synchronized锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;
ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢;
synchronized 关键字和volatile 关键字的区别?
volatile 是线程同步的轻量级实现,性能比synchronized关键字要好;
volatile关键字只能用于变量;而synchronized可以修饰方法和代码块;
多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞;
volatile关键字能保证数据的可见性和有序性,但不能保证数据的原子性;而synchronized都可以保证;
volatile关键字主要用于解决变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性;