diff --git "a/JDK/JVM/Java\345\206\205\345\255\230\346\250\241\345\236\213\346\267\261\345\205\245\350\257\246\350\247\243(JMM).md" "b/JDK/JVM/Java\345\206\205\345\255\230\346\250\241\345\236\213\346\267\261\345\205\245\350\257\246\350\247\243(JMM).md" new file mode 100644 index 000000000..1dbb544b4 --- /dev/null +++ "b/JDK/JVM/Java\345\206\205\345\255\230\346\250\241\345\236\213\346\267\261\345\205\245\350\257\246\350\247\243(JMM).md" @@ -0,0 +1,128 @@ +# 前言 +定义俩共享变量及俩方法: +- 第一个方法, +- 第二个方法 +- (r1,r2)的可能值有哪些? +![](https://img-blog.csdnimg.cn/05139ccfbb40447a869632ff35959841.png) + +在单线程环境下,可先调用第一个方法,最终(r1,r2)为(1,0) +也可以先调用第二个方法,最终为(0,2)。 + +![](https://img-blog.csdnimg.cn/20200404214401993.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) +# 1 Java内存模型的意义 +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTkyYTFmZGY0OGJlMTllMDYucG5n?x-oss-process=image/format,png) +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTIzZTVlOWE0OWFkZWI1YTEucG5n?x-oss-process=image/format,png) +JMM 与硬件内存架构对应关系![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTVlMTM3NGEwYWJmOWM5MjkucG5n?x-oss-process=image/format,png) +JMM抽象结构图 +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWQ0ZWE4ODQzYTg4YTk0MGQucG5n?x-oss-process=image/format,png) +内存模型描述程序的可能行为。 + +Java虚拟机规范中试图定义一种Java内存模型,`来屏蔽掉各种硬件和os的内存访问差异`,规定: +- 线程如何、何时能看到其他线程修改过的共享变量的值 +- 必要时,如何同步地访问共享变量 + +以实现让Java程序在各种平台下都能达到一致性的内存访问效果。 + +JMM通过检查执行跟踪中的每个读操作,并根据某些规则检查该读操作观察到的写操作是否有效来工作。 + +只要程序的所有执行产生的结果都可由JMM预测。具体实现者任意实现,包括操作的重新排序和删除不必要的同步。 + +JMM决定了在程序的每个点上可以读取什么值。 +## 1.1 共享变量(Shared Variables) +可在线程之间共享的内存称为`共享内存或堆内存`。所有实例字段、静态字段和数组元素都存储在堆内存。 +不包括局部变量与方法参数,因为这些是线程私有的,不存在共享。 + +对同一变量的两次访问(读或写),若有一个是写请求,则是冲突的! +# 2 主内存与工作内存 +工作内存缓存 +![](https://img-blog.csdnimg.cn/20191014024209488.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) +JMM的主要是定义了`各个变量的访问规则`,在JVM中的如下底层细节: +- 将变量存储到内存 +- 从内存中取出变量值 + +为获得较好执行效率,JMM并未限制执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器调整代码执行顺序这类权限。 + +JMM规定: +- 所有变量都存储在主内存(Main Memory) +- 每条线程有自己的工作内存(Working Memory) +保存了该线程使用到的`变量的主内存副本拷贝`(线程所访问对象的引用或者对象中某个在线程访问到的字段,不会是整个对象的拷贝) +线程对变量的所有操作(读,赋值等)都必须在工作内存进行,不能直接读写主内存中的变量 +volatile变量依然有工作内存的拷贝,,是他特殊的操作顺序性规定,看起来如同直接在主内存读写 +不同线程间,无法直接访问对方工作内存中的变量,线程间变量值的传递均要通过主内存 + +线程、主内存、工作内存三者的交互关系: +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTEyMjA5YjEyZDU3OGEyZWQucG5n?x-oss-process=image/format,png) +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWJiM2QzN2MxNTVjZDgyZDgucG5n?x-oss-process=image/format,png) + +JVM模型与JMM不是同一层次的内存划分,基本毫无关系的,硬要对应起来,从变量,内存,工作内存的定义来看 +- 主内存 《=》Java堆中的对象实例数据部分 +- 工作内存 《=》虚拟机栈中的部分区域 + +从更底层的层次来看: +- 主内存直接对应物理硬件的内存 +- 为更好的运行速度,虚拟机(甚至硬件系统的本身的优化措施)可能会让工作内存优先存储于寄存器和高速缓存器,因为程序运行时主要访问读写的是工作内存 +# 3 内存间同步操作 +## 3.1 线程操作的定义 +### 操作定义 +write要写的变量以及要写的值。 +read要读的变量以及可见的写入值(由此,我们可以确定可见的值)。 +lock要锁定的管程(监视器monitor)。 +unlock要解锁的管程。 +外部操作(socket等等..) +启动和终止 +### 程序顺序 +如果一个程序没有数据竞争,那么程序的所有执行看起来都是顺序一致的 + +本规范只涉及线程间的操作; +一个变量如何从主内存拷贝到工作内存,从工作内存同步回主内存的实现细节 + +JMM 本身已经定义实现了以下8种操作来完成,且都具备`原子性` +- lock(锁定) +作用于主内存变量,把一个变量标识为一条线程独占的状态 +- unlock(解锁) +作用于主内存变量,把一个处于锁定状态的变量释放,释放后的变量才可以被其它线程锁定 +unlock之前必须将变量值同步回主内存 +- read(读取) +作用于主内存变量,把一个变量的值从主内存传输到工作内存,以便随后的load +- load(载入) +作用于工作内存变量,把read从主内存中得到的变量值放入工作内存的变量副本 +- use(使用) +作用于工作内存变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到的变量的值得字节码指令时将会执行这个操作 +- assign(赋值) +作用于工作内存变量,把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作 +- store(存储) +作用于工作内存变量,把工作内存中一个变量的值传送到主内存,以便随后的write操作使用 +- write(写入) +作用于主内存变量,把store操作从工作内存中得到的值放入主内存的变量中 + +- 把一个变量从主内存`复制`到工作内存 +就要顺序执行read和load + +- 把变量从工作内存`同步`回主内存 +就要顺序地执行store和write操作 + +JMM只要求上述两个操作必须`按序执行`,而没有保证连续执行 +也就是说read/load之间、store/write之间可以插入其它指令 +如对主内存中的变量a,b访问时,一种可能出现的顺序是read a->readb->loadb->load a + +JMM规定执行上述八种基础操作时必须满足如下 +## 3.1 同步规则 +◆ 对于监视器 m 的解锁与所有后续操作对于 m 的加锁 `同步`(之前的操作保持可见) +◆对 volatile变量v的写入,与所有其他线程后续对v的读同步 + +◆ `启动` 线程的操作与线程中的第一个操作同步 +◆ 对于每个属性写入默认值(0, false, null)与每个线程对其进行的操作同步 +◆ 线程 T1的最后操作与线程T2发现线程T1已经结束同步。( isAlive ,join可以判断线程是否终结) +◆ 如果线程 T1中断了T2,那么线程T1的中断操作与其他所有线程发现T2被中断了同步通过抛出*InterruptedException*异常,或者调用*Thread.interrupted*或*Thread.isInterrupted* + +- 不允许read/load、store/write操作之一单独出现 +不允许一个变量从主内存读取了但工作内存不接收,或从工作内存发起回写但主内存不接收 +- 不允许一个线程丢弃它的最近的assign +即变量在工作内存中改变(为工作内存变量赋值)后必须把该变化同步回主内存 +- 新变量只能在主内存“诞生”,不允许在工作内存直接使用一个未被初始化(load或assign)的变量 +换话说就是一个变量在实施use,store之前,必须先执行过assign和load +- 如果一个变量事先没有被load锁定,则不允许对它执行unlock,也不允许去unlock一个被其它线程锁定的变量 +- 对一个变量执行unloack前,必须把此变量同步回主内存中(执行store,write) + +> 参考 +> - https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.1 \ No newline at end of file diff --git "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\344\277\241\345\217\267\351\207\217\346\250\241\345\236\213Semaphore.md" "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\344\277\241\345\217\267\351\207\217\346\250\241\345\236\213Semaphore.md" index 0c8437de8..68b507ec2 100644 --- "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\344\277\241\345\217\267\351\207\217\346\250\241\345\236\213Semaphore.md" +++ "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\344\277\241\345\217\267\351\207\217\346\250\241\345\236\213Semaphore.md" @@ -1,53 +1,55 @@ -# 1 信号量模型 -## 1.1 模型示意图 + +> 面试官一声冷笑:用过semaphore吧,说说信号量模型? + +信号量模型可简单概括为:一个计数器,一个等待队列,三个方法。在信号量模型里,计数器和等待队列对外是透明的,所以只能通过信号量模型提供的三个方法来访问它们,这三个方法分别是:init()、down()和up()。你可以结合下图来形象化地理解。 + +- 信号量模型 ![](https://img-blog.csdnimg.cn/2021042214541150.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -信号量模型可简单概括为:一个计数器,一个等待队列,三个方法。 -计数器和等待队列对外透明,所以只能通过信号量模型提供的三个方法来访问它们: -## 1.2 三个方法 -### 1.2.1 init() + +> 详细解释下里面提到的这些方法? + +- init() 设置计数器初始值。 -### 1.2.2 down() +- down() 计数器-1;若此时计数器<0,则当前线程被阻塞,否则当前线程可继续执行 -### 1.2.3 up() -计数器+1;若此时计数器≤0,则唤醒等待队列中的一个线程,并将其从等待队列中移除。 -有的人可能认为这里的判断条件应该≥0,估计你是理解生产者-消费者模式中的生产者。可这样思考,`>0` 意味着无阻塞的线程,所以只有 ≤0 时才需唤醒一个等待的线程。 +- up() +计数器+1;若此时计数器≤0,则唤醒等待队列中的一个线程,并将其从等待队列中移除。有的人可能认为这里的判断条件应该≥0,估计你是理解生产者-消费者模式中的生产者。可这样思考,`>0` 意味着没有阻塞的线程,所以只有 ≤0 时才需要唤醒一个等待的线程。 -down()和up()应成对出现 && 先调用down()获取锁,处理完成后再调用up()释放锁。若信号量init值为1,应该不会出现>0情况,除非故意调先用up(),这也失去了信号量本身的意义了。 +down()和up()应该成对出现,并且先调用down()获取锁,处理完成后再调用up()释放锁。若信号量init值为1,应该不会出现>0情况,除非故意调先用up(),这也失去了信号量本身的意义了。 这些方法都是原子性的,并且这个原子性是由信号量模型的实现方保证的。JDK里的信号量模型是由java.util.concurrent.Semaphore实现,Semaphore这个类能够保证这三个方法都是原子操作。 -## 1.3 代码实现 + +> talk is cheap,show me code? + +- 代码 ![](https://img-blog.csdnimg.cn/20210422115452600.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) 信号量模型中,down()、up()最早被称为P操作和V操作,信号量模型也称PV原语。还有的人会用semWait()和semSignal()表达它们,叫法不同,语义都相同。JUC的acquire()和release()就对应down()和up()。 -# 2 如何使用信号量? -## 2.1 实例 +> 如何使用信号量? + 就像红绿信号灯,车必须先检查是否为绿灯,绿灯才能通过。 比如累加器,count+=1操作是个临界区,只允许一个线程执行,也就是说要保证互斥。 ![](https://img-blog.csdnimg.cn/20210422122329760.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -## 2.2 分析 -假设线程t1、t2同时访问add(),当同时调用`acquire()`时,由于`acquire()`是个原子操作,只可能有一个线程(假设t1)把信号量里的计数器减为0,t2则是将计数器减为-1: +分析如下:假设线程t1、t2同时访问add(),当同时调用`acquire()`时,由于`acquire()`是一个原子操作,只可能有一个线程(假设t1)把信号量里的计数器减为0,t2则是将计数器减为-1: - 对t1,信号量里面的计数器的值是0,≥0,所以t1继续执行 - 对t2,信号量里面的计数器的值是-1,<0,所以t2被阻塞 所以此时只有t1会进入临界区执行count+=1。 当t1执行release(),信号量里计数器的值是-1,加1之后的值是0,小于等于0,根据up()操作,此时等待队列中的t2会被唤醒。于是t2在t1执行完临界区代码后,才获得进入临界区执行的机会,这就保证了互斥性。 -## 2.3 既有JDK的Lock,为何还造个Semaphore? -实现互斥锁,仅是 Semaphore的部分功能,还可允许多个线程访问一个临界区。 -最常见的就是各种池化资源:连接池、对象池、线程池等。比如数据库连接池,同一时刻,是允许多个线程同时使用连接池的。每个连接在被释放前,不允许其他线程使用。 -对象池要求一次性创建出N个对象,之后所有线程重复利用这N个对象,当然对象在被释放前,不允许其他线程使用。所以核心就是限流器的设计:不允许多于N个线程同时进入临界区。 +既然有JDK提供了Lock,为啥还要提供一个Semaphore ?实现互斥锁,仅是 Semaphore部分功能,Semaphore还可以允许多个线程访问一个临界区。 -那如何快速实现一个这样的限流器呢? -信号量! -若把计数器的值设置成对象池里对象的个数N,就能完美解决对象池的限流问题。 +最常见的就是各种池化资源:连接池、对象池、线程池等。比如数据库连接池,同一时刻,一定是允许多个线程同时使用连接池的。每个连接在被释放前,是不允许其他线程使用的。 +对象池要求一次性创建出N个对象,之后所有的线程重复利用这N个对象,当然对象在被释放前,也是不允许其他线程使用的。所以核心就是限流器的设计,这里限流指不允许多于N个线程同时进入临界区。 +如何快速实现一个这样的限流器呢?那就是信号量。 +如果我们把计数器的值设置成对象池里对象的个数N,就能完美解决对象池的限流问题了。 代码如下: ![](https://img-blog.csdnimg.cn/20210422144637459.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - 注意这里使用的是 Vector,进入临界区的N个线程不安全。add/remove都是不安全的。比如 ArrayList remove() : ```java public E remove(int index) { @@ -66,4 +68,8 @@ public E remove(int index) { return oldValue; } -``` \ No newline at end of file +``` + + + +> 好的,请回家等通知吧! \ No newline at end of file diff --git "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\263\273\345\210\22715\344\271\213\345\216\237\345\255\220\351\201\215\345\216\206\344\270\216\351\235\236\351\230\273\345\241\236\345\220\214\346\255\245\346\234\272\345\210\266(Atomic-Variables-and-Non-blocking-Synchron.md" "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\263\273\345\210\227(15)-\345\216\237\345\255\220\351\201\215\345\216\206\344\270\216\351\235\236\351\230\273\345\241\236\345\220\214\346\255\245\346\234\272\345\210\266.md" similarity index 68% rename from "JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\263\273\345\210\22715\344\271\213\345\216\237\345\255\220\351\201\215\345\216\206\344\270\216\351\235\236\351\230\273\345\241\236\345\220\214\346\255\245\346\234\272\345\210\266(Atomic-Variables-and-Non-blocking-Synchron.md" rename to "JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\263\273\345\210\227(15)-\345\216\237\345\255\220\351\201\215\345\216\206\344\270\216\351\235\236\351\230\273\345\241\236\345\220\214\346\255\245\346\234\272\345\210\266.md" index 13eabf7ad..5aece1fb2 100644 --- "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\263\273\345\210\22715\344\271\213\345\216\237\345\255\220\351\201\215\345\216\206\344\270\216\351\235\236\351\230\273\345\241\236\345\220\214\346\255\245\346\234\272\345\210\266(Atomic-Variables-and-Non-blocking-Synchron.md" +++ "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\263\273\345\210\227(15)-\345\216\237\345\255\220\351\201\215\345\216\206\344\270\216\351\235\236\351\230\273\345\241\236\345\220\214\346\255\245\346\234\272\345\210\266.md" @@ -1,32 +1,30 @@ -近年并发算法领域大多数研究都侧重非阻塞算法,这种算法用底层的原子机器指令代替锁来确保数据在并发访问中的一致性,非阻塞算法被广泛应用于OS和JVM中实现线程/进程调度机制和GC以及锁,并发数据结构中。 - -与锁的方案相比,非阻塞算法都要复杂的多,他们在可伸缩性和活跃性上(避免死锁)都有巨大优势。 +非阻塞算法,用底层的原子机器指令代替锁,确保数据在并发访问中的一致性。 +非阻塞算法被广泛应用于OS和JVM中实现线程/进程调度机制和GC及锁,并发数据结构中。 +与锁相比,非阻塞算法复杂的多,在可伸缩性和活跃性上(避免死锁)有巨大优势。 非阻塞算法,即多个线程竞争相同的数据时不会发生阻塞,因此能更细粒度的层次上进行协调,而且极大减少调度开销。 # 1 锁的劣势 独占,可见性是锁要保证的。 -许多JVM都对非竞争的锁获取和释放做了很多优化,性能很不错了。 - -但是如果一些线程被挂起然后稍后恢复运行,当线程恢复后还得等待其他线程执行完他们的时间片,才能被调度,所以挂起和恢复线程存在很大的开销,其实很多锁的力度很小的,很简单,如果锁上存在着激烈的竞争,那么多调度开销/工作开销比值就会非常高。 - -与锁相比volatile是一种更轻量的同步机制,因为使用volatile不会发生上下文切换或者线程调度操作,但是volatile的指明问题就是虽然保证了可见性,但是原子性无法保证,比如i++的字节码就是N行。 +许多JVM都对非竞争的锁获取和释放做了很多优化,性能很不错。 +但若一些线程被挂起然后稍后恢复运行,当线程恢复后还得等待其他线程执行完他们的时间片,才能被调度,所以挂起和恢复线程存在很大开销。 +其实很多锁的粒度很小,很简单,若锁上存在激烈竞争,那么 调度开销/工作开销 比值就会非常高,降低业务吞吐量。 -如果一个线程正在等待锁,它不能做任何事情,如果一个线程在持有锁的情况下呗延迟执行了,例如发生了缺页错误,调度延迟,那么就没法执行。如果被阻塞的线程优先级较高,那么就会出现priority invesion的问题,被永久的阻塞下去。 +而与锁相比,volatile是一种更轻量的同步机制,因为使用volatile不会发生上下文切换或线程调度操作,但volatile的指明问题就是虽然保证了可见性,但是原子性无法保证。 +- 若一个线程正在等待锁,它不能做任何事情 +- 若一个线程在持有锁情况下被延迟执行了,如发生缺页错误,调度延迟,就没法执行 +- 若被阻塞的线程优先级较高,就会出现priority invesion问题,被永久阻塞 # 2 硬件对并发的支持 +独占锁是悲观锁,对细粒度的操作,更高效的应用是乐观锁,这种方法需要借助**冲突监测机制,来判断更新过程中是否存在来自其他线程的干扰,若存在,则失败重试**。 -独占锁是悲观所,对于细粒度的操作,更高效的应用是乐观锁,这种方法需要借助**冲突监测机制来判断更新过程中是否存在来自其他线程的干扰,如果存在则失败重试**。 - -几乎所有的现代CPU都有某种形式的原子读-改-写指令,例如compare-and-swap等,JVM就是使用这些指令来实现无锁并发。 - +几乎所有现代CPU都有某种形式的原子读-改-写指令,如compare-and-swap等,JVM就是使用这些指令来实现无锁并发。 ## 2.1 比较并交换 - CAS(Compare and set)乐观的技术。Java实现的一个compare and set如下,这是一个模拟底层的示例: - ```java @ThreadSafe public class SimulatedCAS { + @GuardedBy("this") private int value; public synchronized int get() { @@ -47,9 +45,7 @@ public class SimulatedCAS { == compareAndSwap(expectedValue, newValue)); } } - ``` - ## 2.2 非阻塞的计数器 ```java public class CasCounter { @@ -67,14 +63,12 @@ public class CasCounter { return v + 1; } } - ``` Java中使用AtomicInteger。 竞争激烈一般时,CAS性能远超基于锁的计数器。看起来他的指令更多,但无需上下文切换和线程挂起,JVM内部的代码路径实际很长,所以反而好些。 -但激烈程度较高时,它的开销还是较大,但是你会发生这种激烈程度非常高的情况只是理论,实际生产环境很难遇到。 -况且JIT很聪明,这种操作往往能非常大的优化。 +但激烈程度较高时,开销还是较大,但会发生这种激烈程度非常高的情况只是理论,实际生产环境很难遇到。况且JIT很聪明,这种操作往往能非常大的优化。 为确保正常更新,可能得将CAS操作放到for循环,从语法结构看,使用**CAS**比使用锁更加复杂,得考虑失败情况(锁会挂起线程,直到恢复)。 但基于**CAS**的原子操作,性能基本超过基于锁的计数器,即使只有很小的竞争或不存在竞争! @@ -82,37 +76,19 @@ Java中使用AtomicInteger。 在轻度到中度争用情况下,非阻塞算法的性能会超越阻塞算法,因为 CAS 的多数时间都在第一次尝试时就成功,而发生争用时的开销也不涉及**线程挂起**和**上下文切换**,只多了几个循环迭代。 没有争用的 CAS 要比没有争用的锁轻量得多(因为没有争用的锁涉及 CAS 加上额外的处理,加锁至少需要一个CAS,在有竞争的情况下,需要操作队列,线程挂起,上下文切换),而争用的 CAS 比争用的锁获取涉及更短的延迟。 -CAS的缺点是它使用调用者来处理竞争问题,通过重试、回退、放弃,而锁能自动处理竞争问题,例如阻塞。 +CAS的缺点是,它使用调用者来处理竞争问题,通过重试、回退、放弃,而锁能自动处理竞争问题,例如阻塞。 -原子变量可以看做更好的volatile类型变量。 -AtomicInteger在JDK8里面做了改动。 -```java -public final int getAndIncrement() { - return unsafe.getAndAddInt(this, valueOffset, 1); -} - -``` +原子变量可看做更好的volatile类型变量。AtomicInteger在JDK8里面做了改动。 +![](https://img-blog.csdnimg.cn/0f94ab5e4b6045e5aa83d99cbc9c03c4.png) JDK7里面的实现如下: -```java -public final int getAndAdd(int delta) { - for(;;) { - intcurrent= get(); - intnext=current+delta; - if(compareAndSet(current,next)) - returncurrent; - } - } - -``` -Unsafe是经过特殊处理的,不能理解成常规的Java代码,区别在于: -- 1.8在调用getAndAddInt的时候,如果系统底层支持fetch-and-add,那么它执行的就是native方法,使用的是fetch-and-add -- 如果不支持,就按照上面的所看到的getAndAddInt方法体那样,以java代码的方式去执行,使用的是compare-and-swap +![](https://img-blog.csdnimg.cn/d2f94066894a4501b6dd5e6d9ad4a8c1.png) +Unsafe是经过特殊处理的,不能理解成常规的Java代码,1.8在调用getAndAddInt时,若系统底层: +- 支持fetch-and-add,则执行的就是native方法,使用fetch-and-add +- 不支持,就按照上面getAndAddInt那样,以Java代码方式执行,使用compare-and-swap 这也正好跟openjdk8中Unsafe::getAndAddInt上方的注释相吻合: -```java -// The following contain CAS-based Java implementations used on -// platforms not supporting native instructions -``` +以下包含在不支持本机指令的平台上使用的基于 CAS 的 Java 实现 +![](https://img-blog.csdnimg.cn/327bda8392cf4158ab94049e67f9b169.png) # 3 原子变量类 J.U.C的AtomicXXX。 @@ -164,18 +140,11 @@ public class CasNumberRange { } } } - ``` - - # 4 非阻塞算法 - Lock-free算法,可以实现栈、队列、优先队列或者散列表。 - ## 4.1 非阻塞的栈 - -Trebier算法,1986年提出的。 - +Trebier算法,1986年提出。 ```java public class ConcurrentStack { AtomicReference> top = new AtomicReference>(); @@ -210,13 +179,9 @@ Trebier算法,1986年提出的。 } } } - ``` - ## 4.2 非阻塞的链表 - -有点复杂哦,实际J.U.C的ConcurrentLinkedQueue也是参考了这个由Michael and Scott,1996年实现的算法。 - +J.U.C的ConcurrentLinkedQueue也是参考这个由Michael and Scott,1996年实现的算法。 ```java public class LinkedQueue { @@ -257,19 +222,14 @@ public class LinkedQueue { } } } - ``` - ## 4.3 原子域更新 - -AtomicReferenceFieldUpdater,一个基于反射的工具类,它能对指定类的指定的volatile引用字段进行原子更新。(注意这个字段不能是private的) +AtomicReferenceFieldUpdater,一个基于反射的工具类,能对指定类的指定的volatile引用字段进行原子更新。(该字段不能是private的) 通过调用AtomicReferenceFieldUpdater的静态方法newUpdater就能创建它的实例,该方法要接收三个参数: - * 包含该字段的对象的类 * 将被更新的对象的类 * 将被更新的字段的名称 - ```java AtomicReferenceFieldUpdater updater=AtomicReferenceFieldUpdater.newUpdater(Dog.class,String.class,"name"); Dog dog1=new Dog(); @@ -279,5 +239,4 @@ AtomicReferenceFieldUpdater updater=AtomicReferenceFieldUpdater.newUpdater(Dog.c class Dog { volatile String name="dog1"; } - ``` \ No newline at end of file diff --git "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\351\253\230\346\200\247\350\203\275\347\274\226\347\250\213\345\256\236\346\210\230 --- ThreadPoolExecutor-\347\272\277\347\250\213\346\261\240.md" "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\351\253\230\346\200\247\350\203\275\347\274\226\347\250\213\345\256\236\346\210\230 --- ThreadPoolExecutor-\347\272\277\347\250\213\346\261\240.md" new file mode 100644 index 000000000..e0cd2e89b --- /dev/null +++ "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\351\253\230\346\200\247\350\203\275\347\274\226\347\250\213\345\256\236\346\210\230 --- ThreadPoolExecutor-\347\272\277\347\250\213\346\261\240.md" @@ -0,0 +1,1272 @@ +# 1 为什么要用线程池 +## 1.1 线程the more, the better? +1、线程在java中是一个对象,更是操作系统的资源,线程创建、销毁都需要时间。 +如果创建时间+销毁时间>执行任务时间就很不合算。 +2、Java对象占用堆内存,操作系统线程占用系统内存,根据JVM规范,一个线程默认最大栈 +大小1M,这个栈空间是需要从系统内存中分配的。线程过多,会消耗很多的内存。 +3、操作系统需要频繁切换线程上下文(大家都想被运行),影响性能。 + +线程使应用能够更加充分合理地协调利用CPU、内存、网络、I/O等系统资源. +线程的创建需要开辟虚拟机栈、本地方法栈、程序计数器等线程私有的内存空间; +在线程销毁时需要回收这些系统资源. +频繁地创建和销毁线程会浪费大量的系统资源,增加并发编程风险. + +在服务器负载过大的时候,如何让新的线程等待或者友好地拒绝服务? + +这些都是线程自身无法解决的; +所以需要通过线程池协调多个线程,并实现类似主次线程隔离、定时执行、周期执行等任务. + +# 2 线程池的作用 +● 利用线程池管理并复用线程、控制最大并发数等 + +● 实现任务线程队列缓存策略和拒绝机制 + +● 实现某些与时间相关的功能 +如定时执行、周期执行等 + +● 隔离线程环境 +比如,交易服务和搜索服务在同一台服务器上,分别开启两个线程池,交易线程的资源消耗明显要大; +因此,通过配置独立的线程池,将较慢的交易服务与搜索服务隔离开,避免各服务线程相互影响. + +在开发中,合理地使用线程池能够带来3个好处 + - **降低资源消耗** 通过重复利用已创建的线程,降低创建和销毁线程造成的系统资源消耗 + - **提高响应速度** 当任务到达时,任务可以不需要等到线程创建就能立即执行 + - **提高线程的可管理性** 线程是稀缺资源,如果过多地创建,不仅会消耗系统资源,还会降低系统的稳定性,导致使用线程池可以进行统一分配、调优和监控。 + +# 3 概念 +1、**线程池管理器** +用于创建并管理线程池,包括创建线程池,销毁线程池,添加新任务; +2、**工作线程** +线程池中线程,在没有任务时处于等待状态,可以循环的执行任务; +3、**任务接口** +每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等; +4、**任务队列** +用于存放没有处理的任务。提供缓冲机制。. + +- 原理示意图 +![](https://img-blog.csdnimg.cn/20191009015833132.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) + +# 4 线程池API +## 4.1 接口定义和实现类 +![](https://img-blog.csdnimg.cn/2019100901595683.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +### 继承关系图 +![线程池相关类图](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyOTA5XzQ2ODU5NjgtZWFhYWY4ZmQ4ODQ5Nzc1Ny5wbmc?x-oss-process=image/format,png) +可以认为ScheduledThreadPoolExecutor是最丰富的实现类! + +## 4.2 方法定义 +### 4.2.1 ExecutorService +![](https://img-blog.csdnimg.cn/20191009020347726.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) + +### 4.2.2 ScheduledExecutorService +#### public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit); +![](https://img-blog.csdnimg.cn/20191013013014872.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +#### public ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit); +![](https://img-blog.csdnimg.cn/20191013013113751.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) + +#### 以上两种都是创建并执行一个一次性任务, 过了延迟时间就会被执行 +#### public ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit); + +![](https://img-blog.csdnimg.cn/20191013013412305.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +创建并执行一个周期性任务 +过了给定的初始延迟时间,会第一次被执行 +执行过程中发生了异常,那么任务就停止 + +一次任务 执行时长超过了周期时间,下一次任务会等到该次任务执行结束后,立刻执行,这也是它和`scheduleWithFixedDelay`的重要区别 + +#### public ScheduledFuture scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit); +创建并执行一个周期性任务 +过了初始延迟时间,第一次被执行,后续以给定的周期时间执行 +执行过程中发生了异常,那么任务就停止 + +一次任务执行时长超过了周期时间,下一 次任务 会在该次任务执 +行结束的时间基础上,计算执行延时。 +对于超过周期的长时间处理任务的不同处理方式,这是它和`scheduleAtFixedRate`的重要区别。 + +### 实例 +- 测试例子 +![](https://img-blog.csdnimg.cn/20191013153615841.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +- 测试实现 +![](https://img-blog.csdnimg.cn/20191013153730175.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +- 运行结果 +![](https://img-blog.csdnimg.cn/2019101315391641.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +可以看出超过core的线程都在等待,线程池线程数量为何达不到最大线程数呢?那这个参数还有什么意义, 让我们继续往下阅读吧! + + +### 4.2.2 Executors工具类 +你可以自己实例化线程池,也可以用`Executors`创建线程池的工厂类,常用方法如下: + +`ExecutorService` 的抽象类`AbstractExecutorService `提供了`submit`、`invokeAll` 等方法的实现; +但是核心方法`Executor.execute()`并没有在这里实现. +因为所有的任务都在该方法执行,不同实现会带来不同的执行策略. + +通过`Executors`的静态工厂方法可以创建三个线程池的包装对象 +- ForkJoinPool、 +- ThreadPoolExecutor +- ScheduledThreadPoolExecutor + +● Executors.newWorkStealingPool +JDK8 引入,创建持有足够线程的线程池支持给定的并行度; +并通过使用多个队列减少竞争; +构造方法中把CPU数量设置为默认的并行度. +返回`ForkJoinPool` ( JDK7引入)对象,它也是`AbstractExecutorService` 的子类 +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyOTA2XzQ2ODU5NjgtM2I0YThlOGMxNDA4Zjg5Mi5wbmc?x-oss-process=image/format,png) + + +● Executors.newCachedThreadPool +创建的是一个无界的缓冲线程池。它的任务队列是一个同步队列。 +任务加入到池中 +- 如果池中有空闲线程,则用空闲线程执行 +- 如无, 则创建新线程执行。 + +池中的线程空闲超过60秒,将被销毁。线程数随任务的多少变化。 +`适用于执行耗时较小的异步任务`。池的核心线程数=0 ,最大线程数= Integer.MAX_ _VALUE +`maximumPoolSize` 最大可以至`Integer.MAX_VALUE`,是高度可伸缩的线程池. +若达到该上限,相信没有服务器能够继续工作,直接OOM. +`keepAliveTime` 默认为60秒; +工作线程处于空闲状态,则回收工作线程; +如果任务数增加,再次创建出新线程处理任务. + +● Executors.newScheduledThreadPool +能定时执行任务的线程池。该池的核心线程数由参数指定,线程数最大至`Integer.MAX_ VALUE`,与上述相同,存在OOM风险. +`ScheduledExecutorService`接口的实现类,支持**定时及周期性任务执行**; +相比`Timer`,` ScheduledExecutorService` 更安全,功能更强大. +与`newCachedThreadPool`的区别是**不回收工作线程**. + +● Executors.newSingleThreadExecutor +创建一个单线程的线程池,相当于单线程串行执行所有任务,保证按任务的提交顺序依次执行. +只有-个线程来执行无界任务队列的单-线程池。该线程池确保任务按加入的顺序一个一 +个依次执行。当唯一的线程因任务 异常中止时,将创建一个新的线程来继续执行 后续的任务。 +与newFixedThreadPool(1)的区别在于,单线程池的池大小在`newSingleThreadExecutor`方法中硬编码,不能再改变的。 + + +● Executors.newFixedThreadPool +创建一个固定大小任务队列容量无界的线程池 +输入的参数即是固定线程数; +既是核心线程数也是最大线程数; +不存在空闲线程,所以`keepAliveTime`等于0. +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyODE5XzQ2ODU5NjgtOGNkOTFmM2M2ZWFkYTlkZS5wbmc?x-oss-process=image/format,png) +其中使用了 LinkedBlockingQueue, 但是没有设置上限!!!,堆积过多任务!!! + +下面介绍`LinkedBlockingQueue`的构造方法 +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyOTEwXzQ2ODU5NjgtZmNlMjYxZGJlMzBkZWY3MS5wbmc?x-oss-process=image/format,png) +使用这样的无界队列,如果瞬间请求非常大,会有OOM的风险; +除`newWorkStealingPool` 外,其他四个创建方式都存在资源耗尽的风险. + +不推荐使用其中的任何创建线程池的方法,因为都没有任何限制,存在安全隐患. + + `Executors`中默认的线程工厂和拒绝策略过于简单,通常对用户不够友好. +线程工厂需要做创建前的准备工作,对线程池创建的线程必须明确标识,就像药品的生产批号一样,为线程本身指定有意义的名称和相应的序列号. +拒绝策略应该考虑到业务场景,返回相应的提示或者友好地跳转. +以下为简单的ThreadFactory 示例 +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzk3XzQ2ODU5NjgtZDIwMjUyODdhODJhZGQ5NS5wbmc?x-oss-process=image/format,png) + +上述示例包括线程工厂和任务执行体的定义; +通过newThread方法快速、统一地创建线程任务,强调线程一定要有特定意义的名称,方便出错时回溯. + +- 单线程池:newSingleThreadExecutor()方法创建,五个参数分别是ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue())。含义是池中保持一个线程,最多也只有一个线程,也就是说这个线程池是顺序执行任务的,多余的任务就在队列中排队。 +- 固定线程池:newFixedThreadPool(nThreads)方法创建 +[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NZEi0e3y-1570557031347)(https://uploadfiles.nowcoder.com/images/20190625/5088755_1561474494512_5D0DD7BCB7171E9002EAD3AEF42149E6 "图片标题")] + +池中保持nThreads个线程,最多也只有nThreads个线程,多余的任务也在队列中排队。 +[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SId8FBO1-1570557031347)(https://uploadfiles.nowcoder.com/images/20190625/5088755_1561476084467_4A47A0DB6E60853DEDFCFDF08A5CA249 "图片标题")] + +[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6uzv6UAk-1570557031348)(https://uploadfiles.nowcoder.com/images/20190625/5088755_1561476102425_FB5C81ED3A220004B71069645F112867 "图片标题")] +线程数固定且线程不超时 +- 缓存线程池:newCachedThreadPool()创建,五个参数分别是ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue())。 +含义是池中不保持固定数量的线程,随需创建,最多可以创建Integer.MAX_VALUE个线程(说一句,这个数量已经大大超过目前任何操作系统允许的线程数了),空闲的线程最多保持60秒,多余的任务在SynchronousQueue(所有阻塞、并发队列在后续文章中具体介绍)中等待。 + +为什么单线程池和固定线程池使用的任务阻塞队列是LinkedBlockingQueue(),而缓存线程池使用的是SynchronousQueue()呢? +因为单线程池和固定线程池中,线程数量是有限的,因此提交的任务需要在LinkedBlockingQueue队列中等待空余的线程;而缓存线程池中,线程数量几乎无限(上限为Integer.MAX_VALUE),因此提交的任务只需要在SynchronousQueue队列中同步移交给空余线程即可。 + +- 单线程调度线程池:newSingleThreadScheduledExecutor()创建,五个参数分别是 (1, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue())。含义是池中保持1个线程,多余的任务在DelayedWorkQueue中等待。 +- 固定调度线程池:newScheduledThreadPool(n)创建,五个参数分别是 (n, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue())。含义是池中保持n个线程,多余的任务在DelayedWorkQueue中等待。 + +有一项技术可以缓解执行时间较长任务造成的影响,即限定任务等待资源的时间,而不要无限的等待 + +先看第一个例子,测试单线程池、固定线程池和缓存线程池(注意增加和取消注释): + +``` +public class ThreadPoolExam { + public static void main(String[] args) { + //first test for singleThreadPool + ExecutorService pool = Executors.newSingleThreadExecutor(); + //second test for fixedThreadPool +// ExecutorService pool = Executors.newFixedThreadPool(2); + //third test for cachedThreadPool +// ExecutorService pool = Executors.newCachedThreadPool(); + for (int i = 0; i < 5; i++) { + pool.execute(new TaskInPool(i)); + } + pool.shutdown(); + } +} + +class TaskInPool implements Runnable { + private final int id; + + TaskInPool(int id) { + this.id = id; + } + + @Override + public void run() { + try { + for (int i = 0; i < 5; i++) { + System.out.println("TaskInPool-["+id+"] is running phase-"+i); + TimeUnit.SECONDS.sleep(1); + } + System.out.println("TaskInPool-["+id+"] is over"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } +} +``` + +如图为排查底层公共缓存调用出错时的截图 +![有意义的线程命名](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzQ5XzQ2ODU5NjgtODU1MDI1MzM5MDZjMzNmMi5wbmc?x-oss-process=image/format,png) +绿色框采用自定义的线程工厂,明显比蓝色框默认的线程工厂创建的线程名称拥有更多的额外信息:如调用来源、线程的业务含义,有助于快速定位到死锁、StackOverflowError 等问题. + +# 5 创建线程池 +首先从`ThreadPoolExecutor`构造方法讲起,学习如何自定义`ThreadFactory`和`RejectedExecutionHandler`; +并编写一个最简单的线程池示例. +然后,通过分析`ThreadPoolExecutor`的`execute`和`addWorker`两个核心方法; +学习如何把任务线程加入到线程池中运行. + +- ThreadPoolExecutor 的构造方法如下 +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzMyXzQ2ODU5NjgtYTVmOTU1Yjc5MmJkNDUzZS5wbmc?x-oss-process=image/format,png) + +- 第1个参数: corePoolSize 表示常驻核心线程数 +如果等于0,则任务执行完之后,没有任何请求进入时销毁线程池的线程; +如果大于0,即使本地任务执行完毕,核心线程也不会被销毁. +这个值的设置非常关键; +设置过大会浪费资源; +设置过小会导致线程频繁地创建或销毁. + +- 第2个参数: maximumPoolSize 表示线程池能够容纳同时执行的最大线程数 +从第1处来看,必须>=1. +如果待执行的线程数大于此值,需要借助第5个参数的帮助,缓存在队列中. +如果`maximumPoolSize = corePoolSize`,即是固定大小线程池. + +- 第3个参数: keepAliveTime 表示线程池中的线程空闲时间 +当空闲时间达到`keepAliveTime`时,线程会被销毁,直到只剩下`corePoolSize`个线程; +避免浪费内存和句柄资源. +在默认情况下,当线程池的线程数大于`corePoolSize`时,`keepAliveTime`才起作用. +但是当`ThreadPoolExecutor`的`allowCoreThreadTimeOut = true`时,核心线程超时后也会被回收. + +- 第4个参数: TimeUnit表示时间单位 +keepAliveTime的时间单位通常是TimeUnit.SECONDS. + +- 第5个参数: workQueue 表示缓存队列 +当请求的线程数大于`maximumPoolSize`时,线程进入`BlockingQueue`. +后续示例代码中使用的LinkedBlockingQueue是单向链表,使用锁来控制入队和出队的原子性; +两个锁分别控制元素的添加和获取,是一个生产消费模型队列. + +- 第6个参数: threadFactory 表示线程工厂 +它用来生产一组相同任务的线程; +线程池的命名是通过给这个factory增加组名前缀来实现的. +在虚拟机栈分析时,就可以知道线程任务是由哪个线程工厂产生的. + +- 第7个参数: handler 表示执行拒绝策略的对象 +当超过第5个参数`workQueue`的任务缓存区上限的时候,就可以通过该策略处理请求,这是一种简单的限流保护. +友好的拒绝策略可以是如下三种: +(1 ) 保存到数据库进行削峰填谷;在空闲时再提取出来执行 +(2)转向某个提示页面 +(3)打印日志 + +### 2.1.1 corePoolSize(核心线程数量) +线程池中应该保持的主要线程的数量.即使线程处于空闲状态,除非设置了`allowCoreThreadTimeOut`这个参数,当提交一个任务到线程池时,若线程数量Integer 有32位; +最右边29位表工作线程数; +最左边3位表示线程池状态,可表示从0至7的8个不同数值 +线程池的状态用高3位表示,其中包括了符号位. +五种状态的十进制值按从小到大依次排序为 +RUNNING < SHUTDOWN < STOP < TIDYING =核心线程数 或线程创建失败,则将当前任务放到工作队列中 + // 只有线程池处于 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); + // 核心线程和队列都已满,尝试创建一个新线程 + } + else if (!addWorker(command, false)) + // 抛出RejectedExecutionException异常 + // 若 addWorker 返回是 false,即创建失败,则唤醒拒绝策略. + reject(command); + } +``` +发生拒绝的理由有两个 +( 1 )线程池状态为非RUNNING状态 +(2)等待队列已满。 + +下面继续分析`addWorker` + +## addWorker 源码解析 + +根据当前线程池状态,检查是否可以添加新的任务线程,若可以则创建并启动任务; +若一切正常则返回true; +返回false的可能性如下 +1. 线程池没有处于`RUNNING`态 +2. 线程工厂创建新的任务线程失败 +### 参数 +- firstTask +外部启动线程池时需要构造的第一个线程,它是线程的母体 +- core +新增工作线程时的判断指标 + - true +需要判断当前`RUNNING`态的线程是否少于`corePoolsize` + - false +需要判断当前`RUNNING`态的线程是否少于`maximumPoolsize` +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUzMjIwXzQ2ODU5NjgtMjg2MDRmYjVkYTE5MjJlNC5wbmc?x-oss-process=image/format,png) +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzg5XzQ2ODU5NjgtOTk1ZmFlOTQyOTQwMjFjNy5wbmc?x-oss-process=image/format,png) +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyODMwXzQ2ODU5NjgtM2Y3NzViOWQ1MThmMzc4My5wbmc?x-oss-process=image/format,png) + +这段代码晦涩难懂,部分地方甚至违反代码规约,但其中蕴含丰富的编码知识点 + +- 第1处,配合循环语句出现的label,类似于goto 作用 +label 定义时,必须把标签和冒号的组合语句紧紧相邻定义在循环体之前,否则会编译出错. +目的是 在实现多重循环时能够快速退出到任何一层; +出发点似乎非常贴心,但在大型软件项目中,滥用标签行跳转的后果将是灾难性的. +示例代码中在`retry`下方有两个无限循环; +在`workerCount`加1成功后,直接退出两层循环. + +- 第2处,这样的表达式不利于阅读,应如是 +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzg1XzQ2ODU5NjgtMDg2ZTlkNWY5ZGEyYWZkNC5wbmc?x-oss-process=image/format,png) + +- 第3处,与第1处的标签呼应,`AtomicInteger`对象的加1操作是原子性的; +`break retry`表 直接跳出与`retry` 相邻的这个循环体 + +- 第4处,此`continue`跳转至标签处,继续执行循环. +如果条件为false,则说明线程池还处于运行状态,即继续在`for(;)`循环内执行. + +- 第5处,`compareAndIncrementWorkerCount `方法执行失败的概率非常低. +即使失败,再次执行时成功的概率也是极高的,类似于自旋原理. +这里是先加1,创建失败再减1,这是轻量处理并发创建线程的方式; +如果先创建线程,成功再加1,当发现超出限制后再销毁线程,那么这样的处理方式明显比前者代价要大. + +- 第6处,`Worker `对象是工作线程的核心类实现,部分源码如下 +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUzMjMyXzQ2ODU5NjgtYzkzNTI3ODJjNTZjM2Q2Ny5wbmc?x-oss-process=image/format,png) +它实现了`Runnable`接口,并把本对象作为参数输入给`run()`中的`runWorker (this)`; +所以内部属性线程`thread`在`start`的时候,即会调用`runWorker`. + +# 总结 +线程池的相关源码比较精炼,还包括线程池的销毁、任务提取和消费等,与线程状态图一样,线程池也有自己独立的状态转化流程,本节不再展开。 +总结一下,使用线程池要注意如下几点: +(1)合理设置各类参数,应根据实际业务场景来设置合理的工作线程数。 +(2)线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 +(3)创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。 + +线程池不允许使用Executors,而是通过ThreadPoolExecutor的方式创建,这样的处理方式能更加明确线程池的运行规则,规避资源耗尽的风险。 + + + + + +进一步查看源码发现,这些方法最终都调用了ThreadPoolExecutor和ScheduledThreadPoolExecutor的构造函数 +而ScheduledThreadPoolExecutor继承自ThreadPoolExecutor + +## 0.2 ThreadPoolExecutor 自定义线程池 +[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5A6eRvc8-1570557031390)(https://uploadfiles.nowcoder.com/images/20190625/5088755_1561476436402_10FB15C77258A991B0028080A64FB42D "图片标题")] +它们都是某种线程池,可以控制线程创建,释放,并通过某种策略尝试复用线程去执行任务的一个管理框架 + +,因此最终所有线程池的构造函数都调用了Java5后推出的ThreadPoolExecutor的如下构造函数 +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyODEwXzQ2ODU5NjgtYmY0MTAwOTU5Nzk4NjA1OC5wbmc?x-oss-process=image/format,png) + +## Java默认提供的线程池 +Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池 +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUzMDc1XzQ2ODU5NjgtNGYxOGI1ZTk2ZWIxZDkzMC5wbmc?x-oss-process=image/format,png) +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyOTg5XzQ2ODU5NjgtYjdlYzU5YTgwMDQ0MmIyNi5wbmc?x-oss-process=image/format,png) + +我们只需要将待执行的方法放入 run 方法中,将 Runnable 接口的实现类交给线程池的 +execute 方法,作为他的一个参数,比如: +```java +Executor e=Executors.newSingleThreadExecutor(); +e.execute(new Runnable(){ //匿名内部类 public void run(){ +//需要执行的任务 +} +}); + +``` +# 线程池原理 - 任务execute过程 + - 流程图 +![](https://img-blog.csdnimg.cn/20191014020916959.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +- 示意图 +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzMwXzQ2ODU5NjgtYTA3YjhiMzIzMzMxYzE1ZS5wbmc?x-oss-process=image/format,png) + +ThreadPoolExecutor执行execute()分4种情况 + - 若当前运行的线程少于`corePoolSize`,则创建新线程来执行任务(该步需要获取全局锁) + - 若运行的线程多于或等于`corePoolSize`,且工作队列没满,则将新提交的任务存储在工作队列里。即, 将任务加入`BlockingQueue` + - 若无法将任务加入`BlockingQueue`,且没达到线程池最大数量, 则创建新的线程来处理任务(该步需要获取全局锁) + - 若创建新线程将使当前运行的线程超出`maximumPoolSize`,任务将被拒绝,并调用`RejectedExecutionHandler.rejectedExecution()` + +采取上述思路,是为了在执行`execute()`时,尽可能避免获取全局锁 +在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁 + +## 实例 +![](https://img-blog.csdnimg.cn/20191013231005279.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +- 结果 +![](https://img-blog.csdnimg.cn/20191014015653362.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +# **源码分析** +``` + /** + * 检查是否可以根据当前池状态和给定的边界(核心或最大) + * 添加新工作线程。如果是这样,工作线程数量会相应调整,如果可能的话,一个新的工作线程创建并启动 + * 将firstTask作为其运行的第一项任务。 + * 如果池已停止此方法返回false + * 如果线程工厂在被访问时未能创建线程,也返回false + * 如果线程创建失败,或者是由于线程工厂返回null,或者由于异常(通常是在调用Thread.start()后的OOM)),我们干净地回滚。 + * + * @param core if true use corePoolSize as bound, else + * maximumPoolSize. (A boolean indicator is used here rather than a + * value to ensure reads of fresh values after checking other pool + * state). + * @return true if successful + */ + private boolean addWorker(Runnable firstTask, boolean core) { + retry: + for (;;) { + int c = ctl.get(); + int rs = runStateOf(c); + + + /** + * Check if queue empty only if necessary. + * + * 如果线程池已关闭,并满足以下条件之一,那么不创建新的 worker: + * 1. 线程池状态大于 SHUTDOWN,也就是 STOP, TIDYING, 或 TERMINATED + * 2. firstTask != null + * 3. workQueue.isEmpty() + * 简单分析下: + * 状态控制的问题,当线程池处于 SHUTDOWN ,不允许提交任务,但是已有任务继续执行 + * 当状态大于 SHUTDOWN ,不允许提交任务,且中断正在执行任务 + * 多说一句:若线程池处于 SHUTDOWN,但 firstTask 为 null,且 workQueue 非空,是允许创建 worker 的 + * + */ + if (rs >= SHUTDOWN && + ! (rs == SHUTDOWN && + firstTask == null && + ! workQueue.isEmpty())) + return false; + + for (;;) { + int wc = workerCountOf(c); + if (wc >= CAPACITY || + wc >= (core ? corePoolSize : maximumPoolSize)) + return false; + // 如果成功,那么就是所有创建线程前的条件校验都满足了,准备创建线程执行任务 + // 这里失败的话,说明有其他线程也在尝试往线程池中创建线程 + if (compareAndIncrementWorkerCount(c)) + break retry; + // 由于有并发,重新再读取一下 ctl + c = ctl.get(); // Re-read ctl + // 正常如果是 CAS 失败的话,进到下一个里层的for循环就可以了 + // 可如果是因为其他线程的操作,导致线程池的状态发生了变更,如有其他线程关闭了这个线程池 + // 那么需要回到外层的for循环 + if (runStateOf(c) != rs) + continue retry; + // else CAS failed due to workerCount change; retry inner loop + } + } + + /* * + * 到这里,我们认为在当前这个时刻,可以开始创建线程来执行任务 + */ + + // worker 是否已经启动 + boolean workerStarted = false; + // 是否已将这个 worker 添加到 workers 这个 HashSet 中 + boolean workerAdded = false; + Worker w = null; + try { + // 把 firstTask 传给 worker 的构造方法 + w = new Worker(firstTask); + // 取 worker 中的线程对象,Worker的构造方法会调用 ThreadFactory 来创建一个新的线程 + final Thread t = w.thread; + if (t != null) { + //先加锁 + final ReentrantLock mainLock = this.mainLock; + // 这个是整个类的全局锁,持有这个锁才能让下面的操作“顺理成章”, + // 因为关闭一个线程池需要这个锁,至少我持有锁的期间,线程池不会被关闭 + mainLock.lock(); + try { + // Recheck while holding lock. + // Back out on ThreadFactory failure or if + // shut down before lock acquired. + int rs = runStateOf(ctl.get()); + + // 小于 SHUTTDOWN 即 RUNNING + // 如果等于 SHUTDOWN,不接受新的任务,但是会继续执行等待队列中的任务 + if (rs < SHUTDOWN || + (rs == SHUTDOWN && firstTask == null)) { + // worker 里面的 thread 不能是已启动的 + if (t.isAlive()) // precheck that t is startable + throw new IllegalThreadStateException(); + // 加到 workers 这个 HashSet 中 + workers.add(w); + int s = workers.size(); + if (s > largestPoolSize) + largestPoolSize = s; + workerAdded = true; + } + } finally { + mainLock.unlock(); + } + // 若添加成功 + if (workerAdded) { + // 启动线程 + t.start(); + workerStarted = true; + } + } + } finally { + // 若线程没有启动,做一些清理工作,若前面 workCount 加了 1,将其减掉 + if (! workerStarted) + addWorkerFailed(w); + } + // 返回线程是否启动成功 + return workerStarted; + } +``` +看下 `addWorkFailed` +![workers 中删除掉相应的 worker,workCount 减 1 +private void addWor](https://upload-images.jianshu.io/upload_images/4685968-77abdc7bff21cca6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +![记录 workers 中的个数的最大值,因为 workers 是不断增加减少的,通过这个值可以知道线程池的大小曾经达到的最大值](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzA4XzQ2ODU5NjgtMDc4NDcyYjY4MmZjYzljZC5wbmc?x-oss-process=image/format,png) +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUzNjMzXzQ2ODU5NjgtMzNmNTE0NTc3ZTk3ZGMzNS5wbmc?x-oss-process=image/format,png) + + + + +`worker` 中的线程 `start` 后,其 `run` 方法会调用 `runWorker ` +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyODgwXzQ2ODU5NjgtYTAwOWJjMDJhMjI0ZGNlMi5wbmc?x-oss-process=image/format,png) +继续往下看 `runWorker` +``` +// worker 线程启动后调用,while 循环(即自旋!)不断从等待队列获取任务并执行 +// worker 初始化时,可指定 firstTask,那么第一个任务也就可以不需要从队列中获取 +final void runWorker(Worker w) { + Thread wt = Thread.currentThread(); + // 该线程的第一个任务(若有) + Runnable task = w.firstTask; + w.firstTask = null; + // 允许中断 + w.unlock(); + + boolean completedAbruptly = true; + try { + // 循环调用 getTask 获取任务 + while (task != null || (task = getTask()) != null) { + w.lock(); + // 若线程池状态大于等于 STOP,那么意味着该线程也要中断 + /** + * 若线程池STOP,请确保线程 已被中断 + * 如果没有,请确保线程未被中断 + * 这需要在第二种情况下进行重新检查,以便在关中断时处理shutdownNow竞争 + */ + if ((runStateAtLeast(ctl.get(), STOP) || + (Thread.interrupted() && + runStateAtLeast(ctl.get(), STOP))) && + !wt.isInterrupted()) + wt.interrupt(); + try { + // 这是一个钩子方法,留给需要的子类实现 + beforeExecute(wt, task); + Throwable thrown = null; + try { + // 到这里终于可以执行任务了 + task.run(); + } catch (RuntimeException x) { + thrown = x; throw x; + } catch (Error x) { + thrown = x; throw x; + } catch (Throwable x) { + // 这里不允许抛出 Throwable,所以转换为 Error + thrown = x; throw new Error(x); + } finally { + // 也是一个钩子方法,将 task 和异常作为参数,留给需要的子类实现 + afterExecute(task, thrown); + } + } finally { + // 置空 task,准备 getTask 下一个任务 + task = null; + // 累加完成的任务数 + w.completedTasks++; + // 释放掉 worker 的独占锁 + w.unlock(); + } + } + completedAbruptly = false; + } finally { + // 到这里,需要执行线程关闭 + // 1. 说明 getTask 返回 null,也就是说,这个 worker 的使命结束了,执行关闭 + // 2. 任务执行过程中发生了异常 + // 第一种情况,已经在代码处理了将 workCount 减 1,这个在 getTask 方法分析中说 + // 第二种情况,workCount 没有进行处理,所以需要在 processWorkerExit 中处理 + processWorkerExit(w, completedAbruptly); + } +} +``` +看看 `getTask() ` +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzgwXzQ2ODU5NjgtNWU5NDc3MzE5M2Q5Y2Y0OS5wbmc?x-oss-process=image/format,png) +``` +// 此方法有三种可能 +// 1. 阻塞直到获取到任务返回。默认 corePoolSize 之内的线程是不会被回收的,它们会一直等待任务 +// 2. 超时退出。keepAliveTime 起作用的时候,也就是如果这么多时间内都没有任务,那么应该执行关闭 +// 3. 如果发生了以下条件,须返回 null +// 池中有大于 maximumPoolSize 个 workers 存在(通过调用 setMaximumPoolSize 进行设置) +// 线程池处于 SHUTDOWN,而且 workQueue 是空的,前面说了,这种不再接受新的任务 +// 线程池处于 STOP,不仅不接受新的线程,连 workQueue 中的线程也不再执行 +private Runnable getTask() { + boolean timedOut = false; // Did the last poll() time out? + + for (;;) { + // 允许核心线程数内的线程回收,或当前线程数超过了核心线程数,那么有可能发生超时关闭 + + // 这里 break,是为了不往下执行后一个 if (compareAndDecrementWorkerCount(c)) + // 两个 if 一起看:如果当前线程数 wc > maximumPoolSize,或者超时,都返回 null + // 那这里的问题来了,wc > maximumPoolSize 的情况,为什么要返回 null? + // 换句话说,返回 null 意味着关闭线程。 + // 那是因为有可能开发者调用了 setMaximumPoolSize 将线程池的 maximumPoolSize 调小了 + + // 如果此 worker 发生了中断,采取的方案是重试 + // 解释下为什么会发生中断,这个读者要去看 setMaximumPoolSize 方法, + // 如果开发者将 maximumPoolSize 调小了,导致其小于当前的 workers 数量, + // 那么意味着超出的部分线程要被关闭。重新进入 for 循环,自然会有部分线程会返回 null + int c = ctl.get(); + int rs = runStateOf(c); + + // Check if queue empty only if necessary. + if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { + // CAS 操作,减少工作线程数 + decrementWorkerCount(); + return null; + } + + int wc = workerCountOf(c); + + // Are workers subject to culling? + boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; + + 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) { + // 如果此 worker 发生了中断,采取的方案是重试 + // 解释下为什么会发生中断,这个读者要去看 setMaximumPoolSize 方法, + // 如果开发者将 maximumPoolSize 调小了,导致其小于当前的 workers 数量, + // 那么意味着超出的部分线程要被关闭。重新进入 for 循环,自然会有部分线程会返回 null + timedOut = false; + } + } +} +``` +到这里,基本上也说完了整个流程,回到 execute(Runnable command) 方法,看看各个分支,我把代码贴过来一下: +``` +/** + * Executes the given task sometime in the future. The task + * may execute in a new thread or in an existing pooled thread. + * + * If the task cannot be submitted for execution, either because this + * executor has been shutdown or because its capacity has been reached, + * the task is handled by the current {@code RejectedExecutionHandler}. + * + * @param command the task to execute + * @throws RejectedExecutionException at discretion of + * {@code RejectedExecutionHandler}, if the task + * cannot be accepted for execution + * @throws NullPointerException if {@code command} is null + */ + public void execute(Runnable command) { + if (command == null) + throw new NullPointerException(); + /* + * Proceed in 3 steps: + * + * 1. If fewer than corePoolSize threads are running, try to + * start a new thread with the given command as its first + * task. The call to addWorker atomically checks runState and + * workerCount, and so prevents false alarms that would add + * threads when it shouldn't, by returning false. + * + * 2. If a task can be successfully queued, then we still need + * to double-check whether we should have added a thread + * (because existing ones died since last checking) or that + * the pool shut down since entry into this method. So we + * recheck state and if necessary roll back the enqueuing if + * stopped, or start a new thread if there are none. + * + * 3. If we cannot queue task, then we try to add a new + * thread. If it fails, we know we are shut down or saturated + * and so reject the task. + */ + //表示 “线程池状态” 和 “线程数” 的整数 + int c = ctl.get(); + // 如果当前线程数少于核心线程数,直接添加一个 worker 执行任务, + // 创建一个新的线程,并把当前任务 command 作为这个线程的第一个任务(firstTask) + if (workerCountOf(c) < corePoolSize) { + // 添加任务成功,即结束 + // 执行的结果,会包装到 FutureTask + // 返回 false 代表线程池不允许提交任务 + if (addWorker(command, true)) + return; + + c = ctl.get(); + } + + // 到这说明,要么当前线程数大于等于核心线程数,要么刚刚 addWorker 失败 + + // 如果线程池处于 RUNNING ,把这个任务添加到任务队列 workQueue 中 + if (isRunning(c) && workQueue.offer(command)) { + /* 若任务进入 workQueue,我们是否需要开启新的线程 + * 线程数在 [0, corePoolSize) 是无条件开启新线程的 + * 若线程数已经大于等于 corePoolSize,则将任务添加到队列中,然后进到这里 + */ + int recheck = ctl.get(); + // 若线程池不处于 RUNNING ,则移除已经入队的这个任务,并且执行拒绝策略 + if (! isRunning(recheck) && remove(command)) + reject(command); + // 若线程池还是 RUNNING ,且线程数为 0,则开启新的线程 + // 这块代码的真正意图:担心任务提交到队列中了,但是线程都关闭了 + else if (workerCountOf(recheck) == 0) + addWorker(null, false); + } + // 若 workQueue 满,到该分支 + // 以 maximumPoolSize 为界创建新 worker, + // 若失败,说明当前线程数已经达到 maximumPoolSize,执行拒绝策略 + else if (!addWorker(command, false)) + reject(command); + } +``` +**工作线程**:线程池创建线程时,会将线程封装成工作线程Worker,Worker在执行完任务后,还会循环获取工作队列里的任务来执行.我们可以从Worker类的run()方法里看到这点 + +``` + public void run() { + try { + Runnable task = firstTask; + firstTask = null; + while (task != null || (task = getTask()) != null) { + runTask(task); + task = null; + } + } finally { + workerDone(this); + } + } + boolean workerStarted = false; + boolean workerAdded = false; + Worker w = null; + try { + w = new Worker(firstTask); + + final Thread t = w.thread; + if (t != null) { + //先加锁 + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + // Recheck while holding lock. + // Back out on ThreadFactory failure or if + // shut down before lock acquired. + int rs = runStateOf(ctl.get()); + + if (rs < SHUTDOWN || + (rs == SHUTDOWN && firstTask == null)) { + if (t.isAlive()) // precheck that t is startable + throw new IllegalThreadStateException(); + workers.add(w); + int s = workers.size(); + if (s > largestPoolSize) + largestPoolSize = s; + workerAdded = true; + } + } finally { + mainLock.unlock(); + } + if (workerAdded) { + t.start(); + workerStarted = true; + } + } + } finally { + if (! workerStarted) + addWorkerFailed(w); + } + return workerStarted; + } +``` +线程池中的线程执行任务分两种情况 + - 在execute()方法中创建一个线程时,会让这个线程执行当前任务 + - 这个线程执行完上图中 1 的任务后,会反复从BlockingQueue获取任务来执行 + +# 线程池的使用 + +## 向线程池提交任务 + 可以使用两个方法向线程池提交任务 +### execute() +用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功.通过以下代码可知execute()方法输入的任务是一个Runnable类的实例. +``` + threadsPool.execute(new Runnable() { + @Override + public void run() { + // TODO Auto-generated method stub + } + }); +``` +从运行结果可以看出,单线程池中的线程是顺序执行的。固定线程池(参数为2)中,永远最多只有两个线程并发执行。缓存线程池中,所有线程都并发执行。 +第二个例子,测试单线程调度线程池和固定调度线程池。 + +``` +public class ScheduledThreadPoolExam { + public static void main(String[] args) { + //first test for singleThreadScheduledPool + ScheduledExecutorService scheduledPool = Executors.newSingleThreadScheduledExecutor(); + //second test for scheduledThreadPool +// ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(2); + for (int i = 0; i < 5; i++) { + scheduledPool.schedule(new TaskInScheduledPool(i), 0, TimeUnit.SECONDS); + } + scheduledPool.shutdown(); + } +} + +class TaskInScheduledPool implements Runnable { + private final int id; + + TaskInScheduledPool(int id) { + this.id = id; + } + + @Override + public void run() { + try { + for (int i = 0; i < 5; i++) { + System.out.println("TaskInScheduledPool-["+id+"] is running phase-"+i); + TimeUnit.SECONDS.sleep(1); + } + System.out.println("TaskInScheduledPool-["+id+"] is over"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } +} +``` +从运行结果可以看出,单线程调度线程池和单线程池类似,而固定调度线程池和固定线程池类似。 +总结: + +- 如果没有特殊要求,使用缓存线程池总是合适的; +- 如果只能运行一个线程,就使用单线程池。 +- 如果要运行调度任务,则按需使用调度线程池或单线程调度线程池 +- 如果有其他特殊要求,则可以直接使用ThreadPoolExecutor类的构造函数来创建线程池,并自己给定那五个参数。 + +### submit() +用于提交需要返回值的任务.线程池会返回一个future类型对象,通过此对象可以判断任务是否执行成功 +并可通过get()获取返回值,get()会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候可能任务没有执行完. + +``` + Future future = executor.submit(harReturnValuetask); + try { + Object s = future.get(); + } catch (InterruptedException e) { + // 处理中断异常 + } catch (ExecutionException e) { + // 处理无法执行任务异常 + } finally { + // 关闭线程池 + executor.shutdown(); + } +``` +## 关闭线程池 +可通过调用线程池的**shutdown**或**shutdownNow**方法来关闭线程池. +它们的原理是遍历线程池中的工作线程,然后逐个调用线程的**interrupt**方法来中断线程,所以无法响应中断的任务可能永远无法终止. +但是它们存在一定的区别 + + - **shutdownNow**首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表 + - **shutdown**只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程. + +只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true. +当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true. +至于应该调用哪一种方法,应该由提交到线程池的任务的特性决定,通常调用shutdown方法来关闭线程池,若任务不一定要执行完,则可以调用shutdownNow方法. + +## 合理配置 + +要想合理地配置线程池,就必须首先 + +### 分析任务特性 + +可从以下几个角度来分析 + - 任务的性质:CPU密集型任务、IO密集型任务和混合型任务 + - 任务的优先级:高、中和低 + - 任务的执行时间:长、中和短 + - 任务的依赖性:是否依赖其他系统资源,如数据库连接。 + +### 任务性质 +可用不同规模的线程池分开处理 + +#### CPU密集型任务(计算型任务) +应配置尽可能小的线程,配置 + ` N(CPU)+1 `或 `N(CPU) * 2` + +#### I/O密集型任务 +相对比计算型任务,需多一些线程,根据具体 I/O 阻塞时长考量 + +> 如Tomcat中默认最大线程数: 200。 + +也可考虑根据需要在一个最小数量和最大数量间自动增减线程数。 + +业务读取较多,线程并不是一直在执行任务,则应配置尽可能多的线程 +`N(CPU)/1 - 阻塞系数(0.8~0.9)` + +一般,生产环境下,CPU使用率达到80,说明被充分利用 + +#### 混合型的任务 +如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量.如果这两个任务执行时间相差太大,则没必要进行分解. + +可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数. + +优先级不同的任务可以使用PriorityBlockingQueue处理.它可以让优先级高 +的任务先执行. + +> 注意 如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行 + +执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行. + +依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU. + +**建议使用有界队列** 有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点,比如几千. +假如系统里后台任务线程池的队列和线程池全满了,不断抛出抛弃任务的异常,通过排查发现是数据库出现了问题,导致执行SQL变得非常缓慢,因为后台任务线程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻塞,任务积压在线程池里. +如果我们设置成无界队列,那么线程池的队列就会越来越多,有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题. +## 2.5 线程池的监控 +如果在系统中大量使用线程池,则有必要对线程池进行监控,方便在出现问题时,可以根据线程池的使用状况快速定位问题.可通过线程池提供的参数进行监控,在监控线程池的时候可以使用以下属性: + + - taskCount:线程池需要执行的任务数量 + - completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。 + - largestPoolSize:线程池里曾经创建过的最大线程数量.通过这个数据可以知道线程池是否曾经满过.如该数值等于线程池的最大大小,则表示线程池曾经满过. + - getPoolSize:线程池的线程数量.如果线程池不销毁的话,线程池里的线程不会自动销毁,所以这个大小只增不减. + - getActiveCount:获取活动的线程数. + +通过扩展线程池进行监控.可以通过继承线程池来自定义线程池,重写线程池的 +beforeExecute、afterExecute和terminated方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控.例如,监控任务的平均执行时间、最大执行时间和最小执行时间等. +这几个方法在线程池里是空方法. + +``` +protected void beforeExecute(Thread t, Runnable r) { } +``` +## 2.6 线程池的状态 +1.当线程池创建后,初始为 running 状态 +2.调用 shutdown 方法后,处 shutdown 状态,此时不再接受新的任务,等待已有的任务执行完毕 +3.调用 shutdownnow 方法后,进入 stop 状态,不再接受新的任务,并且会尝试终止正在执行的任务。 +4.当处于 shotdown 或 stop 状态,并且所有工作线程已经销毁,任务缓存队列已清空,线程池被设为 terminated 状态。 + +# 总结 +## java 线程池有哪些关键属性? +- corePoolSize 到 maximumPoolSize 之间的线程会被回收,当然 corePoolSize 的线程也可以通过设置而得到回收(allowCoreThreadTimeOut(true))。 +- workQueue 用于存放任务,添加任务的时候,如果当前线程数超过了 corePoolSize,那么往该队列中插入任务,线程池中的线程会负责到队列中拉取任务。 +- keepAliveTime 用于设置空闲时间,如果线程数超出了 corePoolSize,并且有些线程的空闲时间超过了这个值,会执行关闭这些线程的操作 +- rejectedExecutionHandler 用于处理当线程池不能执行此任务时的情况,默认有抛出 RejectedExecutionException 异常、忽略任务、使用提交任务的线程来执行此任务和将队列中等待最久的任务删除,然后提交此任务这四种策略,默认为抛出异常。 +##线程池中的线程创建时机? +- 如果当前线程数少于 corePoolSize,那么提交任务的时候创建一个新的线程,并由这个线程执行这个任务; +- 如果当前线程数已经达到 corePoolSize,那么将提交的任务添加到队列中,等待线程池中的线程去队列中取任务; +- 如果队列已满,那么创建新的线程来执行任务,需要保证池中的线程数不会超过 maximumPoolSize,如果此时线程数超过了 maximumPoolSize,那么执行拒绝策略。 + +## 任务执行过程中发生异常怎么处理? +如果某个任务执行出现异常,那么执行任务的线程会被关闭,而不是继续接收其他任务。然后会启动一个新的线程来代替它。 + +## 什么时候会执行拒绝策略? +- workers 的数量达到了 corePoolSize,任务入队成功,以此同时线程池被关闭了,而且关闭线程池并没有将这个任务出队,那么执行拒绝策略。这里说的是非常边界的问题,入队和关闭线程池并发执行,读者仔细看看 execute 方法是怎么进到第一个 reject(command) 里面的。 +- workers 的数量大于等于 corePoolSize,准备入队,可是队列满了,任务入队失败,那么准备开启新的线程,可是线程数已经达到 maximumPoolSize,那么执行拒绝策略。 + +# 参考 +- 《码出高效》 + +- 《Java并发编程的艺术》 \ No newline at end of file diff --git "a/TODO/uml/redis\344\274\230\345\214\226.xmind" "b/TODO/uml/redis\344\274\230\345\214\226.xmind" new file mode 100644 index 000000000..f802a789d Binary files /dev/null and "b/TODO/uml/redis\344\274\230\345\214\226.xmind" differ diff --git "a/\346\236\266\346\236\204/\351\253\230\345\217\257\347\224\250/\351\253\230\345\217\257\347\224\250\347\232\204\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241-\350\265\204\346\272\220\351\232\224\347\246\273\343\200\201\351\231\220\346\265\201\343\200\201\347\206\224\346\226\255\343\200\201\351\231\215\347\272\247\343\200\201\347\233\221\346\216\247.md" "b/\346\236\266\346\236\204/\351\253\230\345\217\257\347\224\250/\351\253\230\345\217\257\347\224\250\347\232\204\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241-\350\265\204\346\272\220\351\232\224\347\246\273\343\200\201\351\231\220\346\265\201\343\200\201\347\206\224\346\226\255\343\200\201\351\231\215\347\272\247\343\200\201\347\233\221\346\216\247.md" index 877ad0a5c..a475fdcd3 100644 --- "a/\346\236\266\346\236\204/\351\253\230\345\217\257\347\224\250/\351\253\230\345\217\257\347\224\250\347\232\204\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241-\350\265\204\346\272\220\351\232\224\347\246\273\343\200\201\351\231\220\346\265\201\343\200\201\347\206\224\346\226\255\343\200\201\351\231\215\347\272\247\343\200\201\347\233\221\346\216\247.md" +++ "b/\346\236\266\346\236\204/\351\253\230\345\217\257\347\224\250/\351\253\230\345\217\257\347\224\250\347\232\204\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241-\350\265\204\346\272\220\351\232\224\347\246\273\343\200\201\351\231\220\346\265\201\343\200\201\347\206\224\346\226\255\343\200\201\351\231\215\347\272\247\343\200\201\347\233\221\346\216\247.md" @@ -3,22 +3,28 @@ ![](https://img-blog.csdnimg.cn/20200727001339292.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) # 舱壁隔离模式 # 容错理念 -- 凡是依赖,都可能失败 -- 凡是资源,都有限制 +- 凡是依赖都可能会失败 +- 凡是资源都有限制 - CPU/Memory/Threads/Queue -- 网络并不可靠,延迟是应用稳定性的杀手 +- 网络并不可靠,延迟是应用稳定性杀手 + + + # 1 资源隔离 -系统里,某块故障时,不会耗尽系统所有资源(如线程资源)。 +让你的系统里,某一块东西,在故障的情况下,不会耗尽系统所有的资源,比如线程资源 -项目中的一个case,有一块东西,要用多线程做一些事,开发同学不留神,资源隔离那块,在遇到故障时,每个线程跑时,因为那个bug,直接死循环,导致那块东西启动了大量线程,每个线程都死循环。 -最终导致系统资源耗尽,崩溃,不工作,不可用,废了。 +项目中的一个case,有一块东西,是要用多线程做一些事情,小伙伴做项目的时候,没有太留神,资源隔离,那块代码,在遇到一些故障的情况下,每个线程在跑的时候,因为那个bug,直接就死循环了,导致那块东西启动了大量的线程,每个线程都死循环 + +最终导致系统资源耗尽,崩溃,不工作,不可用,废掉了 + +资源隔离,那一块代码,最多最多就是用掉10个线程,不能再多了,就废掉了,限定好的一些资源 -资源隔离,就是确保那块代码,最多只能用掉10个线程,不能再多。 # 2 限流 -高并发的流量涌入进来,比如突然间100万QPS,系统废了。应该10万QPS进入系统,其他90万QPS被拒绝了。 +高并发的流量涌入进来,比如说突然间一秒钟100万QPS,废掉了,10万QPS进入系统,其他90万QPS被拒绝了 + # 3 熔断 -A服务调用B服务的某个功能,由于网络不稳定或B服务宕机,导致功能时间超长。 -若这样的次数太多。我们就可以直接将B断路(A不再请求B接口),凡是 +A服务调用B服务的某个功能,由于网络不稳定问题,或者B服务卡机,导致功能时 +间超长。如果这样的次数太多。我们就可以直接将B断路(A不再请求B接口),凡是 调用B的直接返回降级数据,不必等待B的超长执行。这样B的故障问题,就不会级联影 响到A。 diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\347\255\226\347\225\245\346\250\241\345\274\217(Strategy-Pattern).md" "b/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\347\255\226\347\225\245\346\250\241\345\274\217(Strategy-Pattern).md" index 4e0c46e21..ef6bcb6e2 100644 --- "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\347\255\226\347\225\245\346\250\241\345\274\217(Strategy-Pattern).md" +++ "b/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\347\255\226\347\225\245\346\250\241\345\274\217(Strategy-Pattern).md" @@ -1,101 +1,106 @@ -# 0.0 相关源码链接 -https://github.com/Wasabi1234/design-patterns - -# 1 定义 -![](https://upload-images.jianshu.io/upload_images/4685968-f3e6ce1684ece913.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +[相关源码](https://github.com/Wasabi1234/Java-DesignPatterns-Tuitorial) +# 1 简介 +## 1.1 定义 也叫做政策模式(Policy Pattern) -- 维基百科 -对象有某个行为,但是在不同的场景中,该行为有不同的实现算法. -比如每个人都要“交个人所得税”,但是“在美国交个人所得税”和“在中国交个人所得税”就有不同的算税方法. +- wiki +对象有某个行为,但是在不同的场景中,该行为有不同的实现算法.。比如每个人都要“交个人所得税”,但是“在美国交个人所得税”和“在中国交个人所得税”就有不同的算税方法. - 定义 Define a family of algorithms,encapsulate each one,and make them interchangeable. -定义一组算法,将每个算法都封装起来,并且使它们之间可以互换. +定义一组算法,将每个算法都封装起来,并且使它们之间可以互换。 +常见 if/else 结构。 -在`运行时`(非编译时)改变软件的算法行为 -- 主要思想 -定义一个通用的问题,使用不同的算法来实现,然后将这些算法都封装在一个统一接口的背后. +## 1.2 类型 +行为型。 +在`运行时`(**非编译时**)改变软件的算法行为。 -![策略模式的通用类图](https://upload-images.jianshu.io/upload_images/4685968-ad1caf184324decf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -策略模式使用的就是面向对象的继承和多态机制 +## 1.3 主要思想 +定义一个通用的问题,使用不同的算法来实现,然后将这些算法都封装在一个统一接口。 -策略模式的三个角色 -● Context 封装角色 -也叫做上下文角色,起承上启下封装作用; -屏蔽高层模块对策略、算法的直接访问,封装可能存在的变化. +策略模式使用的就是OOP的继承和多态。 -● Strategy抽象策略角色 -策略、算法家族的抽象,通常为接口,定义每个策略或算法必须具有的方法和属性 +## 1.4 主要角色 +### 通用类图 +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWFkMWNhZjE4NDMyNGRlY2YucG5n?x-oss-process=image/format,png) -● ConcreteStrategy具体策略角色 -实现抽象策略中的操作,含有具体的算法 +- Context 封装角色 +即上下文角色,起承上启下的封装作用。屏蔽高层模块对策略&算法的直接访问,封装可能存在的变化。 -### 通用源码 -- 抽象策略角色,它是一个非常普通的接口,在我们的项目中就是一个普通得不能再普通的接口了,定义一个或多个具体的算法 +- Strategy 抽象策略角色 +策略&算法家族的抽象,通常为接口,定义每个策略或算法必须具有的方法和属性。 +- ConcreteStrategy 具体策略角色 +实现抽象策略中的操作,含有具体的算法。 +### 通用源码 +- 抽象策略角色 +一个非常普通的接口,在项目中就是一个普通接口,定义一或多个具体算法。 # 2 适用场景 -针对一个对象,其行为有些是固定的不变的,有些是容易变化的,针对不同情况有不同的表现形式。那么对于这些容易变化的行为,我们不希望将其实现绑定在对象中,而是希望以动态的形式,针对不同情况产生不同的应对策略。那么这个时候就要用到策略模式了。简言之,策略模式就是为了应对对象中复杂多变的行为而产生的。 - -- 系统有很多类,而他们的区别仅仅在于他们的行为不同 +一个对象,其行为有些固定不变,有些又容易变化。对于这些容易变化的行为,我们不希望将其实现绑定在对象中,而希望能够动态地针对不同场景产生不同应对的策略。 +这时就要用到策略模式,就是为了应对对象中复杂多变的行为而产生的: +- 系统有很多类,而他们的区别仅在于行为不同 - 一个系统需要动态地在几种算法中选择一种 - # 3 优点 - 符合开闭原则 - 避免使用多重条件转移语句 -比如省去大量的 if/else 和 switch 语句,降低代码的耦合 +e.g. 省去大量 if/else、switch,降低代码耦合度 - 提高算法的保密性和安全性 -只需知道策略的作用,而不关心内部实现 - +只需知道策略的业务功能,而不关心内部实现 # 4 缺点 -- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类 +- 客户端必须知道所有的策略类,并决定使用哪个策略类 - 产生很多策略类 # 5 相关设计模式的差异 -## 策略模式和工厂模式 +## 5.1 V.S 工厂模式 - 行为型 -接收已经创建好的对象,从而实现不同的行为 +接收已经创建好的对象,从而实现不同的行为 - 创造型 -接收指令,创建出符合要求的具体对象 - -## 策略模式和状态模式 -- 若系统中某个类的某个行为存在多种实现方式,客户端需要知道到底使用哪个策略 -- 若系统中某个对象存在多种状态,不同状态下的行为又具有差异性,状态之间会自动转换,客户端不需要关心具体状态 - +接收指令,创建符合要求的具体对象 + +## 5.2 V.S 状态模式 +- 若系统中某类的某行为存在多种实现方式,客户端需知道到底使用哪个策略 +- 若系统中某对象存在多种状态,不同状态下的行为又具有差异,状态之间会自动转换,客户端不需要关心具体状态 +## 5.3 V.S 模板模式 +- 策略模式:只有选择权(由用户自己选择已有的固定算法) +- 模板模式,侧重点不是选择,你没得选择,你必须这么做。你可以参与某一部分内容自定义 # 6 实战 -![](https://upload-images.jianshu.io/upload_images/4685968-0ebc08f41e07cdca.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -![](https://upload-images.jianshu.io/upload_images/4685968-98e2b70fe0d9a3f0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -![](https://upload-images.jianshu.io/upload_images/4685968-ecbce7b0043a7490.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -![](https://upload-images.jianshu.io/upload_images/4685968-5dab16664b2d6639.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -![](https://upload-images.jianshu.io/upload_images/4685968-57e3f0490d67cfb0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -![](https://upload-images.jianshu.io/upload_images/4685968-8a75a258378f8a69.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -![image.png](https://upload-images.jianshu.io/upload_images/4685968-844075f01a9e349b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTBlYmMwOGY0MWUwN2NkY2EucG5n?x-oss-process=image/format,png) +- 促销策略接口 +![](https://img-blog.csdnimg.cn/20201104133917501.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) +- 返现策略 +![](https://img-blog.csdnimg.cn/20201104134155926.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) +- 立减策略 +![](https://img-blog.csdnimg.cn/2020110413472547.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) +- 满减策略 +![](https://img-blog.csdnimg.cn/20201104135011162.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) +- 测试类 +![](https://img-blog.csdnimg.cn/20201104135935601.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTg0NDA3NWYwMWE5ZTM0OWIucG5n?x-oss-process=image/format,png) 改造后的测试类 -![](https://upload-images.jianshu.io/upload_images/4685968-4991d2eaad9357c1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTQ5OTFkMmVhYWQ5MzU3YzEucG5n?x-oss-process=image/format,png) 可见 if/else 语句过多,采取策略+工厂模式结合 - 策略工厂 -![](https://upload-images.jianshu.io/upload_images/4685968-230088ca260db256.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTIzMDA4OGNhMjYwZGIyNTYucG5n?x-oss-process=image/format,png) - 最新测试类 -![](https://upload-images.jianshu.io/upload_images/4685968-acf80da4fa5ea954.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWFjZjgwZGE0ZmE1ZWE5NTQucG5n?x-oss-process=image/format,png) - 输出结果 -![](https://upload-images.jianshu.io/upload_images/4685968-7d26033a1b39bd6a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTdkMjYwMzNhMWIzOWJkNmEucG5n?x-oss-process=image/format,png) # 7 源码应用解析 ## JDK中的比较器接口 - 策略比较器 -![](https://upload-images.jianshu.io/upload_images/4685968-307666896c3d1800.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -![具体策略](https://upload-images.jianshu.io/upload_images/4685968-d928dd16bea44a60.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTMwNzY2Njg5NmMzZDE4MDAucG5n?x-oss-process=image/format,png) +![具体策略](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWQ5MjhkZDE2YmVhNDRhNjAucG5n?x-oss-process=image/format,png) 比如Arrays类中的 sort 方法通过传入不同比较接口器的实现达到不同排序策略 -![](https://upload-images.jianshu.io/upload_images/4685968-f92073712e30ce66.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWY5MjA3MzcxMmUzMGNlNjYucG5n?x-oss-process=image/format,png) ## JDK中的TreeMap 类似于促销活动中有促销策略对象,在T reeMap 中也有比较器对象 -![](https://upload-images.jianshu.io/upload_images/4685968-424f787da17d4876.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTQyNGY3ODdkYTE3ZDQ4NzYucG5n?x-oss-process=image/format,png) compare 方法进步加工 -![](https://upload-images.jianshu.io/upload_images/4685968-32e02456542c1e48.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTMyZTAyNDU2NTQyYzFlNDgucG5n?x-oss-process=image/format,png) ## Spring 中的Resource 不同访问策略 -![](https://upload-images.jianshu.io/upload_images/4685968-66d6191177faaf2a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTY2ZDYxOTExNzdmYWFmMmEucG5n?x-oss-process=image/format,png) ## Spring 中bean 的初始化ceInstantiationStrategy - 两种 bean 的初始化策略 -![](https://upload-images.jianshu.io/upload_images/4685968-8fa5e44e491aafdc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LThmYTVlNDRlNDkxYWFmZGMucG5n?x-oss-process=image/format,png) \ No newline at end of file