- Java线程
- Synchronized
- AQS
- ReentrantLock
- Volatile 和 JMM(Java内存模型)
- 乐观锁
- 线程池
- ConcurrentHashMap
- Callable
- ThreadLocal
- 代码题
- End
Java线程
基础概念
进程 & 线程
并行 & 并发
Java 线程原理
线程运行原理
栈与栈帧
上下文切换
线程状态转换
Java 线程常用方法
start( ) & run( )
为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
new 一个 Thread,线程进入了新建状态。
调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。
但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结:
调用 start() 方法方可启动线程并使线程进入就绪状态,即启动新的线程,,通过新的线程间接执行 run 中的代码
直接执行 run() 方法的话不会以多线程的方式执行;在主线程中执行了 run,没有启动新的线程 。
sleep( ) & yield( ) & join( )
sleep
1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException.
3. 睡眠结束后的线程未必会立刻得到执行,需要进入就绪状态,去排队.
4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性 .
yield
1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程 .
2. 具体的实现依赖于操作系统的任务调度器.
join( )
1. join来解决:等待调用 join 的线程运行结束。
∴ 若在主线程中调用 t1.join,直到 t1 结束主线程才能继续执行。
sleep后,不占用时间片;join后,等待的线程占用时间片。
2. join方法还可以携带形参,表示显示等待。若超时了,另一线程仍未执行完,就不等待了。
interrupt( )
1 打断阻塞
2 打断正常
sleep( ) 的应用:防止 CPU 占用 100%
join( ) 的应用:同步
同步:一个线程等待另一个线程返回结果。
主线程 & 守护线程
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
阻塞式的解决方案:synchronized,Lock .
非阻塞式的解决方案:原子变量.
Synchronized
关键字:synchronized
synchronized 作用
synchronized 底层:Monitor 管程/监视器
Java 对象头的 Mark Word
一个Java普通对象在内存中通常由 2 部分组成:对象头 + 成员变量 .
对象头信息中,有一个 Mark Word。根据虚拟机位数,对应有32/64位。
Monitor原理:WaitSet & EntryList & Owner
现在来介绍一下 sychronized 底层中非常重要的一个原理:Monitor
Monitor 就可以当作我们之前说的那个锁,可以翻译成“监视器(字面)” 与 “管程(OS中的概念)”
(1)线程得到锁
每一个Java对象中使用 sychronized 时,就会与 Monitor 相关联。
如图,synchronized对象使用 obj 上锁时,obj 的 mark word 的指针会指向 OS 中Monitor。
回看上节课 mark word 的结构,上锁时,状态不再是nomorl,而是状态4,末尾的两位变成了10。
从而,Monitor中的 Owner 便是当前线程 thread2 。
(2)线程竞争锁失败
如果还有别的线程,也把obj当成锁,此时就先看看 obj 的 markword 是否关联了 Monitor。
此时发现已经关联了,那就去看看Owner是否空缺,发现已经是Thread-2了,那么就乖乖去 EntryList/阻塞队列 里排队了,进入了一种blocked的阻塞状态,等待Owner被释放。
(弹幕说AQS就是把这个原理 用Java封装起来)
(3)释放锁,竞争得到新的Owner
图中,线程2执行完任务,owner就会被释放。
此时 EntryList 中的对象就会被叫醒,进行竞争,成功的得到Owner执行代码,失败的继续进入阻塞BLOCKED状态。(具体“竞争”规则比较复杂,得根据JDK底层实现来看,反正并不是按照先来后到的顺序,后来的也可能先得到锁)
(4)小结
Monitor就是锁,只要 synchronized 中用了同一个 obj ,那么就会关联到同一个Monitor。.
若是锁的不同对象,对应的Monitor也就不一样了。
若是用 sychronized,对象头中的mark word就不会变化,更不会用它取检查是否与 monitor 关联。
同时可参考笔记(原理篇和本篇),图中的 WAITING 等内容后面会讲。
锁升级
无锁 升级 偏向锁
从无锁状态开始说起。
创建一个对象,对象头有 25 位的 hashcode,4位的分代年龄,1位偏向锁标记位,2位锁标记位。
现在有一个很重要的问题:
问题1:如果对象 new 关键字的创建,对象头中 25 位 hash 有没有真正存放对象的hashcode?
这个问题非常重要,当我们讨论 无锁 ---> 偏向锁 的过程中,这个问题就非常重要了!首先了解一下结论:<br />如果仅仅new了一个对象,没有 显示/隐式 调用 object 里的 hashcode 方法,那么此时对象头中的 hashcode 并没有真是存在!<br />没有调用 hashcode 方法时,对象头的这个值就是 0,没有 hashcode。
那 hashcode 方法有哪些调用方式?
① 显示调用:构造方法里写个 super.hashcode;
② 隐式调用:new 完对象后,立刻进行了对象的处理,比如说存放到 hashmap / hashset 这种类hash结构里.
问题2:无锁 和 偏向锁 对应 mark word 的状态是什么?
4位的分代年龄这个无所谓了,一开始就是0;
如下图红框。锁标志位是2位,无锁(normal)和偏向锁(biased)都用 01 代表,区别在于:
无锁状态下,偏向锁标志位是0;
偏向锁状态下,偏向锁标志位是1.
(偏向锁标志位就是图中的 biased_lock,红框中的)
问题3:无锁状态 升级 为偏向锁的状态的 “前提”
升级前提条件:无锁状态 的对象里面的 mark word 里,没有真正的存放 hashcode。
==> 如果 ”对象刚刚创建,我们立即 显式/隐式 调用hashcode方法“,那么这个对象,哪怕是你开启了偏向锁的配置,也无济于事。 也就是说,对象有hashcode时,不能被偏向锁使用!。
问题4: 为什么有了 hashcode 这 25 位,就用不了偏向锁了?
(对着上面问题2中那个 mark word 结构图理解如下内容)
有同学有疑问,我这个偏向锁,本来我是应该把 线程 id 放到 hashcode 的 ”前23位“,后两位放一下epoch,用这 23 + 2 = 25 的 id 的 poch,替换原来 mark word 里面的 hashcode 的这 25 位。(epoch的作用后面再说)
问题在于:为什么有了 hashcode 这 25 位,就用不了偏向锁了?
关键在于:偏向锁把 当前线程的id 放置到我们 hashcode 的这个位置 时,没有逻辑对 hashcode 进行保存。
问题5:打开偏向锁设置,升级为偏向锁后,偏向锁好处是什么?
比如说,线程a再次进行对象的加锁操作的时候,检查一下对象的 hashcode离里前 23 位 线程 id 是不是偏向自己 ,如果偏向直接加锁就可以了。无需竞争,挂起,CAS 等。
这样的话就可以带来一个非常大的性能提升。(不太理解)重量级锁需要竞争,挂起等。轻量级锁需要CAS。
问题6:如何开启偏向锁?
JDK1.7以后,默认开启偏向锁。
总结 无锁和偏向锁的区别在哪里:
① 无所状态下 25 位的hashcode;
偏向锁这里存储对应的 23位线程 id 和 2 位epoch .
② 偏向锁标记位:无锁时是0,偏向锁时是1.
偏向锁 升级 轻量级锁
1 首先否定一句话:发生竞争时,偏向锁升级为轻量级锁
这句话有问题,发生竞争时,不一定升级成轻量级。发生竞争,是 升级为轻量级锁的 必要条件。
2 偏向锁升级为轻量级锁的过程
思考这几个问题:
什么情况会升级?
什么时候偏向到另外一条线程?
什么情况偏向锁无法使用?
设置一个场景:
线程 a 把自己的 id 放置到 mark word 这23位里,即 a 上了偏向锁,对象偏向锁指向 a。
这时候,线程b过来,对这个对象进行一个偏向锁加锁的一个操作,这时候b会检查什么呢?
当前的 锁标记位是01—-> 是否为偏向锁a,偏向锁标记位是1 —->此时b知道了,当前这个对象已经有线程偏向了。
这个时候b会检查,当前对象所偏向的线程的存活状态,即线程a的存活状态,如果线程a已经在临界区之外,我们可以理解成线程a没有执行同步代码块,那么b直接把对象置为“无锁状态”。这是一个宁为玉碎不为瓦全的操作:线程a你没执行,我不直接抢,我把这个对象置成无锁状态,然后才去抢。
如果b此时抢到了.
那么就把 23 位搞成 b 的id,此时就是 偏向锁从 a 偏向 b 了。
这种情况其实比较”牵强“,因为此时 ”线程a已经执行完了“,这并不是真正意义上的 多线程的锁的增强。勉强当作是”多线程环境吧“:正在执行的 和 执行完毕的 进行一个争抢。
如果b竞争失败呢?
假设,对象的被置为”无锁状态后“,来了一个线程c,线程 b c 需要一块争抢这个无所状态下的对象。假如 b 失败了,b 不干了,此时场景就变了:” 线程c正在执行,线程b过来争抢 “ 。
==>b 发起一个更加高级的操作【偏向锁撤销】。
当然要执行这个操作,有个前提:“当前执行线程要进入jvm安全点”。那么线程 b 就会等待线程 c 到达安全点,然后线程 c 到达安全点以后呢,线程c的栈会被执行遍历——“遍历一些锁记录”。 这个遍历为了什么?会导致什么?有三种情况:
第一:升级为轻量级锁
直接把对象设置为 无锁状态,并且将对象的 锁标志位 升级为00 —— 轻量级锁!
==> 置为无锁 可以直接理解为 ‘’偏向锁升级为轻量级锁“,这会把当前对象的锁标志位置为00;然后把自己偏向锁的这个 id 和 epoch 置为空,即回到了一开始没有hashcode的状态,这是它所做的操作。偏向锁标记为是0,代表没有偏向锁一说了。
这就是无锁状态的解释,为什么轻量级锁要理解成”无锁状态“?<br />首先,轻量级锁中 markword 应该是什么呢?除了锁标志位以外的 30 位 都是指向 栈帧 中 锁记录 的这样一个指针。但是现在这个状态非常游离,锁标志位是00,貌似是轻量级锁,但是 hashcode 的 分代年龄 和 偏向锁锁标记 位置还在 mark word里,并没有被替换成 轻量级锁的这种指向栈帧的这种指针,所以可以我们还是把它看成一种 ”没有真正加锁的状态“。
第二:重新偏向线程b
jvm 中除了 偏向锁,还有 ”批量重偏向“ 和 ”批量撤销“。
<1> 如何理解这个 ”批量重偏向“?<br />假如现在有一个student类,它new了40次,然后有一个线程a分别对这40个对象进行了偏向锁的加锁操作。然后a线程一直在对这40个对象保持代码的执行,此时线程b过来了,线程b想要在线程a执行的情况下,对这40个对象进行偏向锁的撤销操作。<br /> 从第1-19个对象,直接把偏向锁升级为轻量级锁。当执行到第20个对象的时候,jvm开始反思:我这40个对象,偏向线程a是不是偏向错了,我是不是把他们偏向线程b?线程b那么想要,那我从第20个开始偏向给你吧,第20-39个对象,把偏向 线程id 直接偏向 线程b。至于从第几个开始,这些都是jvm里面配置的阈值。 20个之前 ,锁升级; 20-40个,批量重偏向。
<2> 那批量撤销是什么?<br /> 我不是创建了40个对象吗?当 b 线程执行到第40个线程时,jvm又开始反思了:我把19-39都给你了,你咋还撤销呢?既然都乱抢,那就别玩了,偏向锁直接撤销!<br /> 此时,在创建第41个对象的时候,再 new 一个 student 时,markword 里的锁标记为变成00(轻量级的)。<br /> 此时 1-19是轻量级,20-39是偏向 b 的,40以后直接升级到 00 级别。
第三:直接把对象置为 不可使用偏向锁
(??emmm…是不是,批量撤销后,就是这个状态了)<br />
竞争到了轻量级锁后,该干什么?
如果线程竞争得到了轻量级锁,该线程会把对象里的 markword 里面的 hashcode 25位 ,分代年龄4 位,偏向锁标记位 1 位,所有加起来这30位,直接复制到当前 a 线程 的 栈帧 “lock revord” 这样一个区域空间,这就是我们所说的 ”替换 mark word“,偏向锁不存在替换一说。 <br /> 它把这部分东西替换到自己的栈帧里,并用 lock reword 进行保存之后,它会将对象头里的这30位替换成指向自己栈帧 lock record 这样一个记录,就是一个指针。
轻量级锁 升级 重量级锁
什么时候升级
如果线程 b 要过来竞争这个轻量级锁,应该采用的是 cas 这个操作。它要尝试:<br /> ① 把自己栈帧中开辟的这个 lock record 里的,这个现在当前是空的对吧,尝试将 mark word 里的hashcode分代年龄以及是否为拍你想锁标记位 这30位 复制到自己当前线程里边的这个 record 里,但是必然是失败的。<br />==> 因为它发现当前这个对象markword里的指针指向了线程a栈帧中的 lockrecord。
② 替换失败以后,进行【cas循环】,默认循环10次,替换还失败,则进行锁升级。<br />==> 则 ”轻量级锁升级为重量级锁“。
升级的过程
(1)
首先,cas失败以后,线程b又干坏事。
直接把指向轻量级锁的指针,替换成指向重量级锁的指针,即指向 object monitor.
(2)
但我这会线程 a 轻量级锁还没有解锁,a 线程还在执行。等到释放的时候,线程a同样会进行cas操作,将自己栈帧里的 lock recard 记录,尝试将 mark word 替换回我们的对象头里,发现失败了!为什么?因为这个时候对象头里的指针是重量级锁的指针,不是之前轻量级的,所以替换失败了。
(3)
替换失败的话,当前持有轻量级锁的这个线程a就明白了,有竞争了,得走重量级锁这个流程了,那么就要走”重量级锁的退出逻辑“。
==> 首先会将 object monitor里的 hider,将自己栈帧里的 lock record 里面的对象头的这些记录,将它设置到 object monitor 里面的 header 里,并且将 object monitor里面的owner这个变量设置为 ”自己“。
为什么要这个操作?我们先来考虑一下,轻量级锁 a 的过程中,mark word 在哪一块?markword在 轻量级锁 线程a 的栈帧中,那么b过来竞争的时候,直接把轻量级锁这个指针直接替换为重量级锁指针。那么问题来了:这个mark word一直保存在a线程里面,而且a线程在最后释放锁的时候,经过cas替换,”替换不回来了“!总不能让这些 mark word 内容全部丢失吧?那么不丢失怎么办?
==> 这里这个逻辑就是 ”从 轻量级锁 到 重量级锁 过程中“,并不是使用 display mark word,而是使用 从 轻量级锁 持有的 轻量级锁线程指针的 lock record 里边,将 mark word 内容 直接赋予给 指向重量级锁指针 的这个 object 里边的 hider 里边,”保证markword不丢失“。 所以markword一直没丢失。
偏向就不会保存。一旦对象创建后,有hashcode的话,偏向锁就用不了,原因也在这里面,要对比着思考这一块!
wait/notify:管程的重要组成,线程通信的基础
API 介绍
强调一下,这几个方法都是Object对象的方法,不是 Thread 类的方法!
并且,想要调用这些方法,必须先获得该对象的锁才行,这是大前提!没获得锁时,调用这些方法会报错,直接说这是非法操作。
至于 notify 唤醒哪一个,这个是根据底层的设定计算的,可以认为是随机唤醒的,我们不能决定。
wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify。无形参的那个代表一直等待。
工作原理:Monitor中 的 WaitSet
- 分清楚 BLOCKED 和 WAITING 这俩状态区别:
不同点:
(1)BLOCKED:还在等待锁;
(2)WAITING:已经获得了锁,但是放弃了锁,在WaitSet中排队;
相同点:
都处于阻塞状态,不会占用CPU时间.
- 何时被唤醒
对于WAITING的,调用了 Notify 方法或者 notify 方法。
对于BLOCKED的,Owner 释放锁的时候,就回去唤醒 BLOCKED 队列中的线程。
wait VS sleep
开始之前先看看,sleep(long n)和wait(long n)的区别(这里两个方法是带参数的)
==>不同点
1) sleep 是 Thread 方法。
而 wait 是 Object 的方法 。
2) sleep 不需要强制和 synchronized 配合使用。
但 wait 需要和synchronized 一起用 。
3) sleep 在睡眠的同时,不会释放对象锁的;此时,其他线程来,获得不了锁。
但 wait 在等待的时候会释放对象锁 。
==>相同点
4) 它们状态 TIMED_WAITING。
(注意哈,这里说的是带参数的waiting方法,如果是不带参数的waiting方法,进入的就是WAITING状态)
一个小技巧,对于锁对象,最好都加上一个 final!意味着引用不可变。(不懂)
使用套路
应用:同步模式——保护性暂停
应用:异步模式——生产者和消费者
AQS
ReentrantLock
Volatile 和 JMM(Java内存模型)
请你解释一下volatile?这个关键词能达到什么效果
至少打出两点:保证可见性,禁止指令重排
如何保证可见性?
(1)“总线”
让什么可见?让其他线程可见。现在处理器都是多核的处理器,那就有多个CPU,每个CPU理论上能提供1条线程,如果多个CPU对一个volatile修饰的变量修改时,其中一旦一个CPU,即一个线程,拿到修改权限,当他修改后,得立刻推送给主存,推送给主存的过程中,通过“总线”。
(2)MESI 和 CPU嗅探
其他线程怎么可见的?刚才一直再说其中一条线程,修改完了以后,过“总线”,推送到主存。可见性体现在“主线”这个层面【重要】,一定要记住了。当我们对这个之进行了修改,推导主存的过程中,到“总线”的这一部分,其他CPU一直再在【嗅探】“总线的数据流通”,通过嗅探,在缓存一致性协议(MESI)的保证下,能够嗅探到这条数据的修改。如果自己缓存行有这个数据,那就把自己的缓存行置为“不可用”。如果下次有线程需要进行读/写,需要先从主存拉取这条数据,保存到自己的缓存行,然后再做进一步的处理。
**总线这部分通过 MESI(缓存一致性协议),和嗅探技术一起保证 可见性**
(3)lock前缀指令
那么CPU在嗅探总线的过程中,它怎么知道这个变量就是volatile修饰的呢?这个得到汇编层面解答。当我们变量被volatile修饰时, 如果一个线程对变量进行修改,并推送到总线后,它的汇编指令的码,会【加lock前缀指令】,这就是关键作用点。汇编层面,lock两层含义:信息的修改推送到主存;lock指令过总线时,其他cpu会嗅探带有lock指令的汇编指令,然后置其他缓存行为不可用(为啥是其他???不是相对应的这行???)
(4)总结:volatile保证可见性:总线的MESI + CPU的嗅探技术 + volatile修饰的变量在修改时,会在汇编层面加上lock前缀 ==> 三者配合最终达到volatile的可见性
禁止指令重排序
对于volatile修饰的变量,如何指令重排?
在 “编译阶段” 禁止指令重排,
方法表属性表的CODE放的是JVM的执行指令,编译期指令就得全部编译了
volatile 编译的4句话
store:存储 —— 写
load:加载 —— 读
• ·在每个 volatile 写 操作的 前 面 插入一个 Store Store 屏障。 写写 不能指令重排.
• ·在每个 volatile 写 操作的 后 面 插入一个 Store Load 屏障。 写读 不能指令重排.
• ·在每个 volatile 读 操作的 后 面 插入一个 Load Load 屏障。 读读 不能指令重排.
• ·在每个 volatile 读 操作的 后 面 插入一个 Load Store 屏障。 读写 不能指令重排.
load store是全能性屏障,由于这个屏障开销比较大,所以对于很多处理器,目前都不会采用单独的 l-s 屏障进行所有的全功能的指令重排
在特殊情况下,保证原子性
(理解不深入,建议不要说,不然狂问你JVM底层)
i++有三条指令,不能保证原子性。load i ,i add, i store.
volatile对单条字节码指令可以保证原子性,多条就不行
volatile双重检查锁(后面单独讲)
乐观锁
线程池
自定义线程池
线程池的组成
两个属性: 阻塞队列(任务队列)
线程集合(生产者,一个HashSet,线程中只有 Thread 类太单薄,包装成 work 类)
重要方法:execute
其他属性: 线程数 + 超时时间 + 时间单位 + 拒绝策略 + ……
1 阻塞队列:平衡线程的 “消费者” 和 “生产者”
属性
任务列表:双向链表,先进先出。
两种实现方式:LinkedList 和 ArrayDeque,后者更好。
锁:一个任务只能被一个线程获取 —— 使用ReentrantLock
2个条件变量:使用 ReentrantLock 中的方法 newCondition() 来创建
队列中无任务时,消费者wait,对应emptyCondition
队列中满任务时,生产者wait,对应fullCondition
容量:队列容量上限。
属性 和 构造方法(线程池中使用)
方法
take:队列中获取/阻塞获取
==> 由线程池属性,works中的work类的run方法调用
先写lock结构,finally 中 unlock.
锁中 while:看队列是否为空
为空时,去 emptyCondition 中等
不空时,继续.
用 removeFirest 从队列中得到元素并移除,唤醒消费者线程。
put:阻塞添加 ==> 由线程池的方法execute调用
同 Lock 和 unlock.
队列满了就 wait,没满就继续.
addLast 添加到队尾,唤醒消费者线程.
size:得到队列中元素数量
加锁(?)
queue.size
poll 方法:超时阻塞获取
其中,take方法中不要直接采用 wait(),而是应该用带有 “时限的等待” 方法。
专门写一个 poll 方法来替代 take 方法,内部用有时限的wait方法:awaitNanos。其返回值=等待时间 - 经过时间 = 剩余时间restTime。restTime < 0时,就不用等了。这样可以避免长时间等待,也能避免“虚假唤醒”(???)
2 execute方法:执行任务
① 形参:传入线程 Runable Task
② 任务数 <= coreSize时,线程富余,直接给 work 执行.(调用线程集合的方法)
④ new一个work 类线程
⑤ 线程集合works中加入work实例,并start
⑥ 给 ②⑤ 用到的共享资源加上 synchronized 的保护!
==>调用 take 方法获得任务
③ 任务数 > coreSize时,线程不足,加入阻塞队列暂存.(调用阻塞队列的方法)
==>后期优化后,将使用 “拒绝策略”
3 生产者类 Worker(Thread的封装类)
Worker 类 extends Thread.
属性:task,属于 Runnable 类,这个就是之前推荐的线程创建的类
构造函数.
重写run方法:用来执行任务
途径1:创建 work 时,已传入 task,task 不为空!
途径2:task 执行完,work 得再利用,得从 阻塞队列 中获取 task
while(task != null || ! (task = taskQueue.take()) == null)
4 拒绝策略:策略模式,用户定义拒绝策略
任务数 > 阻塞队列容量 + coreSize 时,多余的任务没办法被接收,会一直卡在等待队列外边,很影响性能。因此提出了“拒绝策略”这个概念。
阻塞队列的 put 方法 + 线程池的 execute 方法
execute 中,已经调用了 阻塞队列的 Put 方法。此时,需要补充拒绝策略方法,拒绝策略是去实现接口,在线程池实例化时,拒绝策略是构造器中需要实现的属性,由用户自己实现!
ThreadPoolExecutor:JDK 线程池实现
一、5 种 池状态
状态的表示
ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量.
为什么要用一个数表示两个状态(高低位),而不用两个整数存?
==> 保证二者赋值的原子性。放到两个数,就需要两次原子操作,放到一个数一次就够了.
状态的含义
刚创建就是 running,可以接收新任务,可以处理阻塞状态任务
调用了 shutdown 方法,温和,不会接受新任务,但是已经提交的任务都会执行
stop 是暴力状态,打断正在执行的方法
tidying 过度状态,准备进入 terminated 状态
从数字上比较,TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING
runing 的 111 最左边的1是符号位,所以转成10进制是-3的意思,最小,而非最大
二、构造方法的 7 个重要参数
7 个参数介绍
(自定义中KO)corePoolSize:核心线程数目 (最多保留的线程数)
(自定义中KO)maximumPoolSize:最大线程数目
(自定义中KO)workQueue:阻塞队列
(自定义中KO)handler :拒绝策略
keepAliveTime:生存时间 - 针对救急线程
unit: 时间单位 - 针对救急线程
threadFactory :线程工厂 - 可以为线程创建时起个好名字,便于区分线程池的线程和其他线程.
救济线程介绍(理解7个参数中的 keepAliveTime 和 unit为什么存在——救济线程存在生存时间)
【救济线程与核心线程最大区别】救济线程存在生存时间,核心线程没有生存时间。
当高峰过去后,超过corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由 keepAliveTime 和 unit 来控制。
救急线程数 = maximumPoolSize(总线程数) - corePoolSize(核心线程数)
线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。
当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue 队列排 队,直到有空闲的线程。
救济线程前提:有界队列。如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线 程来救急。
JDK的 4种拒绝策略
如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现。
RejectedExecutionHandler 是拒绝策略的接口.
(1)AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
(2)CallerRunsPolicy 让调用者运行任务
(3)DiscardPolicy 放弃本次任务
(4)DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
三、Executors 框架:JDK Executors 类中提供了众多工厂方法来创建各种用途的线程池
newFixedThreadPool:固定大小线程池(Fixed = 固定)
public static ExecutorService newFixedThreadPool(int nThreads) { //【仅传入线程数量】
return new ThreadPoolExecutor(nThreads, nThreads, //核心线程数 == 最大线程数
0L, TimeUnit.MILLISECONDS, //超时时间 = 0
new LinkedBlockingQueue<Runnable>()); //队列没有指定大小
}
特点 :
核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
阻塞队列是无界的,可以放任意数量的任务 .
评价:
适用于任务量已知,相对耗时的任务
newCachedThreadPool:带缓冲池线程池
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, //没有核心线程,全是救济
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>()); //这是个同步队列.
}
特点 :
(1)核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着
① 全部都是救急线程(60s 后可以回收).
② 救急线程可以无限创建 .
(2)队列采用了 SynchronousQueue 实现特点是,它没有容量(无界),没有线程来取是放不进去的
(一手交钱、一手交 货)
评价
整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线
程。 适合任务数比较密集,但每个任务执行时间较短的情况。
newSingleThreadExecutor:单线程线程池
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1, //只有1个核心线程,无救济线程
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>())); //有界
}
使用场景:
希望多个任务排队执行(串行)。
线程数固定为 1,任务数多于 1 时,会放入无界队列排队。
任务执行完毕,这唯一的线程 也不会被释放。
单线程池还能称为池吗?与单线程区别是什么?
① 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一 个线程,保证池的正常工作 .
newFixedThreadPool(固定大小) 和 newSingleThreadExecutor(单线程)区别在哪里?
返回值类型不同,单线程线程池返回值使用装饰器模式进行了包装。
② Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改 .
对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改。
③ Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改 .
FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法 .
提交任务的方法:submit() 和 execute()
本质是 Runnable 和 Callable 的区别.
submit 这里接收结果利用了 “保护性暂停”.
// 执行任务
void execute(Runnable command);
// 提交任务 task,用返回值 Future 获得任务执行结果
<T> Future<T> submit(Callable<T> task); //参数是callable而不是runnable,可以返回结果
提交任务的方法:invokeXxx() 相关方法
// 提交 tasks 中所有任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;
// 提交 tasks 中所有任务,带超时时间
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit) throws InterruptedException;
// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
<T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;
// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
关闭线程池的方法:shutdown() 和 shutdownNow()
本质是 SHUTDOWN 和 STOP 的区别.
(1)shutdown :void shutdown ();
线程池状态变为 SHUTDOWN
不会接收新任务 ;
但已提交任务会执行完 ;
此方法不会阻塞调用线程的执行 ;
(2)shutdownNow:List
线程池状态变为 STOP
不会接收新任务
会将队列中的任务返回
并用 interrupt 的方式中断正在执行的任务
线程池异常处理
方法1:主动捉异常,使用 try-catch 自己捕捉
方法2:使用 Future,得有返回值.
ScheduledThreadPoolExecutor :任务调度线程池
有时希望任务 延时执行 + 定时执行。
在『任务调度线程池』功能加入之前,可以使用 java.util.Timer 来实现定时功能,Timer 的优点在于简单易用,但 由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务。
Fork/Join
ConcurrentHashMap
Callable
callable不能直接替换 runnable(如下图报错),需要找一个 二者都能识别的中间件:FutureTask,未来任务。
不影响主线程,单开线程.
汇总一次(不懂)
ThreadLocal
是什么?
辨析 Thread & ThreadLocal & ThreadLocalMap
为什么用弱引用 ?内存泄漏问题?
小节
代码题
卖火车票
import java.util.concurrent.locks.ReentrantLock;
public class TicketDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket, "1号窗口");
Thread t2 = new Thread(ticket, "2号窗口");
Thread t3 = new Thread(ticket, "3号窗口");
t1.start();
t2.start();
t3.start();
}
}
class Ticket implements Runnable {
private static int ticket = 100; //静态
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {//重写run方法
while (true) {
//加锁
lock.lock();
//
try {
//有票就卖
if (ticket > 0) {
// 模拟网络阻塞
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// ticket-- 表示卖票了
System.out.println(Thread.currentThread().getName()
+ "售出车票,ticket号为:" + ticket--);
} else break;
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();//解锁,一定要在finally里写
}//try的
}//while的
}//run的
}
import java.util.concurrent.locks.ReentrantLock;
public class TicketDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket, "1号窗口");
Thread t2 = new Thread(ticket, "2号窗口");
Thread t3 = new Thread(ticket, "3号窗口");
t1.start();
t2.start();
t3.start();
}
}
class Ticket implements Runnable {
private static int ticket = 100;
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
synchronized (Ticket.class) {
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "售出车票,ticket号为:" + ticket--);
} else break;
}//synchronized的
}//while的
}
}
生产者消费者
public class ThreadTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Thread productThread = new Thread(new Productor(clerk), "生产者1");
Thread consumerThread = new Thread(new Consumer(clerk), "消费者1");
Thread consumerThread2 = new Thread(new Consumer(clerk), "消费者2");
productThread.start();
consumerThread.start();
consumerThread2.start();
}
}
//一、Clerk类
class Clerk {
private int product = 0;
//添加商品
public synchronized void addProduct() {
if (product >= 20) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
product++;
System.out.println(Thread.currentThread().getName() + "生产了第" + product + "个产品");
notifyAll();
}
}
//得到商品
public synchronized void getProduct() {
if (this.product <= 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(Thread.currentThread().getName() + "消费了第" + product + "个产品");
product--;
notifyAll();
}
}
}
//二、生产者类
class Productor implements Runnable {
Clerk clerk;
public Productor(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println("生产者开始生成产品");
while (true) {
try {
Thread.sleep((int) Math.random() * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.addProduct();
}
}
}
//三、消费者类
class Consumer implements Runnable {
Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println("消费者开始消费产品");
while (true) {
try {
Thread.sleep((int) Math.random() * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.getProduct();
}
}
}
单例模式
饿汉式
// 问题1:为什么加 final
// 问题2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例:readResolve()方法
public final class Singleton implements Serializable {
// 问题3:为什么设置为私有? 是否能防止反射创建新的实例?
private Singleton() {}
// 问题4:这样初始化是否能保证单例对象创建时的线程安全?
private static final Singleton INSTANCE = new Singleton();
// 问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public, 说出你知道的理由
public static Singleton getInstance() {
return INSTANCE;
}
public Object readResolve() {
return INSTANCE;
}
}
懒汉式
低性能 懒汉式
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
// 分析这里的线程安全, 并说明有什么缺点 ==> 无论如何,都得等待所释放
public static synchronized Singleton getInstance() {
if( INSTANCE != null ){
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
DCL 优化 懒汉式
什么是双重检查锁:
双重检查锁是一个对单例模式的优化,具体做了如下步骤:
在synchronized外边,做了一个空判断,好处如下:
- (相对于直接synchronized)避免用户态到内核态的一个消耗,即不用等锁释放,进行上下文切换
new 对象的时候需要经过如下步骤:
- 分配内存空间
- 对象初始化
- instance 指针指向内存空间
不使用 volitale修饰的话,可能会出现一个指令重排,会导致如下结果
- 如果先执行 instance 指针指向内存空间:
A线程刚执行到new对象,B线程刚判断实例是否为空(即synchronized方法前).
B线程此时拿到的是没有初始化的结果(只分配了空间,还没有调用构造方法,拿到了个半成品)
- 为什么加volitale能拿到一个正常的结果:
- volitale只能保证单个JVM指令的原子性和可见性
- 真正的作用:
- 在 new 对象前边添加 store store屏障
- 在 new 对象后边添加 store load 屏障
- 最终保证 new 对象的过程,对外不可乱序获取,因此保证了线程安全性
public final class Singleton {
private Singleton() { }
// 问题1:解释为什么要加 volatile ?
//==>避免重排序后,拿到的引用,只是分配了内存空间,但是没有来得及调用构造方法!
private static volatile Singleton INSTANCE = null;
// 问题2:对比实现3, 说出这样做的意义
// ==> 创建好后,大家都直接用就行了,不像未优化的版本,就算已经创建了,也得等待锁
public static Singleton getInstance() {
if (INSTANCE != null) return INSTANCE;
synchronized (Singleton.class) {
// 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗
//==>若无:t1 t2都判断外面的实例为null,t1拿到锁创建实例后,t2又进来创建一个.
if (INSTANCE != null) { // t2
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
}
静态内部类 懒汉式
public final class Singleton {
private Singleton() { }
// 问题1:属于懒汉式还是饿汉式(懒汉式,类的创建是懒汉式,第一次加载类时,才会初始化)
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
}
// 问题2:在创建时是否有并发问题(JVM负责的类加载是能够保证线程安全的)
public static Singleton getInstance() {
return LazyHolder.INSTANCE; //在这类调用类
}
}