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位。

image.png

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 。

image.png

(2)线程竞争锁失败
如果还有别的线程,也把obj当成锁,此时就先看看 obj 的 markword 是否关联了 Monitor
此时发现已经关联了,那就去看看Owner是否空缺,发现已经是Thread-2了,那么就乖乖去 EntryList/阻塞队列 里排队了,进入了一种blocked的阻塞状态,等待Owner被释放。
(弹幕说AQS就是把这个原理 用Java封装起来)

image.png

(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?

  1. 这个问题非常重要,当我们讨论 无锁 ---> 偏向锁 的过程中,这个问题就非常重要了!首先了解一下结论:<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,红框中的)
image.png

问题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,代表没有偏向锁一说了。

  1. 这就是无锁状态的解释,为什么轻量级锁要理解成”无锁状态“?<br />首先,轻量级锁中 markword 应该是什么呢?除了锁标志位以外的 30 都是指向 栈帧 锁记录 的这样一个指针。但是现在这个状态非常游离,锁标志位是00,貌似是轻量级锁,但是 hashcode 分代年龄 偏向锁锁标记 位置还在 mark word里,并没有被替换成 轻量级锁的这种指向栈帧的这种指针,所以可以我们还是把它看成一种 ”没有真正加锁的状态“。

第二:重新偏向线程b
  1. jvm 中除了 偏向锁,还有 ”批量重偏向“ ”批量撤销“。
  2. <1> 如何理解这个 ”批量重偏向“?<br />假如现在有一个student类,它new40次,然后有一个线程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个,批量重偏向。
  3. <2> 那批量撤销是什么?<br /> 我不是创建了40个对象吗?当 b 线程执行到第40个线程时,jvm又开始反思了:我把19-39都给你了,你咋还撤销呢?既然都乱抢,那就别玩了,偏向锁直接撤销!<br /> 此时,在创建第41个对象的时候,再 new 一个 student 时,markword 里的锁标记为变成00(轻量级的)。<br /> 此时 1-19是轻量级,20-39是偏向 b 的,40以后直接升级到 00 级别。

第三:直接把对象置为 不可使用偏向锁
  1. (??emmm…是不是,批量撤销后,就是这个状态了)<br />

竞争到了轻量级锁后,该干什么?

  1. 如果线程竞争得到了轻量级锁,该线程会把对象里的 markword 里面的 hashcode 25 ,分代年龄4 位,偏向锁标记位 1 位,所有加起来这30位,直接复制到当前 a 线程 栈帧 lock revord 这样一个区域空间,这就是我们所说的 ”替换 mark word“,偏向锁不存在替换一说。 <br /> 它把这部分东西替换到自己的栈帧里,并用 lock reword 进行保存之后,它会将对象头里的这30位替换成指向自己栈帧 lock record 这样一个记录,就是一个指针。

轻量级锁 升级 重量级锁

什么时候升级

  1. 如果线程 b 要过来竞争这个轻量级锁,应该采用的是 cas 这个操作。它要尝试:<br /> 把自己栈帧中开辟的这个 lock record 里的,这个现在当前是空的对吧,尝试将 mark word 里的hashcode分代年龄以及是否为拍你想锁标记位 30 复制到自己当前线程里边的这个 record 里,但是必然是失败的。<br />==> 因为它发现当前这个对象markword里的指针指向了线程a栈帧中的 lockrecord
  2. 替换失败以后,进行【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 介绍

image.png

强调一下,这几个方法都是Object对象的方法,不是 Thread 类的方法!
并且,想要调用这些方法,必须先获得该对象的锁才行,这是大前提!没获得锁时,调用这些方法会报错,直接说这是非法操作。
至于 notify 唤醒哪一个,这个是根据底层的设定计算的,可以认为是随机唤醒的,我们不能决定。

  1. wait(long n) 有时限的等待, n 毫秒后结束等待,或是被 notify。无形参的那个代表一直等待。

工作原理:Monitor中 的 WaitSet

  1. 分清楚 BLOCKED 和 WAITING 这俩状态区别:

不同点:
(1)BLOCKED:还在等待锁;
(2)WAITING:已经获得了锁,但是放弃了锁,在WaitSet中排队;

相同点:
都处于阻塞状态,不会占用CPU时间.

  1. 何时被唤醒
    对于WAITING的,调用了 Notify 方法或者 notify 方法。
    对于BLOCKED的,Owner 释放锁的时候,就回去唤醒 BLOCKED 队列中的线程。

image.png

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!意味着引用不可变。(不懂)

使用套路

