并发编程-尚硅谷教程笔记

  1. Java 线程
    3.1 创建和运行线程
    方法一,直接使用 Thread
    方法二,使用 Runnable 配合 Thread
    方法三,FutureTask 配合 Thread
    FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
    3.3 查看进程线程的方法
    linux中
    ps -fe 查看所有进程
    ps -fT -p 查看某个进程(PID)的所有线程
    kill 杀死进程
    top 按大写 H 切换是否显示线程
    top -H -p 查看某个进程(PID)的所有线程
    3.4 * 原理之线程运行
    栈与栈帧
    Java Virtual Machine Stacks (Java 虚拟机栈)
    我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

线程上下文切换(Thread Context Switch)
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
例如:
线程的 cpu 时间片用完
垃圾回收
有更高优先级的线程需要运行
线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念
就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等Context Switch 频繁发生会影响性能
3.5 常见方法

3.6 start 与 run
小结
直接调用 run 是在主线程中执行了 run,没有启动新的线程
使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
3.7 sleep 与 yield
sleep

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  3. 睡眠结束后的线程未必会立刻得到执行
  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

yield

  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  2. 具体的实现依赖于操作系统的任务调度器

线程优先级
线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
3.8 join 方法详解
3.9 interrupt 方法详解
打断 sleep,wait,join 的线程
这几个方法都会让线程进入阻塞状态
可以使用 Thread.interrupted() 清除打断状态
3.10 不推荐的方法
3.11 主线程与守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束
3.12 五种状态
这是从 操作系统 层面来描述的
【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联

【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行

【运行状态】指获取了 CPU 时间片运行中的状态当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换

【阻塞状态】
如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们

【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
3.13 六种状态
这是从 Java API 层面来描述的
根据 Thread.State 枚举,分为六种状态

NEW 线程刚被创建,但是还没有调用 start() 方法

RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分
TERMINATED 当线程代码运行结束
4. 共享模型之管程
本章内容
共享问题
synchronized
线程安全分析
Monitor
wait/notify
线程状态转换
活跃性
Lock
4.1 共享带来的问题
临界区 Critical Section
一个程序运行多个线程本身是没有问题的
问题出在多个线程访问共享资源
多个线程读共享资源其实也没有问题
在多个线程对共享资源读写操作时发生指令交错,就会出现问题
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
4.2 synchronized 解决方案
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
阻塞式的解决方案:synchronized,Lock
非阻塞式的解决方案:原子变量

虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

所谓的“线程八锁”
其实就是考察 synchronized 锁住的是哪个对象

情况四: 是两把锁,所以互相不影响,阻塞的后输出
情况五: 如果一个静态同步方法,一个是非静态同步方法,两个线程都是不同的锁就不会有冲突,一个锁this一个锁class
情况七: 两把锁, 并且一个锁this, 一个锁class, 肯定不冲突
4.4 变量的线程安全分析
成员变量和静态变量是否线程安全?
如果它们没有共享,则线程安全
如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
如果只有读操作,则线程安全
如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?
局部变量是线程安全的
但局部变量引用的对象则未必
如果该对象没有逃离方法的作用访问,它是线程安全的
如果该对象逃离方法的作用范围,需要考虑线程安全

局部变量,会在每个线程的栈帧内存中被创建多份,因此不存在共享
常见线程安全类
String
Integer
StringBuffffer
Random
Vector
Hashtable
java.util.concurrent 包下的类
它们的每个方法是原子的
但注意它们多个方法的组合不是原子的

不可变类线程安全性
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
4.6 Monitor 概念
Java 对象头
Mark Word 结构
4.7 wait notify
小故事 - 为什么需要 wait
由于条件不满足,小南不能继续进行计算
但小南如果一直占用着锁,其它人就得一直阻塞,效率太低
于是老王单开了一间休息室(调用 wait 方法),让小南到休息室(WaitSet)等着去了,但这时锁释放开,
其它人可以由老王随机安排进屋
直到小M将烟送来,大叫一声 [ 你的烟到了 ] (调用 notify 方法)
小南于是可以离开休息室,重新进入竞争锁的队列

