- 整理一下并发相关的基础概念以及跟并发相关的问题整理(同时补充一点虚拟机的知识,算是复习,学习虚拟机知识是学习并发的前提);
一.什么是线程
-
了解线程之前需要先了解进程,进程是执行中的程序。是系统进行资源分配和调度的基本单位;
-
线程是进程中的一个实体(线程本身是不会独立存在的);
- 一个进程有多个线程
装逼操作:如何指定cpu跑某个进程(程序); 调用linux内核的api(java里是做不到的,必须要用c语言去写) 有面试官问则是装逼,反问更装逼。
-
操作系统与CPU的资源分配
- 操作系统是把资源分配给进程的;
- CPU是把资源分配给线程的(也说线程是CPU分配的基本单位);
二.程序计数器、栈、虚拟机栈、堆、方法区
- 讨论线程就离不开对堆、方法区、栈、程序计数器的学习,这几个内容在前面JVM的学习总有详细的介绍,这里只是基于线程做一下简单的总结;
2.1.程序计数器
- 较小的内存空间,用来记录线程当前要执行的指令地址;
- 各线程之间独立存储,互不影响;
2.2.栈
- 用于存储该线程的局部变量;
每个线程私有的,线程在运行时,在执行每个方法的时候都会打包成一个栈帧,存储了局部变量表,操作数栈,动态链接,方法出口等信息,然后放入栈。
每个时刻正在执行的当前方法就是虚拟机栈顶的栈桢。
方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程。
栈的大小确定为1M,可用参数 –Xss调整大小,例如-Xss256k;
局部变量表:
顾名思义就是局部变量的表,用于存放我们的局部变量的。首先它是一个32位的长度,主要存放我们的Java的八大基础数据类型,一般32位就可以存放下,如果是64位的就使用高低位占用两个也可以存放下,如果是局部的一些对象,比如我们的Object对象,我们只需要存放它的一个引用地址即可。
操作数据栈:
存放我们方法执行的操作数的,它就是一个栈,先进后出的栈结构,操作数栈,就是用来操作的,操作的的元素可以是任意的java数据类型,所以我们知道一个方法刚刚开始的时候,这个方法的操作数栈就是空的,操作数栈运行方法是会一直运行入栈/出栈的操作;
动态连接:
Java语言特性多态(需要类加载、运行时才能确定具体的方法),动态特性(Groovy、JS、动态代理);
返回地址:
正常返回(调用程序计数器中的地址作为返回)、异常的话(通过异常处理器表<非栈帧中的>来确定);
2.3.本地方法栈
- 保存的是native方法的信息;
2.4.堆
- 主要存放使用new操作创建的对象实例、数组(必须熟记)
几乎所有对象都分配在这里,也是垃圾回收发生的主要区域,用以下参数调整:
-Xms:堆的最小值;
-Xmx:堆的最大值;
-Xmn:新生代的大小;
-XX:NewSize;新生代最小值;
-XX:MaxNewSize:新生代最大值;
例如- Xmx256m
2.5.方法区
- 用来存放JVM加载的类、常量及静态变量等信息;
用于存储已经被虚拟机加载的类信息,常量,静态变量等数据(必须熟记),可用以下参数调整:
jdk1.7及以前:-XX:PermSize;-XX:MaxPermSize;
jdk1.8以后:-XX:MetaspaceSize; -XX:MaxMetaspaceSize
jdk1.8以后大小就只受本机总内存的限制
如:-XX:MaxMetaspaceSize=3M
2.6.站在线程角度
2.7.深入辨析堆和栈
2.7.1.功能
- 栈:以栈帧的方式存储方法调用的过程,并存储方法调用过程中基本数据类型的变量(int、short、long、byte、float、double、boolean、char等)以及对象的引用变量,其内存分配在栈上,变量出了作用域就会自动释放;
- 堆:用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中;
2.7.2.线程独享还是共享
- 栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存;
- 堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问;
2.7.3.空间大小
- 栈的内存要远远小于堆内存,栈的深度是有限制的,可能发生StackOverFlowError问题;
2.8.常见问题(必须熟记)
2.8.1.为什么需要程序计数器
- Java是多线程的,意味着线程切换,确保多线程情况下的程序正常执行;
2.8.2.为何要将程序计数器设计为线程私有的呢?
- 线程是占用CPU执行的基本单位,而CPU一般是使用时间片轮转方式让线程轮询占用的,所以当前线程CPU时间片用完后,要让出CPU,等下次轮到自己的时候再执行。那么如何知道之前程序执行到哪里了呢?其实程序计数器就是为了记录该线程让出CPU时的执行地址的,待再次分配到时间片时线程就可以从自己私有的计数器指定地址继续执行。
2.8.3.为什么JVM要使用栈?
- 非常符合JAVA中方法间的调用;
2.8.4.方法区放了哪些东西?
- 类信息、常量、静态变量、即时编译期编译后的代码;
三.对象相关
- 该部分请结合之前的学习笔记《Jvm小专题-一》,效果更佳;
3.1.对象的分配
- 虚拟机遇到一条new指令时,会有5个步骤
3.1.1.检查加载
- 先执行相应的类加载过程;
3.1.2.分配内存
-
为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来;
-
有两种方案,指针碰撞和空闲列表:
指针碰撞
如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”。
空闲列表
如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”。
- 选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定;
并发安全
- 除如何划分可用空间之外,还有另外一个需要考虑的问题是对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
- 解决这个问题有两种方案,CAS机制和分配缓冲
CAS机制
对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;
分配缓冲
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块私有内存,也就是本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个Buffer,如果需要分配内存,就在自己的Buffer上分配,这样就不存在竞争的情况,可以大大提升分配效率,当Buffer容量不够的时候,再重新从Eden区域申请一块继续使用。
TLAB的目的是在为新对象分配内存空间时,让每个Java应用线程能在使用自己专属的分配指针来分配空间,减少同步开销。
TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。当一个TLAB用满(分配指针top撞上分配极限end了),就新申请一个TLAB。
3.1.3.内存空间初始化
- 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(如int值为0,boolean值为false等等)。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值;
- 注意:不是构造方法;
3.1.4.设置
- 接下来,虚拟机要对对象进行必要的设置;
- 例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。
3.1.5.对象初始化
- 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚刚开始,所有的字段都还为零值。所以,一般来说,执行new指令之后会接着把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
3.2.对象的内存布局
-
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding);
-
对象头包括两部分信息;
- 第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等;
- 另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;
-
实例数据:程序代码中所定义的各种类型的字段内容;
-
对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对对象的大小必须是8字节的整数倍。当对象其他数据部分没有对齐时,就需要通过对齐填充来补全;
3.3.对象的访问定位
- 建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。目前主流的访问方式有使用句柄和直接指针两种;
- 对Sun HotSpot而言,它是使用直接指针访问方式进行对象访问的;
3.3.1.句柄
- 如果使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;
3.3.2.直接指针
- 如果使用直接指针访问, reference中存储的直接就是对象地址。
3.3.3.比较
- 这两种对象访问方式各有优势:
- 使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改;
- 使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本;
3.4.栈上分配
- 虚拟机提供的一种优化技术,基本思想是,对于线程私有的对象,将它打散分配在栈上,而不分配在堆上。好处是对象跟着方法调用自行销毁,不需要进行垃圾回收,可以提高性能;
- 栈上分配需要的技术基础,逃逸分析(了解即可,具体请看《Jvm小专题-一》);
- 启用栈上分配(略);
3.5.栈的执行过程演示
- Clazz反编译成字节:javap -v 文件名.class > NewName.txt,NewName文件 中可以查看字节码信息(目前能分析一个简单的字节码文件的执行);
- 如果要手写Jvm必须要做操作数栈 和 字节码非常熟悉。go语言相对而言要容易写一些;
四.Jvm相关
- Jvm相关的具体请看《Jvm小专题-一》,这里只做一点补充
- 空间担保:(2个对象放不下了怎么办? todo 温习视频,找出原因
4.1.GC是如何判断对象的存活
- 引用计数法:快,方便,实现简单,缺点:对象相互引用时,很难判断对象是否该回收(PHP语言在用);
- 可达性分析(牢记):基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的;
作为GC Roots的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即一般说的Native方法)引用的对象。
4.2.回收垃圾的算法
4.2.1.复制算法(Copying)
- 实现简单、运行高效
- 内存复制、没有内存碎片
- 利用率只有一半
补充:from、to区是因为复制算法才存在
4.2.2.标记-清除算法(Mark-Sweep)
- 利用率百分之百
- 不需要内存复制
- 有内存碎片
4.2.3.标记-整理算法(Mark-Compact)
- 利用率百分之百
- 没有内存碎片
- 需要内存复制
- 效率一般般
4.3.堆内存分配策略
- 对象优先在Eden分配,如果说Eden内存空间不足,就会发生Minor GC;
- 大对象直接进入老年代,大对象:需要大量连续内存空间的Java对象,比如很长的字符串和大型数组,1、导致内存有空间,还是需要提前进行垃圾回收获取连续空间来放他们;2、会进行大量的内存复制;
- 长期存活的对象将进入老年代,默认15岁(ART是6),-XX:MaxTenuringThreshold调整(了解即可);
- **动态对象年龄判定,**为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄;
- 空间分配担保:新生代中有大量的对象存活,survivor空间不够,当出现大量对象在MinorGC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代.只要老年代的连续空间大于新生代对象的总大小或者历次晋升的平均大小,就进行Minor GC,否则FullGC;
4.3.垃圾回收器
- todo 享学
补充:为什么新生代要用一个8:1:1(todo 重新看视频整理)
90%的对象不需要主动回收,没有必要将所有的区域都使用复制回收算法的方式来实现。结合经验,只需要预留10%的空间给被回收的对象即可。根据复制回收算法,额外需要10%的空间,因此就是8:1:1;
五.多线程
5.1.基础概念
5.1.1.时间片轮训机制
- 时间片轮转调度是一种最古老、最简单、最公平且使用最广的算法,又称RR调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间;
- 注意:时间片设得太短会导致过多的进程切换,降低了CPU效率;而设得太长又可能引起对短的交互请求的响应变差。将时间片设为100ms通常是一个比较合理的折衷;
5.2.2.并行与并发
- 并发:指应用能够交替执行不同的任务,比如单CPU核心下执行多线程并非是同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不断去切换这两个任务,已达到"同时执行效果",其实并不是的,只是计算机的速度太快,我们无法察觉到而已;
- 并行:指应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边打电话,这两件事情可以同时执行;
- 两者区别(识记):一个是交替执行,一个是同时执行;
- 面试高频题;
5.3.3.高并发编程
5.3.1.高并发编程的意义
- (1)充分利用CPU的资源
- (2)加快响应用户的时间
- (3)可以使你的代码模块化,异步化,简单化
5.3.2.多线程程序需要注意事项
- (1)线程之间的安全性
- (2)线程之间的死循环过程
- (3)线程太多了会将服务器资源耗尽形成死机当机
5.2.线程的启动与终止
5.2.1.启动
启动线程的方式有(3种):
1、X extends Thread;,然后X.run
2、X implements Runnable;然后交给Thread运行
3、X implements Callable;然后交给Thread运行
第1、2方式都有一个缺陷就是:在执行完任务之后无法获取执行结果。从Java 1.5开始,就提供了Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。
补充:
runnable是对任务的抽象;
thread是对线程的抽象(唯一的);
5.2.2.中止
-
自然中止,run方法执行完;
-
手动中止
暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。但是这些API是过期的,也就是不建议使用的。不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。正因为suspend()、resume()和stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法。
-
注意:处于死锁状态的线程无法被中断;
-
怎样才能让java里面的线程安全停止工作呢(面试点)
-
安全的中止则是其他线程通过调用某个线程A的interrupt()方法对其进行中断操作, 中断好比其他线程对该线程打了个招呼,“A,你要中断了”,不代表线程A会立即停止自己的工作,同样的A线程完全可以不理会这种中断请求。因为java里的线程是协作式的,不是抢占式的。线程通过检查自身的中断标志位是否被置为true来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()来进行判断当前线程是否被中断,不过Thread.interrupted()会同时将中断标识位改写为false。
5.3.run()、start()、yield()、join()
- start()方法:让一个线程进入就绪队列等待分配cpu,分到cpu后才调用实现的run()方法,start()方法不能重复调用;
- 而run方法:是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方法并没有任何区别,可以重复执行,可以被单独调用;
- start跟run
- run:普通的成员方法,可重读调用
- start:跟业务逻辑挂钩的,表示启动一个线程
- start跟run
- yield()方法:当前线程会让出CPU使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出CPU的那个线程来获取CPU执行权;
- (了解)将线程从运行状态编程就绪状态;
- join方法:把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行(面试可能会被问到,考察:让两个线程顺序执行)的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B;
5.4.synchronized
- 具体请看笔记《锁》
- synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制;
5.5.等待/通知机制
- wait():调用该方法的线程进入 WAITING状态,只有等待另外线程的通知或被中断才会返回.需要注意,调用wait()方法后,会释放对象的锁;
sleep 和wait 的区别:
1、这两个方法来自不同的类分别是Thread和Object;
2、最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法;
3、wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用(使用范围);
4、sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常;
5、sleep是Thread类的静态方法。sleep的作用是让线程休眠制定的时间,在时间到达时恢复,也就是说sleep将在接到时间到达事件事恢复线程执行。wait是Object的方法,也就是说可以对任意一个对象调用wait方法,调用wait方法将会将调用者的线程挂起,直到其他线程调用同一个对象的notify方法才会重新激活调用者;
sleep 抛出异常 并不终止线程
如果线程在sleep()状态下停止线程,会是什么效果?
如果在sleep状态下停止某一线程,会进入sleep的catch块中,抛出InterruptedException 异常,并且清除停止状态值,使之变成false;
网上查询的资料,未验证;
- notify():通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该线程获取到了对象的锁,没有获得锁的线程重新进入WAITING状态;
- notifyAll():通知所有等待在该对象上的线程;
5.6.锁
- 内置锁、显示锁、可重入锁、读写锁、排它锁、Condition接口,先记录关键字,后续在系统学习;
5.7.多线程程序需要注意的事项
- 线程之间的安全性
- 线程之间的死循环过程
- 线程太多了会将服务器资源耗尽形成死机当机
六.线程池
6.1.什么是线程池
- 线程池就是将线程进行池化,需要运行任务时从池中拿一个线程来执行,执行完毕,线程放回池中;
- 合理地使用线程池能够带来3个好处
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。 如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。它把T1,T3分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有T1,T3的开销了。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
6.2.JDK中的线程池和工作机制
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize:线程池中的核心线程数;
当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;
如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;
如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。
- 创建线程池各个参数的含义(面试点)
- 影响线程的工作机制,是从其参数中体现出来的
- maximumPoolSize:线程池中允许的最大线程数;
- 如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize;
- keepAliveTime:救急线程的存活时间;
- TimeUnit:keepAliveTime的时间单位;
- workQueue:workQueue必须是BlockingQueue阻塞队列。
- 当线程池中的线程数超过它的corePoolSize的时候,线程会进入阻塞队列进行阻塞等待。通过workQueue,线程池实现了阻塞功能;
6.3.阻塞队列
-
1)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满;
-
2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空;
-
阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器;
-
抛出异常、返回特殊值、一直阻塞、超时退出;
-
常用阻塞队列
•ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
•LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
•PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
•DelayQueue:一个使用优先级队列实现的无界阻塞队列。
•SynchronousQueue:一个不存储元素的阻塞队列。
•LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
•LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
-
threadFactory
- 创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名Executors静态工厂里默认的threadFactory,线程的命名规则是“pool-数字-thread-数字”;
-
RejectedExecutionHandler(饱和策略)
- 线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
(1)AbortPolicy:直接抛出异常,默认策略;
(2)CallerRunsPolicy:用调用者所在的线程来执行任务;
(3)DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
(4)DiscardPolicy:直接丢弃任务;
当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。
6.4.线程池的工作机制
-
当有任务提交给线程池的时候,如果当前线程数小于corePoolSize,则会新建一个线程;
-
如果继续提交任务,此时线程数大于等于corePoolSize,则将任务加入BlockingQueue存放起来,由corePool 从BlockingQueue中拿任务来执行;
-
若BlockingQueue也被填满了,则会根据maximumPool来进行判断;
- 小于maximumPool,则再启动线程来接收任务;
- 大于maximumPool,则会调用一个饱和或拒绝模式的方法(具体的策略由具体的实现定义);
-
注意:工作机制必须熟记;
6.5.合理配置线程池
- 了解即可(Android一般不会自己去创建线程池)
- 与任务的特性有点
CPI密集型:不要超过机器上CPU同时运行的线程个数;
IO密集型:2* 机器上CPU同时运行的线程个数;
混合型:拆分成上面2种;
评论区