image.png

应用:同步模式——保护性暂停

应用:异步模式——生产者和消费者

AQS

ReentrantLock

Volatile 和 JMM(Java内存模型)

请你解释一下volatile?这个关键词能达到什么效果

至少打出两点:保证可见性,禁止指令重排

如何保证可见性?

(1)“总线”

  1. 让什么可见?让其他线程可见。现在处理器都是多核的处理器,那就有多个CPU,每个CPU理论上能提供1条线程,如果多个CPU对一个volatile修饰的变量修改时,其中一旦一个CPU,即一个线程,拿到修改权限,当他修改后,得立刻推送给主存,推送给主存的过程中,通过“总线”。

(2)MESI 和 CPU嗅探

  1. 其他线程怎么可见的?刚才一直再说其中一条线程,修改完了以后,过“总线”,推送到主存。可见性体现在“主线”这个层面【重要】,一定要记住了。当我们对这个之进行了修改,推导主存的过程中,到“总线”的这一部分,其他CPU一直再在【嗅探】“总线的数据流通”,通过嗅探,在缓存一致性协议(MESI)的保证下,能够嗅探到这条数据的修改。如果自己缓存行有这个数据,那就把自己的缓存行置为“不可用”。如果下次有线程需要进行读/写,需要先从主存拉取这条数据,保存到自己的缓存行,然后再做进一步的处理。
  2. **总线这部分通过 MESI(缓存一致性协议),和嗅探技术一起保证 可见性**

(3)lock前缀指令

  1. 那么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 屏障。 读写 不能指令重排.

  1. load store是全能性屏障,由于这个屏障开销比较大,所以对于很多处理器,目前都不会采用单独的 l-s 屏障进行所有的全功能的指令重排

在特殊情况下,保证原子性

(理解不深入,建议不要说,不然狂问你JVM底层)

i++有三条指令,不能保证原子性。load i ,i add, i store.

volatile对单条字节码指令可以保证原子性,多条就不行

image.png

volatile双重检查锁(后面单独讲)

乐观锁

线程池

JUC - 图9

自定义线程池

image.png

线程池的组成