原理之 wait / notify
API 介绍
obj.wait() 让进入 object 监视器的线程到 waitSet 等待
obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒
它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法, 在synchronized代码块中使用

final static Object obj = new Object();
public static void main(String[] args) {

new Thread(() -> {
synchronized (obj) {
log.debug(“执行…”);
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(“其它代码…”);
}
}).start();

new Thread(() -> {
synchronized (obj) {
log.debug(“执行…”);
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(“其它代码…”);
}
}).start();

// 主线程两秒后执行
sleep(2);
log.debug(“唤醒 obj 上其它线程”);
synchronized (obj) {
obj.notify(); // 唤醒obj上一个线程
// obj.notifyAll(); // 唤醒obj上所有等待线程
}
}

notify 的一种结果 , 只唤醒一个
20:00:53.096 [Thread-0] c.TestWaitNotify - 执行…
20:00:53.099 [Thread-1] c.TestWaitNotify - 执行…
20:00:55.096 [main] c.TestWaitNotify - 唤醒 obj 上其它线程
20:00:55.096 [Thread-0] c.TestWaitNotify - 其它代码…
notifyAll 的结果, 会唤醒所有正在等待的线程
19:58:15.457 [Thread-0] c.TestWaitNotify - 执行…
19:58:15.460 [Thread-1] c.TestWaitNotify - 执行…
19:58:17.456 [main] c.TestWaitNotify - 唤醒 obj 上其它线程
19:58:17.456 [Thread-1] c.TestWaitNotify - 其它代码…
19:58:17.456 [Thread-0] c.TestWaitNotify - 其它代码…

wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到notify 为止
wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify
4.8 wait notify 的正确姿势(重要)
开始之前先看看sleep(long n) 和 wait(long n) 的区别

  1. sleep是 Thread 方法,而 wait 是 Object 的方法
  2. sleep不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
  3. sleep在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
  4. 它们状态 TIMED_WAITING

4.9 Park & Unpark
基本使用
它们是 LockSupport 类中的方法
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
先 park 再 unpark

特点
与 Object 的 wait & notify 相比
wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】
park & unpark 可以先 unpark,而 wait & notify 不能先 notify
4.10 重新理解线程状态转换
假设有线程 Thread t
情况 1 NEW --> RUNNABLE
当调用 t.start() 方法时,由 NEW --> RUNNABLE

情况 2 RUNNABLE <–> WAITING
t 线程用 synchronized(obj) 获取了对象锁后
调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING
调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
竞争锁成功,t 线程从 WAITING --> RUNNABLE
竞争锁失败,t 线程从 WAITING --> BLOCKED

情况 3 RUNNABLE <–> WAITING
当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING
注意是当前线程在t 线程对象的监视器上等待
t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE

情况 4 RUNNABLE <–> WAITING
当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE

情况 5 RUNNABLE <–> TIMED_WAITING
t 线程用 synchronized(obj) 获取了对象锁后
调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING
t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE
竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED

情况 6 RUNNABLE <–> TIMED_WAITING
当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING
注意是当前线程在t 线程对象的监视器上等待
当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从TIMED_WAITING --> RUNNABLE

情况 7 RUNNABLE <–> TIMED_WAITING
当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE
情况 8 RUNNABLE <–> TIMED_WAITING
当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE --> TIMED_WAITING调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从TIMED_WAITING–> RUNNABLE

情况 9 RUNNABLE <–> BLOCKED
t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED

情况 10 RUNNABLE <–> TERMINATED
当前线程所有代码运行完毕,进入 TERMINATED
4.11 多把锁
多把不相干的锁
一间大屋子有两个功能:睡觉、学习,互不相干。
现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低
解决方法是准备多个房间(多个对象锁)
private final Object studyRoom = new Object();
private final Object bedRoom = new Object();
将锁的粒度细分好处,是可以增强并发度
坏处,如果一个线程需要同时获得多把锁,就容易发生死锁

死锁
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
t1 线程 获得 A对象 锁,接下来想获取 B对象 的锁 t2 线程 获得 B对象 锁,接下来想获取 A对象 的锁

定位死锁
检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁

避免死锁要注意加锁顺序
另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查

哲学家就餐问题
多线程编程题(难道高)

饥饿
很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题
4.13 ReentrantLock
相对于 synchronized 它具备如下特点
可中断
可以设置超时时间
可以设置为公平锁
支持多个条件变量
与 synchronized 一样,都支持可重入

// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}

可打断
ReentrantLock lock = new ReentrantLock();
lock.lockInterruptibly();

注意如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断

锁超时
立刻失败,不等待
lock.tryLock()

超时失败 ,等1秒
lock.tryLock(1, TimeUnit.SECONDS)

公平锁
ReentrantLock 默认是不公平的
ReentrantLock lock = new ReentrantLock(true);
lock.lock();
公平锁一般没有必要,会降低并发度

条件变量
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比 synchronized 是那些不满足条件的线程都在一间休息室等消息
而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
使用要点:
await 前需要获得锁
await 执行后,会释放锁,进入 conditionObject 等待
await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
竞争 lock 锁成功后,从 await 后继续执行
static ReentrantLock lock = new ReentrantLock();
static Condition waitCigaretteQueue = lock.newCondition();
static Condition waitbreakfastQueue = lock.newCondition();
waitCigaretteQueue.await();
waitCigaretteQueue.signal()

本章小结
本章我们需要重点掌握的是
分析多线程访问共享资源时,哪些代码片段属于临界区
使用 synchronized 互斥解决临界区的线程安全问题
掌握 synchronized 锁对象语法
掌握 synchronzied 加载成员方法和静态方法语法
掌握 wait/notify 同步方法
使用 lock 互斥解决临界区的线程安全问题
掌握 lock 的使用细节:可打断、锁超时、公平锁、条件变量
学会分析变量的线程安全性、掌握常见线程安全类的使用
了解线程活跃性问题:死锁、活锁、饥饿

应用方面
互斥:使用 synchronized 或 Lock 达到共享资源互斥效果
同步:使用 wait/notify 或 Lock 的条件变量来达到线程间通信效果

原理方面
monitor、synchronized 、wait/notify 原理
synchronized 进阶原理
park & unpark 原理

模式方面
同步模式之保护性暂停
异步模式之生产者消费者
同步模式之顺序控制
5. 共享模型之内存
深入学习共享变量在多线程间的【可见性】问题与多条指令执行时的【有序性】问题
5.1 Java 内存模型
JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、
CPU 指令优化等。
JMM 体现在以下几个方面
原子性 - 保证指令不会受到线程上下文切换的影响
可见性 - 保证指令不会受 cpu 缓存的影响
有序性 - 保证指令不会受 cpu 指令并行优化的影响
5.2 可见性
退不出的循环
主存和工作内存问题

解决方法
volatile(易变关键字)
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存

可见性 vs 原子性
前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可 见, 不能保证原子性,仅用在一个写线程,多个读线程的情况
注意 synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作,性能相对更低
5.3 有序性
JVM 会在不影响正确性的前提下,可以调整语句的执行顺序
这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性
解决方法
volatile 修饰的变量,可以禁用指令重排

happens-before
happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见

本章小结
本章重点讲解了 JMM 中的
可见性 - 由 JVM 缓存优化引起
有序性 - 由 JVM 指令重排序优化引起

happens-before 规则
原理方面
CPU 指令并行
volatile
6. 共享模型之无锁
本章内容
CAS 与 volatile
原子整数
原子引用
原子累加器
Unsafe
6.2 CAS 与 volatile
前面看到的 AtomicInteger 的解决方法,内部并没有用锁来保护共享变量的线程安全
其中的关键是 compareAndSet,它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作