两个属性: 阻塞队列(任务队列)
线程集合(生产者,一个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

  1. while(task != null || ! (task = taskQueue.take()) == null)

4 拒绝策略:策略模式,用户定义拒绝策略

任务数 > 阻塞队列容量 + coreSize 时,多余的任务没办法被接收,会一直卡在等待队列外边,很影响性能。因此提出了“拒绝策略”这个概念。

阻塞队列的 put 方法 + 线程池的 execute 方法

execute 中,已经调用了 阻塞队列的 Put 方法。此时,需要补充拒绝策略方法,拒绝策略是去实现接口,在线程池实例化时,拒绝策略是构造器中需要实现的属性,由用户自己实现

image.png

ThreadPoolExecutor:JDK 线程池实现

image.png

一、5 种 池状态

状态的表示

ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量.

为什么要用一个数表示两个状态(高低位),而不用两个整数存?
==> 保证二者赋值的原子性。放到两个数,就需要两次原子操作,放到一个数一次就够了.

image.png

状态的含义

刚创建就是 running,可以接收新任务,可以处理阻塞状态任务
调用了 shutdown 方法,温和,不会接受新任务,但是已经提交的任务都会执行
stop 是暴力状态,打断正在执行的方法
tidying 过度状态,准备进入 terminated 状态

从数字上比较,TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING
runing 的 111 最左边的1是符号位,所以转成10进制是-3的意思,最小,而非最大

image.png

二、构造方法的 7 个重要参数

7 个参数介绍

(自定义中KO)corePoolSize:核心线程数目 (最多保留的线程数)
(自定义中KO)maximumPoolSize:最大线程数目
(自定义中KO)workQueue:阻塞队列
(自定义中KO)handler :拒绝策略
keepAliveTime:生存时间 - 针对救急线程
unit: 时间单位 - 针对救急线程
threadFactory :线程工厂 - 可以为线程创建时起个好名字,便于区分线程池的线程和其他线程.

image.png

救济线程介绍(理解7个参数中的 keepAliveTimeunit为什么存在——救济线程存在生存时间

【救济线程与核心线程最大区别】救济线程存在生存时间,核心线程没有生存时间。
当高峰过去后,超过corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由 keepAliveTime 和 unit 来控制。

救急线程数 = maximumPoolSize(总线程数) - corePoolSize(核心线程数)

线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。
当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue 队列排 队,直到有空闲的线程。
救济线程前提:有界队列。如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线 程来救急

image.png

JDK的 4种拒绝策略

image.png
如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现。
RejectedExecutionHandler 是拒绝策略的接口.
(1)AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
(2)CallerRunsPolicy 让调用者运行任务
(3)DiscardPolicy 放弃本次任务
(4)DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之

三、Executors 框架:JDK Executors 类中提供了众多工厂方法来创建各种用途的线程池

newFixedThreadPool:固定大小线程池(Fixed = 固定)

  1. public static ExecutorService newFixedThreadPool(int nThreads) { //【仅传入线程数量】
  2. return new ThreadPoolExecutor(nThreads, nThreads, //核心线程数 == 最大线程数
  3. 0L, TimeUnit.MILLISECONDS, //超时时间 = 0
  4. new LinkedBlockingQueue<Runnable>()); //队列没有指定大小
  5. }

特点 :
核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
阻塞队列是无界的,可以放任意数量的任务 .

评价:
适用于任务量已知,相对耗时的任务

newCachedThreadPool:带缓冲池线程池

  1. public static ExecutorService newCachedThreadPool() {
  2. return new ThreadPoolExecutor(0, Integer.MAX_VALUE, //没有核心线程,全是救济
  3. 60L, TimeUnit.SECONDS,
  4. new SynchronousQueue<Runnable>()); //这是个同步队列.
  5. }

特点 :
(1)核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着
全部都是救急线程(60s 后可以回收).
② 救急线程可以无限创建 .
(2)队列采用了 SynchronousQueue 实现特点是,它没有容量(无界),没有线程来取是放不进去的
(一手交钱、一手交 货)

评价
整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线
程。 适合任务数比较密集,但每个任务执行时间较短的情况。

newSingleThreadExecutor:单线程线程池

  1. public static ExecutorService newSingleThreadExecutor() {
  2. return new FinalizableDelegatedExecutorService
  3. (new ThreadPoolExecutor(1, 1, //只有1个核心线程,无救济线程
  4. 0L, TimeUnit.MILLISECONDS,
  5. new LinkedBlockingQueue<Runnable>())); //有界
  6. }

使用场景
希望多个任务排队执行(串行)
线程数固定为 1,任务数多于 1 时,会放入无界队列排队。
任务执行完毕,这唯一的线程 也不会被释放。

单线程池还能称为池吗?与单线程区别是什么?
① 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一 个线程,保证池的正常工作 .

newFixedThreadPool(固定大小) 和 newSingleThreadExecutor(单线程)区别在哪里?
返回值类型不同,单线程线程池返回值使用装饰器模式进行了包装。
② Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改 .
对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改。

③ Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改 .
FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法 .

提交任务的方法:submit() 和 execute()

本质是 Runnable 和 Callable 的区别.
submit 这里接收结果利用了 “保护性暂停”.

  1. // 执行任务
  2. void execute(Runnable command);
  3. // 提交任务 task,用返回值 Future 获得任务执行结果
  4. <T> Future<T> submit(Callable<T> task); //参数是callable而不是runnable,可以返回结果

提交任务的方法:invokeXxx() 相关方法

  1. // 提交 tasks 中所有任务
  2. <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;
  3. // 提交 tasks 中所有任务,带超时时间
  4. <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
  5. long timeout, TimeUnit unit) throws InterruptedException;
  6. // 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
  7. <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;
  8. // 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间
  9. <T> T invokeAny(Collection<? extends Callable<T>> tasks,
  10. long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;

关闭线程池的方法:shutdown() 和 shutdownNow()

本质是 SHUTDOWN 和 STOP 的区别.
(1)shutdown :void shutdown ();
线程池状态变为 SHUTDOWN
不会接收新任务 ;
但已提交任务会执行完 ;
此方法不会阻塞调用线程的执行 ;

(2)shutdownNow:List shutdownNow();
线程池状态变为 STOP
不会接收新任务
会将队列中的任务返回
并用 interrupt 的方式中断正在执行的任务

线程池异常处理

方法1:主动捉异常,使用 try-catch 自己捕捉
方法2:使用 Future,得有返回值.

ScheduledThreadPoolExecutor :任务调度线程池

image.png

有时希望任务 延时执行 + 定时执行
在『任务调度线程池』功能加入之前,可以使用 java.util.Timer 来实现定时功能,Timer 的优点在于简单易用,但 由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务

Fork/Join

ConcurrentHashMap

Callable

image.png
callable不能直接替换 runnable(如下图报错),需要找一个 二者都能识别的中间件:FutureTask,未来任务。

image.png

image.png

不影响主线程,单开线程.
汇总一次(不懂)
image.png

ThreadLocal

是什么?

辨析 Thread & ThreadLocal & ThreadLocalMap

image.png

image.png

image.png

为什么用弱引用 ?内存泄漏问题?

image.png

image.png
image.png

image.png
….

小节

image.png

代码题

卖火车票

  1. import java.util.concurrent.locks.ReentrantLock;
  2. public class TicketDemo {
  3. public static void main(String[] args) {
  4. Ticket ticket = new Ticket();
  5. Thread t1 = new Thread(ticket, "1号窗口");
  6. Thread t2 = new Thread(ticket, "2号窗口");
  7. Thread t3 = new Thread(ticket, "3号窗口");
  8. t1.start();
  9. t2.start();
  10. t3.start();
  11. }
  12. }
  13. class Ticket implements Runnable {
  14. private static int ticket = 100; //静态
  15. private final ReentrantLock lock = new ReentrantLock();
  16. @Override
  17. public void run() {//重写run方法
  18. while (true) {
  19. //加锁
  20. lock.lock();
  21. //
  22. try {
  23. //有票就卖
  24. if (ticket > 0) {
  25. // 模拟网络阻塞
  26. try {
  27. Thread.sleep(10);
  28. } catch (InterruptedException e) {
  29. e.printStackTrace();
  30. }
  31. // ticket-- 表示卖票了
  32. System.out.println(Thread.currentThread().getName()
  33. + "售出车票,ticket号为:" + ticket--);
  34. } else break;
  35. } catch (Exception e) {
  36. e.printStackTrace();
  37. } finally {
  38. lock.unlock();//解锁,一定要在finally里写
  39. }//try的
  40. }//while的
  41. }//run的
  42. }
  1. import java.util.concurrent.locks.ReentrantLock;
  2. public class TicketDemo {
  3. public static void main(String[] args) {
  4. Ticket ticket = new Ticket();
  5. Thread t1 = new Thread(ticket, "1号窗口");
  6. Thread t2 = new Thread(ticket, "2号窗口");
  7. Thread t3 = new Thread(ticket, "3号窗口");
  8. t1.start();
  9. t2.start();
  10. t3.start();
  11. }
  12. }
  13. class Ticket implements Runnable {
  14. private static int ticket = 100;
  15. private final ReentrantLock lock = new ReentrantLock();
  16. @Override
  17. public void run() {
  18. while (true) {
  19. synchronized (Ticket.class) {
  20. if (ticket > 0) {
  21. try {
  22. Thread.sleep(10);
  23. } catch (InterruptedException e) {
  24. e.printStackTrace();
  25. }
  26. System.out.println(Thread.currentThread().getName()
  27. + "售出车票,ticket号为:" + ticket--);
  28. } else break;
  29. }//synchronized的
  30. }//while的
  31. }
  32. }

生产者消费者

  1. public class ThreadTest {
  2. public static void main(String[] args) {
  3. Clerk clerk = new Clerk();
  4. Thread productThread = new Thread(new Productor(clerk), "生产者1");
  5. Thread consumerThread = new Thread(new Consumer(clerk), "消费者1");
  6. Thread consumerThread2 = new Thread(new Consumer(clerk), "消费者2");
  7. productThread.start();
  8. consumerThread.start();
  9. consumerThread2.start();
  10. }
  11. }
  12. //一、Clerk类
  13. class Clerk {
  14. private int product = 0;
  15. //添加商品
  16. public synchronized void addProduct() {
  17. if (product >= 20) {
  18. try {
  19. wait();
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. } else {
  24. product++;
  25. System.out.println(Thread.currentThread().getName() + "生产了第" + product + "个产品");
  26. notifyAll();
  27. }
  28. }
  29. //得到商品
  30. public synchronized void getProduct() {
  31. if (this.product <= 0) {
  32. try {
  33. wait();
  34. } catch (InterruptedException e) {
  35. e.printStackTrace();
  36. }
  37. } else {
  38. System.out.println(Thread.currentThread().getName() + "消费了第" + product + "个产品");
  39. product--;
  40. notifyAll();
  41. }
  42. }
  43. }
  44. //二、生产者类
  45. class Productor implements Runnable {
  46. Clerk clerk;
  47. public Productor(Clerk clerk) {
  48. this.clerk = clerk;
  49. }
  50. @Override
  51. public void run() {
  52. System.out.println("生产者开始生成产品");
  53. while (true) {
  54. try {
  55. Thread.sleep((int) Math.random() * 1000);
  56. } catch (InterruptedException e) {
  57. e.printStackTrace();
  58. }
  59. clerk.addProduct();
  60. }
  61. }
  62. }
  63. //三、消费者类
  64. class Consumer implements Runnable {
  65. Clerk clerk;
  66. public Consumer(Clerk clerk) {
  67. this.clerk = clerk;
  68. }
  69. @Override
  70. public void run() {
  71. System.out.println("消费者开始消费产品");
  72. while (true) {
  73. try {
  74. Thread.sleep((int) Math.random() * 1000);
  75. } catch (InterruptedException e) {
  76. e.printStackTrace();
  77. }
  78. clerk.getProduct();
  79. }
  80. }
  81. }

单例模式

饿汉式

  1. // 问题1:为什么加 final
  2. // 问题2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例:readResolve()方法
  3. public final class Singleton implements Serializable {
  4. // 问题3:为什么设置为私有? 是否能防止反射创建新的实例?
  5. private Singleton() {}
  6. // 问题4:这样初始化是否能保证单例对象创建时的线程安全?
  7. private static final Singleton INSTANCE = new Singleton();
  8. // 问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public, 说出你知道的理由
  9. public static Singleton getInstance() {
  10. return INSTANCE;
  11. }
  12. public Object readResolve() {
  13. return INSTANCE;
  14. }
  15. }

懒汉式

低性能 懒汉式

  1. public final class Singleton {
  2. private Singleton() { }
  3. private static Singleton INSTANCE = null;
  4. // 分析这里的线程安全, 并说明有什么缺点 ==> 无论如何,都得等待所释放
  5. public static synchronized Singleton getInstance() {
  6. if( INSTANCE != null ){
  7. return INSTANCE;
  8. }
  9. INSTANCE = new Singleton();
  10. return INSTANCE;
  11. }
  12. }

DCL 优化 懒汉式

什么是双重检查锁:
双重检查锁是一个对单例模式的优化,具体做了如下步骤:

  1. 在synchronized外边,做了一个空判断,好处如下:

    1. (相对于直接synchronized)避免用户态到内核态的一个消耗,即不用等锁释放,进行上下文切换
  2. new 对象的时候需要经过如下步骤:

    1. 分配内存空间
    2. 对象初始化
    3. instance 指针指向内存空间
  3. 不使用 volitale修饰的话,可能会出现一个指令重排,会导致如下结果

    1. 如果先执行 instance 指针指向内存空间:

A线程刚执行到new对象,B线程刚判断实例是否为空(即synchronized方法前).
B线程此时拿到的是没有初始化的结果(只分配了空间,还没有调用构造方法,拿到了个半成品)

  1. 为什么加volitale能拿到一个正常的结果:
    1. volitale只能保证单个JVM指令的原子性和可见性
    2. 真正的作用:
      1. 在 new 对象前边添加 store store屏障
      2. 在 new 对象后边添加 store load 屏障
      3. 最终保证 new 对象的过程,对外不可乱序获取,因此保证了线程安全性
  1. public final class Singleton {
  2. private Singleton() { }
  3. // 问题1:解释为什么要加 volatile ?
  4. //==>避免重排序后,拿到的引用,只是分配了内存空间,但是没有来得及调用构造方法!
  5. private static volatile Singleton INSTANCE = null;
  6. // 问题2:对比实现3, 说出这样做的意义
  7. // ==> 创建好后,大家都直接用就行了,不像未优化的版本,就算已经创建了,也得等待锁
  8. public static Singleton getInstance() {
  9. if (INSTANCE != null) return INSTANCE;
  10. synchronized (Singleton.class) {
  11. // 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗
  12. //==>若无:t1 t2都判断外面的实例为null,t1拿到锁创建实例后,t2又进来创建一个.
  13. if (INSTANCE != null) { // t2
  14. return INSTANCE;
  15. }
  16. INSTANCE = new Singleton();
  17. return INSTANCE;
  18. }
  19. }
  20. }

静态内部类 懒汉式

  1. public final class Singleton {
  2. private Singleton() { }
  3. // 问题1:属于懒汉式还是饿汉式(懒汉式,类的创建是懒汉式,第一次加载类时,才会初始化)
  4. private static class LazyHolder {
  5. static final Singleton INSTANCE = new Singleton();
  6. }
  7. // 问题2:在创建时是否有并发问题(JVM负责的类加载是能够保证线程安全的)
  8. public static Singleton getInstance() {
  9. return LazyHolder.INSTANCE; //在这类调用类
  10. }
  11. }

End