其实 CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性。
在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。

volatile
获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。

注意
volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)
CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果

为什么无锁效率高
无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。
打个比喻线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速… 恢复到高速运行,代价比较大但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。

CAS 的特点
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思
因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
6.3 原子整数
J.U.C 并发包提供了:
AtomicBoolean
AtomicInteger
AtomicLong

AtomicInteger i = new AtomicInteger(0);
// 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
System.out.println(i.getAndIncrement());
…等API
6.4 原子引用
为什么需要原子引用类型?
AtomicReference
AtomicMarkableReference
AtomicStampedReference

ABA 问题及解决
ABA 问题
仅比较值是不够的,需要再加一个版本号
AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如: A -> B -> A -> C ,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。 但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference
6.5 原子数组
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray

不安全的数组
new int[10]

安全的数组
new AtomicIntegerArray(10)
6.6 字段更新器
AtomicReferenceFieldUpdater // 域 字段
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常
用来更新字段值
6.7 原子累加器
LongAdder 用于++操作
6.8 Unsafe
Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得
本章小结
CAS 与 volatile
API
原子整数
原子引用
原子数组
字段更新器
原子累加器
Unsafe
原理方面
LongAdder 源码
伪共享
7. 共享模型之不可变
本章内容
不可变类的使用
不可变类设计
无状态类设计
7.1 日期转换的问题
问题提出
下面的代码在运行时,由于 SimpleDateFormat 不是线程安全的思路 - 不可变如果一个对象在不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改啊
7.2 不可变设计
另一个大家更为熟悉的 String 类也是不可变的
final 的使用
发现该类、类中所有属性都是 final 的
属性用 final 修饰保证了该属性是只读的,不能修改
类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性

保护性拷贝
通过创建副本对象来避免共享的手段称之为【保护性拷贝(defensive copy)】
7.3 无状态
在web阶段学习时,设计 Servlet 时为了保证其线程安全,都会有这样的建议,不要为 Servlet 设置成员变量,这种没有任何成员变量的类是线程安全的
因为成员变量保存的数据也可以称为状态信息,因此没有成员变量就称之为【无状态】

本章小结
不可变类使用
不可变类设计

  • 原理方面
    final
    模式方面
    享元
  1. 共享模型之工具
    8.1 线程池
  2. 自定义线程池(重要)

步骤1:自定义拒绝策略接口

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

从数字上比较,TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING
这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作进行赋值

  1. 构造方法
    public ThreadPoolExecutor(int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler)

corePoolSize 核心线程数目 (最多保留的线程数)
maximumPoolSize 最大线程数目
keepAliveTime 生存时间 - 针对救急线程
unit 时间单位 - 针对救急线程
workQueue 阻塞队列
threadFactory 线程工厂 - 可以为线程创建时起个好名字
handler 拒绝策略

工作方式:
线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。
当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue 队列排队,直到有空闲的线程。
如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线程来救急。
如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现,其它著名框架也提供了实现
AbortPolicy 让调用者抛出 RejectedExecutionExcep
CallerRunsPolicy 让调用者运行任务
DiscardPolicy 放弃本次任务
DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题
Netty 的实现,是创建一个新线程来执行任务
ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略
PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
当高峰过去后,超过corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由keepAliveTime 和 unit 来控制。

根据这个构造方法,JDK Executors 类中提供了众多工厂方法来创建各种用途的线程池

  1. newFixedThreadPool
    特点
    核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
    阻塞队列是无界的,可以放任意数量的任务
    评价 适用于任务量已知,相对耗时的任务

  2. newCachedThreadPool
    特点
    核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着 全部都是救急线程(60s 后可以回收)救急线程可以无限创建
    队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交货)
    评价 整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线程。 适合任务数比较密集,但每个任务执行时间较短的情况

  3. newSingleThreadExecutor
    使用场景:
    希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。
    区别:
    自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作
    Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改
    FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法
    Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改
    对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改

  4. 提交任务
    // 执行任务
    void execute(Runnable command);
    // 提交任务 task,用返回值 Future 获得任务执行结果
    Future submit(Callable task);
    // 提交 tasks 中所有任务
    List<Future> invokeAll(Collection<? extends Callable> tasks)
    throws InterruptedException;
    // 提交 tasks 中所有任务,带超时时间
    List<Future> invokeAll(Collection<? extends Callable> tasks,
    long timeout, TimeUnit unit) throws InterruptedException;
    // 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
    T invokeAny(Collection<? extends Callable> tasks)
    throws InterruptedException, ExecutionException;
    // 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间
    T invokeAny(Collection<? extends Callable> tasks,
    long timeout, TimeUnit unit)
    throws InterruptedException, ExecutionException, TimeoutException;

  5. 关闭线程池
    shutdown
    线程池状态变为 SHUTDOWN

  • 不会接收新任务
  • 但已提交任务会执行完
  • 此方法不会阻塞调用线程的执行
    shutdownNow
    线程池状态变为 STOP
  • 不会接收新任务
  • 会将队列中的任务返回
  • 并用 interrupt 的方式中断正在执行的任务

其它方法

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

使用 ScheduledExecutorService代替timier

  1. 正确处理执行任务异常
    方法1:主动捉异常
    方法2:使用 Future

  2. Tomcat 线程池
    Tomcat 在哪里用到了线程池呢
    Tomcat 线程池扩展了 ThreadPoolExecutor

  1. Fork/Join
    Fork/Join 是 JDK 1.7 加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的 cpu 密集型运算
    8.2 J.U.C
    • AQS 原理
    • ReentrantLock 原理
  2. 读写锁
    3.1 ReentrantReadWriteLock
    当读操作远远高于写操作时,这时候使用 读写锁
    提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法
    private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
    private ReentrantReadWriteLock.ReadLock r = rw.readLock();
    private ReentrantReadWriteLock.WriteLock w = rw.writeLock();
    r.lock();
    w.lock();
    读锁-读锁 可以并发
    读锁-写锁 相互阻塞
    注意事项
    读锁不支持条件变量
    重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待
    3.2 StampedLock
    该类自 JDK 8 加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合【戳】使用
    加解读锁
    long stamp = lock.readLock();
    lock.unlockRead(stamp);
    加解写锁
    long stamp = lock.writeLock();
    lock.unlockWrite(stamp);
    乐观读,StampedLock 支持 tryOptimisticRead() 方法(乐观读),读取完毕后需要做一次 戳校验 如果校验通
    过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。
    StampedLock 不支持条件变量
    StampedLock 不支持可重入
  3. Semaphore
    基本使用
    // 定义3个
    Semaphore semaphore = new Semaphore(3);
    // 获取
    semaphore.acquire();
    // 释放
    semaphore.release();
  4. CountdownLatch
    CountDownLatch latch = new CountDownLatch(3);
    latch.await();
    latch.countDown();
  5. CyclicBarrier
    [ˈsaɪklɪk ˈbæriɚ] 循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置『计数个数』,每个线程执行到某个需要“同步”的时刻调用 await() 方法进行等待,当等待的线程数满足『计数个数』时,继续执行
    CyclicBarrier cb = new CyclicBarrier(2); // 个数为2时才会继续执行
    cb.await(); // 当个数不足时,等待

注意 CyclicBarrier 与 CountDownLatch 的主要区别在于 CyclicBarrier 是可以重用的 CyclicBarrier 可以被比喻为『人满发车』
7. 线程安全集合类概述
线程安全集合类可以分为三大类:
遗留的线程安全集合如 Hashtable , Vector

使用 Collections 装饰的线程安全集合,如:
Collections.synchronizedCollection
Collections.synchronizedList
Collections.synchronizedMap
Collections.synchronizedSet
Collections.synchronizedNavigableMap
Collections.synchronizedNavigableSet
Collections.synchronizedSortedMap
Collections.synchronizedSortedSet

java.util.concurrent.*
重点介绍 java.util.concurrent.* 下的线程安全集合类,可以发现它们有规律,里面包含三类关键词:
Blocking、CopyOnWrite、Concurrent
Blocking 大部分实现基于锁,并提供用来阻塞的方法
CopyOnWrite 之类容器修改开销相对较重

Concurrent 类型的容器
内部很多操作使用 cas 优化,一般可以提供较高吞吐量
弱一致性
遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍 历,这时内容是旧的
求大小弱一致性,size 操作未必是 100% 准确
读取弱一致性

遍历时如果发生了修改,对于非安全容器来讲,使用 fail-fast 机制也就是让遍历立刻失败,抛出 ConcurrentModifificationException,不再继续遍历
8. ConcurrentHashMap

  • ConcurrentHashMap 原理
  1. BlockingQueue
  • BlockingQueue 原理
  1. ConcurrentLinkedQueue
    ConcurrentLinkedQueue 的设计与 LinkedBlockingQueue 非常像,也是
    两把【锁】,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
    dummy 节点的引入让两把【锁】将来锁住的是不同对象,避免竞争
    只是这【锁】使用了 cas 来实现
  2. CopyOnWriteArrayList
    CopyOnWriteArraySet 是它的马甲 底层实现采用了 写入时拷贝 的思想,增删改操作会将底层数组拷贝一份,更改操作在新数组上执行,这时不影响其它线程的并发读,读写分离
    适合『读多写少』的应用场景

不要觉得弱一致性就不好
数据库的 MVCC 都是弱一致性的表现
并发高和一致性是矛盾的,需要权衡

原理篇
join 原理
是调用者轮询检查线程 alive 状态
join 体现的是【保护性暂停】模式
AQS 原理

  1. 概述
    全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架
    特点:
    用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
    getState - 获取 state 状态
    setState - 设置 state 状态
    compareAndSetState - cas 机制设置 state 状态
    独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源

提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList
条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet

子类主要实现这样一些方法(默认抛出 UnsupportedOperationException)
tryAcquire
tryRelease
tryAcquireShared
tryReleaseShared
isHeldExclusively

AQS 要实现的功能目标
阻塞版本获取锁 acquire 和非阻塞的版本尝试获取锁 tryAcquire
获取锁超时机制
通过打断取消机制
独占机制及共享机制
条件不满足时的等待机制

设计
AQS 的基本思想其实很简单

获取锁的逻辑
while(state 状态不允许获取) {
if(队列中还没有此线程) {
入队并阻塞
}
}
当前线程出队

释放锁的逻辑
if(state 状态允许了) {
恢复阻塞的线程(s)
}

要点
原子维护 state 状态
阻塞及恢复线程
维护队列

  1. state 设计
    state 使用 volatile 配合 cas 保证其修改时的原子性
    state 使用了 32bit int 来维护同步状态,因为当时使用 long 在很多平台下测试的结果并不理想

  2. 阻塞恢复设计
    早期的控制线程暂停和恢复的 api 有 suspend 和 resume,但它们是不可用的,因为如果先调用的 resume那么 suspend 将感知不到
    解决方法是使用 park & unpark 来实现线程的暂停和恢复,具体原理在之前讲过了,先 unpark 再 park 也没问题
    park & unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细
    park 线程还可以通过 interrupt 打断

  3. 队列设计
    使用了 FIFO 先入先出队列,并不支持优先级队列
    设计时借鉴了 CLH 队列,它是一种单向无锁队列

主要用到 AQS 的并发工具类

ReentrantLock 原理
先从构造器开始看,默认为非公平锁实现
5. 条件变量实现原理
每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值