多线程与JUC
- 1.进程与线程
- 2. Java线程
- 3 线程同步(共享模型之管程)
- 4 共享模型之内存(JMM)
- 5 共享模型之无锁(乐观锁CAS)
- 6 共享模型之不可变
- 7 共享模型之工具
- 8 异步编程
- 9 ThreadLocal
1.进程与线程
1.1 进程与线程的概念及对比
**进程:**可以看做是一个正在运行的程序,进程就是用来加载指令、管理内存、管理 IO 的程序
**线程:**线程则是一段指令流,一个进程包含多个线程
管程:Monitor(锁),Monitor其实是一种同步机制,它的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码
区别对比:
- 进程之间是相互独立的,而一个进程包括多个线程
- 进程之间的资源是线程之间所共享的
- 进程是资源的最小分配单元,而线程是最小调度单元
- 进程之间通信更为复杂,需要遵守共同的协议,而线程通信则较为简单,因为线程共享进程之间的内存,可以访问同一个变量
- 线程更为轻量,上下文切换成本较低
1.2并行与并发的概念
**并行:**并行是多条线程在同一时刻执行,通常是多核CPU
**并发:**并发是多条线程在同一段时间内执行,通常是单核CPU,需要切换时间片
2. Java线程
2.1 线程的分类
- 用户线程:是系统的工作线程,它会完成这个程序需要完成的业务操作。
- 守护线程:是一种特殊线程,服务于其它线程,例如GC线程,若没有了服务对象,则自己就暂停运行,无论自己有没有执行完自己的代码。守护线程会伴随着JVM一同结束工作
2.2 线程的创建方式
-
直接继承Thread类
-
实现Runnable接口重写run方法传入Thread对象
// 创建任务对象 Runnable task2 = () -> log.debug("hello"); // 参数1 是任务对象; 参数2 是线程名字 Thread t2 = new Thread(task2, "t2"); t2.start();
-
实现Callable接口重写run方法,并且run方法有返回值且会抛异常,传入FutureTask
// 创建任务对象 FutureTask<Integer> task3 = new FutureTask<>(() -> { log.debug("hello"); return 100; }); // 参数1 是任务对象; 参数2 是线程名字,推荐 new Thread(task3, "t3").start(); // 主线程阻塞,同步等待 task 执行完毕的结果 Integer result = task3.get(); log.debug("结果是:{}", result);
2.3 查看进程线程的方法
-
linux
- ps -fe 查看所有进程
- ps -fT -p 查看某个进程(PID)的所有线程
- kill 杀死进程
- top -H -p 查看某个进程(PID)的所有线程
-
JVM
- jps 命令查看所有 Java 进程
- jstack 查看某个 Java 进程(PID)的所有线程状态
- jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)
2.4 线程的相关方法
方法 | 功能 | 说明 |
---|---|---|
public void start() | 启动一个新线程;Java虚拟机调用此线程的run方法 | start 方法只是让线程进入就绪,里面代码不一定立刻 运行(CPU 的时间片还没分给它)。每个线程对象的 start方法只能调用一次,如果调用了多次会出现 IllegalThreadStateException |
public void run() | 线程启动后调用该方法 | 如果在构造 Thread 对象时传递了 Runnable 参数,则 线程启动后会调用 Runnable 中的 run 方法,否则默 认不执行任何操作。但可以创建 Thread 的子类对象, 来覆盖默认行为 |
public void setName(String name) | 给当前线程取名字 | |
public void getName() | 获取当前线程的名字。线程存在默认名称:子线程是Thread-索引,主线程是main | |
public static Thread currentThread() | 获取当前线程对象,代码在哪个线程中执行 | |
public static void sleep(long time) | 让当前线程休眠多少毫秒再继续执行。Thread.sleep(0) : 让操作系统立刻重新进行一次cpu竞争 | |
public static native void yield() | 提示线程调度器让出当前线程对CPU的使用 | 主要是为了测试和调试 |
public final int getPriority() | 返回此线程的优先级 | |
public final void setPriority(int priority) | 更改此线程的优先级,常用1 5 10 | java中规定线程优先级是1~10 的整数,较大的优先级 能提高该线程被 CPU 调度的机率 |
public void interrupt() | 中断这个线程,异常处理机制 | |
public static boolean interrupted() | 判断当前线程是否被打断,清除打断标记 | |
public boolean isInterrupted() | 判断当前线程是否被打断,不清除打断标记 | |
public final void join() | 等待这个线程结束 | |
public final void join(long millis) | 等待这个线程死亡millis毫秒,0意味着永远等待 | |
public final native boolean isAlive() | 线程是否存活(还没有运行完毕) | |
public final void setDaemon(boolean on) | 将此线程标记为守护线程或用户线程 | |
public long getId() | 获取线程长整型 的 id | id 唯一 |
public state getState() | 获取线程状态 | Java 中线程状态是用 6 个 enum 表示,分别为: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED |
public boolean isInterrupted() | 判断是否被打 断 | 不会清除 打断标记 |
2.4.1 start和run方法
调用run方法:
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug(Thread.currentThread().getName());
FileReader.read(Constants.MP4_FULL_PATH);
}
};
t1.run();
log.debug("do other things ...");
}
先输出main方法相关信息,再输出t1线程do other things,表明还是main线程和t1线程同步
调用start方法:
程序在 t1 线程运行, FileReader.read() 方法调用是异步的
小结:
-
直接调用 run 是在主线程中执行了 run,没有启动新的线程
-
使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
2.4.2 sleep和yield方法(不会释放锁)
sleep方法:
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
yield方法:
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,即让出cpu执行资源,然后调度执行其它线程
- 把执行机会让给优先级相同或更高的线程
- getPriority() : 返回线程优先值
- setPriority(int newPriority) : 改变线程的优先级
2.4.3 join()方法
当某个程序执行流中调用其他线程的 join() 方法时, 调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止 ,例如下面程序代码,在主线程调用t1.join()方法,主线程被阻塞直到t1线程执行完再执行主线程
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
r = 10;
});
t1.start();
//t1.join()
log.debug("结果为:{}", r);
log.debug("结束");
}
2.4.4 interrupt()方法
- 打断 sleep,wait,join 正在处于阻塞状态的线程,并清空打断状态并且抛出异常信息
- 打断正常运行的线程, 不会清空打断状态也不会抛出异常信息
- 打断park线程(LockSupport.park()也会让当前线程进入阻塞状态),调用 interrupt()方法 会让其终止运行
2.5 线程的生命周期
操作系统定义了线程的五种状态 :新建,就绪,运行,阻塞,死亡
JDK中用Thread.State类定义了线程的六种状态 :new,runnable,waiting,timed_waiting,blocked,terminated
runnable包括就绪,运行,阻塞
3 线程同步(共享模型之管程)
3.1 Synchronized的使用方法
-
同步方法 :锁为,静态方法(类名.class) 、 非静态方法(this)
public synchronized void show (String name){ ...... }
-
同步代码块 :锁obj可以使用任何对象作为锁,很多时候也是指定为this或类名.class
synchronized (obj){
// 需要被同步的代码;
}
什么时候会释放锁?
- 当前线程的同步方法、同步代码块执行结束
- 当前线程在同步代码块、同步方法中遇到break、 return终止了该代码块、该方法的继续执行
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception, 导致异常结束
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁
不会释放锁的操作:
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。(方法被废弃)
3.2 线程八锁
线程八锁讨论的问题就是八个案例是否拿住的是同一把锁
3.3 变量的安全问题分析
本质看否被共享
- 成员变量和静态变量一般涉及到读写操作则不是安全的,会被共享
- 局部变量若没有出方法或者没有被引用则是安全的
常见的安全类:String,Integer包装类,StringBuffer,HashTable,JUC包类
3.4 Monitor(非公平的重量级锁,悲观锁)
-
Java对象存放在堆中,分为对象头,实例数据,padding填充
对象头包括:
- 哈希值,GC分代年龄,一些锁的状态信息,比如是否加锁 01表示未加锁,00表示已经上锁
- 类型指针,指向元数据区的该对象类的信息
-
Monitor:即锁的概念,每一个锁对象会通过关联一个唯一的Monitor,当一个线程获得了这把锁就会成为owner,其他线程进入阻塞队列排队
-
synchronized 实现原理----字节码反编译
栈帧:局部变量表和操作数栈
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #2 // <- lock引用 (synchronized开始)入栈
3: dup //复制一份
4: astore_1 // lock引用 -> slot 1
5: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针
6: getstatic #3 // <- i
9: iconst_1 // 准备常数 1压入操作数栈
10: iadd // +1
11: putstatic #3 // -> i
14: aload_1 // <- slot1 lock引用入操作数栈
15: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
16: goto 24
19: astore_2 // e -> slot 2
20: aload_1 // <- lock引用
21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
22: aload_2 // <- slot 2 (e)
23: athrow // throw e 发生异常
24: return
Exception table:
from to target type
6 16 19 any 监测同步代码块是否异常
19 22 19 any
LineNumberTable:
line 8: 0
line 9: 6
line 10: 14
line 11: 24
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 args [Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 19
locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
3.5 轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized
假设有两个方法同步块,利用同一个对象加锁
轻量级锁通过栈帧中的锁记录充当锁
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
- 第一个synchronized创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的对象头
- 然后进行CAS判断,如果锁obj对象头处于01状态即没有线程持有这把锁,那么此线程就能通过将该线程创建的栈帧中的所记录与对象头进行交换,交换完成后锁obj对象头处于00状态,那么则上锁成功了。否则如果失败了,有其他线程持有轻量级锁,则进入锁膨胀,升级为重量级锁
- 对于上述代码的嵌套重入锁进行CAS判断,同样再对方法二创建一个栈帧,但是锁记录为空
- 方法结束后,则弹出栈帧,即锁记录2,到了方法一时,锁记录不为空,则需要进行还原工作,将锁记录和对象头交换回来,
- 如果成功了则释放锁
- 如果失败了则说明锁升级为了重量级锁,走重量级锁解锁流程
3.5.1 锁膨胀
过程:
-
一个线程Thread0刚开始上是轻量级,但是另外有其他的线程Thread1尝试加轻量级锁时进行cas比较失败
-
失败后则需要进行锁膨胀,即需要申请Monitor重量级锁,object对象不再指向Thread0栈帧创建的所记录,而是指向Monitor对象,然后自己进入EntryList的阻塞队列中
- Thread0退出synchronized通过cas进行解锁操作时交换会信息失败,那么进行重量级解锁操作,即设置owner为null,唤醒被阻塞的EntryList中的线程
3.5.2 自旋优化
针对重量级锁,当线程进行锁争夺时,若没有抢到锁则需要自己进行阻塞,但可以通过让当前线程进行重复自旋,等待锁释放,不过会消耗CPU资源
3.6 偏向锁
对象头的格式;
static final Object obj = new Object();
public static void m1() {
synchronized( obj ) {
// 同步块 A
m2();
}
}
public static void m2() {
synchronized( obj ) {
// 同步块 B
m3();
}
}
public static void m3() {
synchronized( obj ) {
// 同步块 C
}
}
针对上述的轻量级锁的可重入操作,每次都需要通过CAS进行判断能否通过新产生的栈帧中的lock record与对象头进行交换,但是每次都是失败的,因此产生了偏向锁的思路
偏向锁:由于可重入锁每次都需要进行CAS操作,那么干脆把当前线程的id存入对象头,只要判断Thread id是否是自己即可
3.6.1撤销偏向锁的操作
- 调用了hashcode方法,会从偏向锁转为轻量级锁,因为Thread id位置被hashcode覆盖了,由biased状态转为normal状态
- 多个线程访问对象锁,会从偏向锁转为轻量级锁
- wait/notify因为是重量级锁的方法,若使用了wait/notify很显然也会从偏向锁转为重量级锁
3.6.2 批量重偏向
对于偏向锁被多个线程在不同的时间访问,但是不存在竞争关系,如果降为轻量级锁显然不太合适,则可以用重偏向解决
批量重偏向操作对于当新的线程想访问偏向锁,如果撤销偏向锁超过一定的阈值20,JVM觉得自己偏向的又问了,就可以将偏向锁的ThreadID更换为新线程的ThreadID
3.6.3 批量撤销
当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向,比如很多线程都想访问锁对象。于是整个类的所有对象 都会变为不可偏向的,新建的对象也是不可偏向的
3.7 锁消除
加锁对象进行逃逸分析没有逃逸,不是一个共享对象,则JIT会优化掉synchronized,性能不会发生太多变化
public class MyBenchmark {
static int x = 0;
@Benchmark
public void a() throws Exception {
x++;
}
@Benchmark
public void b() throws Exception {
Object o = new Object();
synchronized (o) {
x++;
}
}
}
3.8 锁粗化
锁粗化:假如方法中首尾相接,前后相邻的都是同一个锁对象,那JIT编译器会把这几个synchronized块合并为一个大块加粗加大范围,一次申请锁使用即可,避免次次的申请和释放锁,提高了性能
/**
* 锁粗化
* 假如方法中首尾相接,前后相邻的都是同一个锁对象,那JIT编译器会把这几个synchronized块合并为一个大块
* 加粗加大范围,一次申请锁使用即可,避免次次的申请和释放锁,提高了性能
*/
public class LockBigDemo {
static Object objectLock = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (objectLock) {
System.out.println("111111111111");
}
synchronized (objectLock) {
System.out.println("222222222222");
}
synchronized (objectLock) {
System.out.println("333333333333");
}
synchronized (objectLock) {
System.out.println("444444444444");
}
//底层JIT的锁粗化优化
synchronized (objectLock) {
System.out.println("111111111111");
System.out.println("222222222222");
System.out.println("333333333333");
System.out.println("444444444444");
}
}, "t1").start();
}
}
3.9 wait/notify
原理:
- Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁时唤醒
- WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争
某个线程必须获得了锁才能调用这些方法
obj.wait()
让进入 object 监视器的线程到 waitSet 等待obj.notify()
在 object 上正在 waitSet 等待的线程中挑一个唤醒obj.notifyAll()
让 object 上正在 waitSet 等待的线程全部唤醒
它们都是线程之间进行协作通信的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法
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上所有等待线程
}
}
wait()方法有无参和有参方法:wait()无限制等待下去 wait(long timeout)
wait()和sleep()的区别对比:
- sleep 是 Thread 方法,而 wait 是 Object 的方法
- sleep 不需要强制和 synchronized 配合使用,但 wait 需要 和 synchronized 一起用
- sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
- 它们 状态 TIMED_WAITING(状态一样)
3.10 lockSupport类中的park()和unpark()方法
方法api使用:
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
原理:每个线程都会有一个parker对象,分为 _condition, _mutex, _counter
- mutext:互斥锁
- condition:调用park方法会将counter置为0,进入condition条件,若counter原本值为0,则进入阻塞状态,若原本值为1,则会继续运行,都会将counter值置为0
- counter:当调用unpark()时,无论之前的值为0还是1,都会将值变为1,使得被唤醒的对象继续运行
3.11 重新理解线程的六种状态
-
New->Runnable 线程调用start()方法
-
Runnable<->Waiting
- 获得了锁的线程调用wait()方法,runnable->waiting,进入monitor的waitSet等待
- 其他线程使用notify、notifyall、t.interrupt()方法,当被唤醒的线程竞争到了锁则由waiting->runnable,否则进入entrySet一起争夺锁,waiting->blocked
-
Runnable<->Waiting
- 例如当主线程调用t线程的join()方法,则主线程runnable->waiting
- 当t线程执行结束后或者主线程调用interrupt()方法打断自己的阻塞状态,主线程都会由waiting->runnable
-
Runnable<->waiting
- 当前线程调用LockSupport().park()方法,runnable->waiting
- 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE
-
runnable<->Time_Waiting
与情况2相似,针对synchronized当前线程获得锁之后调用了wait(long timeout),那么runnable->TimeWaited,只是有了等待时间,wait()方法是无限等待
-
runnable<->Time_Waiting
与情况3相似,只是调用了join()方法的有参形式,加入了等待时间
-
runnable<->Time_Waiting
当前线程调用了sleep(long n)方法
-
runnable<->Time_Waiting
当前线程调用了LockSupport.parkNanos(long nanos) 或LockSupport.parkUntil(long millis) 时
-
Runnable<->Blocked
线程竞争锁失败Runnable->Blocked,当持有锁的线程执行完代码会唤醒正在monitor阻塞的entryList所有线程,共同竞争锁,竞争成功的线程会从Blocked->Runnable
-
Runnable<->Terminated
3.12 多把锁
通过将锁的粒度缩小,提高不同互不干扰的业务之间的性能
3.13 线程活跃性
3.13.1 死锁
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("lock A");
sleep(1);
synchronized (B) {
log.debug("lock B");
log.debug("操作...");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (B) {
log.debug("lock B");
sleep(0.5);
synchronized (A) {
log.debug("lock A");
log.debug("操作...");
}
}
}, "t2");
t1.start();
t2.start();
两个线程都持有不把不同的锁,但是都互相需要各自手中的锁(哲学家就餐问题)
定位死锁 jps查看进程,jstack查看进程中线程的具体信息定位死锁
死锁的四要素:
- 互斥条件
- 持有并等待条件
- 不可剥夺条件
- 环路等待条件
破坏死锁:
- 破坏持有并等待条件:一个线程必须一次性申请所有的锁,不能单独持有某一个锁
- 破坏不可剥夺条件:一个线程获取不到锁时,就先主动释放持有的所有锁
- 破坏环路等待条件:规定各个线程获取锁的顺序
3.13.2 活锁
活锁:线程没有发生阻塞,但是都执行不下去,例如两个线程都改变对方的结束条件,就可能谁也无法结束
3.13.3 饥饿
饥饿指的是线程因无法访问所需资源而无法执行下去的情况:
- 在CPU繁忙时,如果一个线程优先级太低,就有可能遇到一直得不到执行
- 持有锁的线程,如果执行的时间过长,会导致其他阻塞的线程一直获取不到锁
解决方案:
- 保证资源充足
- 公平地分配资源,如果有需求可以使用公平锁,不过效率较低,很少使用。
- 避免持有锁的线程长时间执行
3.14 ReentrantLock
与synchronized的区别与对比:
- 可中断
- 可以设置超时时间
- 可以设置为公平锁(FIFO)
- 支持多个条件变量
相同点:都是可重入的
基本语法:
// 获取锁
ReentrantLock reentrantLock =new ReentrantLock() ;
//哪个线程调用lock()方法谁就拥有了锁
reentrantLock.lock()
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
3.14.1可重入
略
3.14.2 可打断
使用lock.lockInterruptibly()方法
- 如果没有线程竞争则相当于使用lock.lock()方法
- 如果有竞争则进入阻塞队列,可以被其他线程调用interrupt()方法打断
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("启动...");
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("等锁的过程中被打断");
return;
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {
sleep(1);
t1.interrupt();
log.debug("执行打断");
} finally {
lock.unlock();
}
3.14.3 锁超时
使用lock.tryLock()方法,当前线程去尝试获得锁,失败则直接终止等待下一个时刻去获取锁,成功了则获取锁继续执行
非公平锁与公平锁:非公平锁指的是当锁持有者线程释放锁后,阻塞队列的线程都随机的去抢锁,而不是公平锁先到先得的策略
3.14.4 条件变量
相比于synchronized相当于只有一个条件变量waitSet,无论哪个持有锁的线程调用wait()方法,都进入waitSet等待,而reentrantlock可以设置多个条件变量,使得线程可以进入不同的waiSet等待被唤醒
使用要点 :
- 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() //或者signalAll()
3.15 同步模式之交替打印
- 三个线程交替打印abcabcabcabcabc
/**
* @author hdf
* @create 2023-12-21 17:16
* 三个线程交替打印abc
*/
public class test1 {
public static void main(String[] args) {
WaitNotify waitNotify=new WaitNotify(1,5);
new Thread(()->{
waitNotify.print("a",1,2);
},"t1").start();
new Thread(()->{
waitNotify.print("b",2,3);
},"t2").start();
new Thread(()->{
waitNotify.print("c",3,1);
},"t3").start();
}
}
class WaitNotify{
//打印标记
private int flag;
private int loopNum;
public WaitNotify(int flag, int loopNum) {
this.flag = flag;
this.loopNum = loopNum;
}
public void print(String str,int waitFlag,int nextFlag){
for (int i = 0; i < loopNum; i++) {
synchronized (this){
while(flag!=waitFlag){
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.print(str);
this.notifyAll();
flag=nextFlag;
}
}
}
}
- 两个线程交替打印0-100类的奇偶数
/**
* @author hdf
* @create 2023-12-21 17:46
* 两个线程交替打印1-100奇偶数
*/
//法一 synchronized
public class Test2 {
static int count=0;
static Object obj=new Object();
public static void main(String[] args) {
new Thread(()->{
while(count<100){
synchronized (obj){
if(count%2==0){
System.out.println(Thread.currentThread().getName()+":"+count++);
}
}
}
},"偶数线程").start();
new Thread(()->{
while(count<100){
synchronized (obj){
if(count%2!=0){
System.out.println(Thread.currentThread().getName()+":"+count++);
}
}
}
},"奇数线程").start();
}
}
//法2 synchronized配合wait/notify
public class Test3 {
static int count=0;
public static void main(String[] args) {
Object obj=new Object();
new Thread(()->{
while(count<=100){
synchronized (obj){
obj.notify();
System.out.println(Thread.currentThread().getName()+":"+count++);
try {
obj.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
},"偶数线程").start();
new Thread(()->{
while(count<=100){
synchronized (obj){
obj.notify();
System.out.println(Thread.currentThread().getName()+":"+count++);
try {
obj.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
},"奇数线程").start();
}
}
4 共享模型之内存(JMM)
4.1 Java内存模型
JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。
针对共享变量临界区资源访问JMM的三个特性:
- 原子性 - 保证指令不会受到线程上下文切换的影响 ,
- 可见性 - 保证指令不会受 cpu 缓存的影响
- 有序性 - 保证指令不会受 cpu 指令并行优化的影响
4.2 可见性
可见性指的是不同线程之间对于修改过的值对另外一个线程可见
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
// ....
}
});
t.start();
sleep(1);
run = false; // 线程t不会如预想的停下来
}
原因:是由于Jit编译器会对某一条经常需要从主存读数据的线程进行优化,即将经常访问的数据存入自己的工作内存,但其他线程进行修改后的值自己将会不可见
解决办法:使用volatile关键字,会使得线程必须从主存中操作和读取共享变量
4.3 有序性
保证有序性是为了由于Jvm会对代码进行执行指令重排,有可能导致结果错误,可以使用volatile关键字禁止指令重排
4.4 synchronized和volatile的对比
- 相同点:都能够保证可见性
- 不同点:votile还能保证有序性,synchronized能保证原子性,synchronized关键字不能阻止指令重排,但在一定程度上能保证有序性(如果共享变量没有逃逸出同步代码块的话)。因为在单线程的情况下指令重排不影响结果,相当于保障了有序性。
4.5 volatile的实现原理
volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
- 对 volatile 变量的写指令后会加入写屏障
- 对 volatile 变量的读指令前会加入读屏障
实现原理之可见性:
- 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
public void actor2(I_Result r) {
num = 2;
ready = true; // ready 是 volatile 赋值带写屏障
// 写屏障
}
- 而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
public void actor1(I_Result r) {
// 读屏障
// ready 是 volatile 读取值带读屏障
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
实现原理之有序性:
- 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
public void actor2(I_Result r) {
num = 2;
ready = true; // ready 是 volatile 赋值带写屏障
// 写屏障
}
- 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
public void actor1(I_Result r) {
// 读屏障
// ready 是 volatile 读取值带读屏障
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
4.6 happens-before规则
happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结
5 共享模型之无锁(乐观锁CAS)
5.1 CAS原理及特点
CompareAndSet比较并交换,并且配合volatile一起使用,保持不同线程之间对共享变量的可见性
核心思想是通过比较当前获得的共享变量最新值是否被其他线程修改过,若被修改过,则更改值失败,需要通过不断尝试
CAS 的特点 :
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
- CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再 重试。
- synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想 改,我改完了解开锁,你们才有机会。
- CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思
- 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
- 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
5.2 原子整数
J.U.C 并发包提供了:
- AtomicBoolean
- AtomicInteger
- AtomicLong
5.3 原子引用
为什么需要原子引用类型?
- AtomicReference
- AtomicStampedReference
- AtomicMarkableReference
实际开发的过程中我们使用的不一定是int、long等基本数据类型,也有可能时BigDecimal这样的类型,这时就需要用到原子引用作为容器。原子引用设置值使用的是unsafe.compareAndSwapObject()
方法。原子引用中表示数据的类型需要重写equals()
方法。
**ABA问题:**若某一个线程想把A->C,但是这时其他两个线程先把对A进行了修改,其中一个线程把A->B,另外一个线程又把B->A,导致A值的修改并没有变,使得A进行compareAnsSet时也能成功,事实上当前线程没有感知到A的改变
解决方法:将AtomicReference改用AtomicStampedReference ,加入版本号机制,若关注点并不是想要知道改变的版本号,而是只关注共享变量是否被被改变,则可以使用AtomicMarkableReference
5.4 原子数组
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
若只想修改引用类型里的具体数值,而不是引用本身,那么可以使用原子数组,仅修改数组中某些具体的值
5.5 字段更新器
AtomicIntegerFieldUpdater
:原子更新对象中int类型字段的值AtomicLongFieldUpdater
:原子更新对象中Long类型字段的值AtomicReferenceFieldUpdater
:原子更新对象中引用类型字段的值利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现 异常
5.6 字段累加器LongAdder
DoubleAccumulator
:一个或多个变量,它们一起保持运行double使用所提供的功能更新值DoubleAdder
:一个或多个变量一起保持初始为零double总和LongAccumulator
:一个或多个变量,一起保持使用提供的功能更新运行的值long ,提供了自定义的函数操作LongAdder
:一个或多个变量一起维持初始为零long总和(重点),只能用来计算加法,且从0开始计算
相比原子基本类型做累加操作,使用字段累加器性能更快
性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加 Cell[1]… 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性 能。
LongAdder为什么这么快?
- LongAdder的基本思路就是分散热点,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多,如果要获取真正的long值,只要将各个槽中的变量值累加返回
- Sum()会将所有的Cell数组中的value和base累加作为返回值,核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点。
- 内部有一个base变量,一个Cell[]数组
- base变量:低并发,直接累加到该变量上
- Cell[]数组:高并发,累加进各个线程自己的槽Cell[i]中
LongAdder在无竞争的情况下,跟AtomicLong一样,对同一个base进行操作,当出现竞争关系时则是采用化整为零分散热点的做法,用空间换时间,用一个数组cells,将一个value值拆分进这个数组cells。多个线程需要同时对value进行操作的时候,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cells的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组cells的所有值和base都加起来作为最终结果
5.6 Unsafe对象
上述所有原子类都是基于底层Unsafe对象实现的,Unsafe是一个单例模式,并且需要通过反射获得Unsafe对象
5.7 总结
AtomicLong
-
原理:CAS+自旋
-
场景:低并发下的全局计算,AtomicLong能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性问题
-
缺陷:高并发后性能急剧下降----AtomicLong的自旋会成为瓶颈(N个线程CAS操作修改线程的值,每次只有一个成功过,其他N-1失败,失败的不停自旋直至成功,这样大量失败自旋的情况,一下子cpu就打高了)
LongAdder
-
原理:CAS+Base+Cell数组分散-----空间换时间并分散了热点数据
-
场景:高并发下的全局计算
-
缺陷:sum求和后还有计算线程修改结果的话,最后结果不够准确
6 共享模型之不可变
6.1 不可变定义
指的是多线程操作同一个共享对象时需要保证该对象结果一致,例如多线程使用SimpleDataFormat转换时间日期时会发生转换错误
解决办法:
- 同步 synchronized
- 不可变 一个对象在不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改啊!这样的对象在 Java 中有很多,例如在 Java 8 后,提供了一个新的日期格式化类
6.2 不可变设计
举例:String类
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
// ...
}
说明:
- 将类声明为final,避免被带外星方法的子类继承,从而破坏了不可变性。
- 将字符数组声明为final,避免被修改
- hash虽然不是final的,但是其只有在调用
hash()
方法的时候才被赋值,除此之外再无别的方法修改。
final 的使用
发现该类、类中所有属性都是 final 的
- 属性用 final 修饰保证了该属性是只读的,不能修改
- 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
保护性拷贝:
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
发现其内部是调用 String 的构造方法创建了一个新字符串,再进入这个构造看看,是否对 final char[] value 做出 了修改:
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
结果发现也没有,构造新字符串对象时,会生成新的 char[] value,对内容进行复制 。这种通过创建副本对象来避 免共享的手段称之为保护性拷贝(defensive copy)
6.3 final的实现原理
设置 final 变量的原理
理解了 volatile 原理,再对比 final 的实现就比较简单了
发现 final 变量的赋值也会通过 putfield 指令来完成,同样在这条指令之后也会加入写屏障,这样对final变量的写入不会重排序到构造方法之外,保证在其它线程读到 它的值时不会出现为 0 的情况。普通变量不能保证这一点了。
读取final变量原理
jvm对final变量的访问做出了优化:另一个类中的方法调用final变量是,不是从final变量所在类中获取(共享内存),而是直接复制一份到方法栈栈帧中的操作数栈中(工作内存),这样可以提升效率,是一种优化。
总结:
- 对于较小的static final变量:复制一份到操作数栈中
- 对于较大的static final变量:复制一份到当前类的常量池中
- 对于非静态final变量,优化同上。
6.4 无状态
在 web 阶段学习时,设计 Servlet 时为了保证其线程安全,都会有这样的建议,不要为 Servlet 设置成员变量,这 种没有任何成员变量的类是线程安全的 。
7 共享模型之工具
7.1 线程池
ThreadPoolExecutor
说明:
- ScheduledThreadPoolExecutor是带调度的线程池
- ThreadPoolExecutor是不带调度的线程池
线程池状态
ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态
,低 29 位表示线程数量
状态名 | 高3位 | 接收新任务 | 处理阻塞队列任务 | 说明 |
---|---|---|---|---|
RUNNING | 111 | Y | Y | |
SHUTDOWN | 000 | N | Y | 不会接收新任务,但会处理阻塞队列剩余 任务 |
STOP | 001 | N | N | 会中断正在执行的任务,并抛弃阻塞队列 任务 |
TIDYING | 010 | 任务全执行完毕,活动线程为 0 即将进入 终结 | ||
TERMINATED | 011 | 终结状态 |
从数字上比较,TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING
这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作 进行赋值
构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize 核心线程数目 (最多保留的线程数)
- maximumPoolSize 最大线程数目
- keepAliveTime 生存时间 - 针对救急线程
- unit 时间单位 - 针对救急线程
- workQueue 阻塞队列
- threadFactory 线程工厂 - 可以为线程创建时起个好名字
- handler 拒绝策略
工作方式
当阻塞队列是有界队列并且队列满时,核心线程也在被任务占用时,那么线程池会创建maximumPoolSize -corePoolSize个救急线程来处理新的任务
拒绝策略:
- AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
- CallerRunsPolicy 让调用者运行任务
- DiscardPolicy 放弃本次任务
- DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
根据这个构造方法,JDK Executors 类中提供了众多工厂方法来创建各种用途的线程池。
-
newFixedThreadPool
- 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
- 阻塞队列是无界的,可以放任意数量的任务
-
newCachedThreadPool
- 核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s
- 队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交货)
-
newSingleThreadExecutor
希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程 也不会被释放。
区别:
- 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一 个线程,保证池的正常工作
- Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改
- FinalizableDelegatedExecutorService 应用的是装饰器模式,在调用构造方法时将ThreadPoolExecutor对象传给了内部的ExecutorService接口。只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法,也不能重新设置线程池的大小。
- Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改
- 对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改
提交任务:
// 执行任务
void execute(Runnable command);
// 提交任务 task,用返回值 Future 获得任务执行结果
<T> Future<T> submit(Callable<T> task);
// 提交 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;
关闭线程池:
/*
线程池状态变为 STOP
- 不会接收新任务
- 会将队列中的任务返回
- 并用 interrupt 的方式中断正在执行的任务
*/
List<Runnable> shutdownNow();
任务调度线程池
在『任务调度线程池』功能加入之前(JDK1.3),可以使用 java.util.Timer 来实现定时功能,Timer 的优点在于简单易用,但 由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个 任务的延迟或异常都将会影响到之后的任务。
使用 ScheduledExecutorService 改写:
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
// 添加两个任务,希望它们都在 1s 后执行
executor.schedule(() -> {
System.out.println("任务1,执行时间:" + new Date());
try { Thread.sleep(2000); } catch (InterruptedException e) { }
}, 1000, TimeUnit.MILLISECONDS);
executor.schedule(() -> {
System.out.println("任务2,执行时间:" + new Date());
}, 1000, TimeUnit.MILLISECONDS);
scheduleWithFixedDelay 例子:
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
log.debug("start...");
pool.scheduleWithFixedDelay(()-> {
log.debug("running...");
sleep(2);
}, 1, 1, TimeUnit.SECONDS);
输出分析:一开始,延时 1s,scheduleWithFixedDelay 的间隔是 上一个任务结束 <-> 延时 <-> 下一个任务开始 所 以间隔都是 3s
* 应用之定时任务
如何让每周四 18:00:00 定时执行任务?
// 获得当前时间
LocalDateTime now = LocalDateTime.now();
// 获取本周四 18:00:00.000
LocalDateTime thursday =
now.with(DayOfWeek.THURSDAY).withHour(18).withMinute(0).withSecond(0).withNano(0);
// 如果当前时间已经超过 本周四 18:00:00.000, 那么找下周四 18:00:00.000
if(now.compareTo(thursday) >= 0) {
thursday = thursday.plusWeeks(1);
}
// 计算时间差,即延时执行时间
long initialDelay = Duration.between(now, thursday).toMillis();
// 计算间隔时间,即 1 周的毫秒值
long oneWeek = 7 * 24 * 3600 * 1000;
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
System.out.println("开始时间:" + new Date());
executor.scheduleAtFixedRate(() -> {
System.out.println("执行时间:" + new Date());
}, initialDelay, oneWeek, TimeUnit.MILLISECONDS);
Tomcat线程池
- LimitLatch 用来限流,可以控制最大连接个数,类似 J.U.C 中的 Semaphore 后面再讲
- Acceptor 只负责【接收新的 socket 连接】
- Poller 只负责监听 socket channel 是否有【可读的 I/O 事件】
- 一旦可读,封装一个任务对象(socketProcessor),提交给 Executor 线程池处理
- Executor 线程池中的工作线程最终负责【处理请求】
Tomcat 线程池扩展了 ThreadPoolExecutor,行为稍有不同
- 如果总线程数达到 maximumPoolSize
- 这时不会立刻抛 RejectedExecutionException 异常
- 而是再次尝试将任务放入队列,如果还失败,才抛出 RejectedExecutionException 异常
7.2 Fork/Join线程池
Fork/Join 是 JDK 1.7 加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的 cpu 密集型 运算
所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计 算,如归并排序、斐波那契数列、都可以用分治思想进行求解
Fork/Join 在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运 算效率
Fork/Join 默认会创建与 cpu 核心数大小相同的线程池
7.3 AQS原理
**概述:**全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架
特点:
- 用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取 锁和释放锁
- getState - 获取 state 状态
- setState - 设置 state 状态
- compareAndSetState - cas 机制设置 state 状态
- 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
- 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList
- 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet
子类主要实现这样一些方法(默认抛出 UnsupportedOperationException)
- tryAcquire
- tryRelease
- tryAcquireShared
- tryReleaseShared
- isHeldExclusively
要点
- 原子维护 state 状态
- 阻塞及恢复线程
- 维护队列
- state 设计
- state 使用 volatile 配合 cas 保证其修改时的原子性
- state 使用了 32bit int 来维护同步状态,因为当时使用 long 在很多平台下测试的结果并不理想
- 阻塞恢复设计
- 早期的控制线程暂停和恢复的 api 有 suspend 和 resume,但它们是不可用的,因为如果先调用的 resume 那么 suspend 将感知不到
- 解决方法是使用 park & unpark 来实现线程的暂停和恢复,具体原理在之前讲过了,先 unpark 再 park 也没 问题
- park & unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细
- park 线程还可以通过 interrupt 打断
- 队列设计
- 使用了 FIFO 先入先出队列,并不支持优先级队列
- 设计时借鉴了 CLH 队列,它是一种单向无锁队列
7.4 ReentrantLock实现原理
7.4.1非公平锁实现原理
ReentrantLock默认使用非公平锁
//NonfairSync继承自AbstractQueuedSynchronizer
public ReentrantLock() {
sync = new NonfairSync();
}
加锁解锁流程原理:
加锁调用lock
,尝试将state从0修改为1
- 成功:将owner设为当前线程
- 失败:调用
acquire
->tryAcquire
->nonfairTryAcquire
,判断state=0则获得锁,或者state不为0但当前线程持有锁则重入锁,以上两种情况tryAcquire
返回true,剩余情况返回false。- true:获得锁
- false:调用
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
,其中addwiter
将关联线程的节点插入AQS队列尾部,进入acquireQueued
中的for循环:- 如果当前节点是头节点,并尝试获得锁成功,将当前节点设为头节点,清除此节点信息,返回打断标记。
- 调用
shoudParkAfterFailure
,第一次调用返回false,并将前驱节点改为-1,第二次循环如果再进入此方法,会进入阻塞并检查打断的方法。
解锁调用unlock方法:
-
Thread-0调用tryRelease方法,如果成功则设置exclusiveOwnerThread设为null,state设置为0,并且唤醒最新入队的线程,
-
如果被唤醒的线程竞争锁成功,unpark恢复运行,并将dummy节点出队,将自己设置为dummyNode,并且关联信息设置为null,exclusiveOwnerThread设置为自己,state设置为0
-
如果有其他线程例如Thread-4也来竞争锁,由于是非公平设置,那么有可能Thread-4竞争成功,如果Thread-4竞争成功,那么它又会被阻塞住
-
7.4.2可重入原理
- 加锁是让当前线程所占用的aqs的state状态+1
- 释放锁时让当前线程所占用的aqs的state状态-1,直到state状态状态减为了0才该线程才真正释放锁
7.4.3 可打断原理
-
不可打断原理:当某个线程正在aqs等待队列等待,有其他线程使用unpark去唤醒该线程,只会将打断标志设置为true,并且还是需要进入acquireQueued方法继续进入for循环尝试获得锁,只有真正获得锁时才会被打断
-
可打断原理:同样进入acquireQueued方法继续for循环,如果有其他线程使用unpark去唤醒该线程,那么直接抛出异常跳出循环,线程被打断
7.4.4 公平锁实现原理
简而言之,公平与非公平的区别在于,公平锁中的tryAcquire方法被重写了,新来的线程即便得知了锁的state为0,也要先判断等待队列中是否还有线程等待,只有当队列没有线程等待式,才获得锁。
7.4.5 条件变量Condition实现原理
每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject
await实现流程:
开始 Thread-0 持有锁,调用 await,进入 ConditionObject 的 addConditionWaiter 流程
创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部
接下来进入 AQS 的 fullyRelease 流程,释放同步器上的锁,fullRelease是为了防止锁重入,必须是的state状态变为0
unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功
park 阻塞 Thread-0
signal流程
假设 Thread-1 要来唤醒 Thread-0
进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node
执行 transferForSignal 流程,将该 Node 加入 AQS 队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的 waitStatus 改为 -1
7.5 读写锁(ReentrantReadWriteLock)
ReentrantReadWriteLock:当读操作远远高于写操作时,这时候使用读写锁
让读-读
可以并发,提高性能。 类似于数据库中的select ... from ... lock in share mode
提供一个数据容器类
内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法
读锁与读锁可以并发,读写,读读会阻塞
注意事项
- 读锁不支持条件变量
- 重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待、
- 重入时降级支持:即持有写锁的情况下去获取读锁
读写锁实现原理:与ReentrantLock实现原理几乎一致,在lock与unlock的过程区别在于当线程释放锁时会唤醒dummyHead的所有后继读锁结点
,并将state的标志位全部需要加一,原理很简单,因为读锁支持并发,都可以拿到锁
7.6 StampedLock
该类自 JDK 8 加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合【戳】使用 加解读锁
long stamp = lock.readLock();
lock.unlockRead(stamp);
long stamp = lock.writeLock();
lock.unlockWrite(stamp);
加解写锁
乐观读,StampedLock 支持 tryOptimisticRead() 方法(乐观读),读取完毕后需要做一次 戳校验 如果校验通 过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。
long stamp = lock.tryOptimisticRead();
// 验戳
if(!lock.validate(stamp)){
// 锁升级
}
7.7 Semaphore
[ˈsɛməˌfɔr] 信号量,用来限制能同时访问共享资源的线程上限。
public static void main(String[] args) {
// 1. 创建 semaphore 对象
Semaphore semaphore = new Semaphore(3);
// 2. 10个线程同时运行
for (int i = 0; i < 10; i++) {
new Thread(() -> {
// 3. 获取许可
try {
semaphore.acquire();
//对于非打断式获取,如果此过程中被打断,线程依旧会等到获取了信号量之后才进入catch块。
//catch块中的线程依旧持有信号量,捕获该异常后catch块可以不做任何处理。
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
log.debug("running...");
sleep(1);
log.debug("end...");
} finally {
// 4. 释放许可
semaphore.release();
}
}).start();
}
}
应用:限流
加锁解锁流程:Semaphore有点像一个停车场,permits就好像停车位数量,当线程获得了permits就像是获得了停车位,然后停车场显示空余车位减一。
刚开始,permits(state)为 3,这时 5 个线程来获取资源
假设其中 Thread-1,Thread-2,Thread-4 cas 竞争成功,而 Thread-0 和 Thread-3 竞争失败,进入 AQS 队列 park 阻塞
这时 Thread-4 释放了 permits,状态如下
接下来 Thread-0 竞争成功,permits 再次设置为 0,设置自己为 head 节点,断开原来的 head 节点,unpark 接 下来的 Thread-3 节点,但由于 permits 是 0,因此 Thread-3 在尝试不成功后再次进入 park 状态
加锁流程总结:
acquire
->acquireSharedInterruptibly(1)
->tryAcquireShared(1)
->nonfairTryAcquireShared(1)
,如果资源用完了,返回负数,tryAcquireShared
返回负数,表示失败。否则返回正数,tryAcquireShared
返回正数,表示成功。
- 如果成功,获取信号量成功。
- 如果失败,调用
doAcquireSharedInterruptibly
,进入for循环:- 如果当前驱节点为头节点,调用
tryAcquireShared
尝试获取锁- 如果结果大于等于0,表明获取锁成功,调用
setHeadAndPropagate
,将当前节点设为头节点,之后又调用doReleaseShared
,唤醒后继节点。
- 如果结果大于等于0,表明获取锁成功,调用
- 调用
shoudParkAfterFailure
,第一次调用返回false,并将前驱节点改为-1,第二次循环如果再进入此方法,会进入阻塞并检查打断的方法
- 如果当前驱节点为头节点,调用
解锁流程总结:
release
->sync.releaseShared(1)
->tryReleaseShared(1)
,只要不发生整数溢出,就返回true- 如果返回true,调用
doReleaseShared
,唤醒后继节点。 - 如果返回false,解锁失败。
- 如果返回true,调用
7.8 CountdownLatch
用来进行线程同步协作,等待所有线程完成倒计时。
其中构造参数用来初始化等待计数值,await() 用来等待计数归零,countDown() 用来让计数减一
相比于join,CountDownLatch能配合线程池使用。
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
ExecutorService service = Executors.newFixedThreadPool(4);
service.submit(() -> {
log.debug("begin...");
sleep(1);
latch.countDown();
log.debug("end...{}", latch.getCount());
});
service.submit(() -> {
log.debug("begin...");
sleep(1.5);
latch.countDown();
log.debug("end...{}", latch.getCount());
});
service.submit(() -> {
log.debug("begin...");
sleep(2);
latch.countDown();
log.debug("end...{}", latch.getCount());
});
service.submit(()->{
try {
log.debug("waiting...");
latch.await();
log.debug("wait end...");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
说明:等待多个带有返回值的任务的场景,还是用future比较合适,CountdownLatch适合任务没有返回值的场景。
7.9 CyclicBarrier
CountdownLatch的缺点在于不能重用
循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置『计数个数』,每个线程执 行到某个需要“同步”的时刻调用 await() 方法进行等待,当等待的线程数满足『计数个数』时,继续执行
注意
- CyclicBarrier 与 CountDownLatch 的主要区别在于 CyclicBarrier 是可以重用的 CyclicBarrier 可以被比 喻为『人满发车』
- CountDownLatch的计数和阻塞方法是分开的两个方法,而CyclicBarrier是一个方法。
- CyclicBarrier的构造器还有一个Runnable类型的参数,在计数为0时会执行其中的run方法。
7.10 线程安全集合类
7.10.1 概述
线程安全集合类可以分为三大类:
- 遗留的线程安全集合如
Hashtable
,Vector
- 使用
Collections
装饰的线程安全集合,如:Collections.synchronizedCollection
Collections.synchronizedList
Collections.synchronizedMap
Collections.synchronizedSet
Collections.synchronizedNavigableMap
Collections.synchronizedNavigableSet
Collections.synchronizedSortedMap
Collections.synchronizedSortedSet
- 说明:以上集合均采用修饰模式设计,将非线程安全的集合包装后,在调用方法时包裹了一层synchronized代码块。其并发性并不比遗留的安全集合好。
- java.util.concurrent.*
重点介绍java.util.concurrent.*
下的线程安全集合类,可以发现它们有规律,里面包含三类关键词: Blocking、CopyOnWrite、Concurrent
- Blocking 大部分实现基于锁,并提供用来阻塞的方法
- CopyOnWrite 之类容器修改开销相对较重
- Concurrent 类型的容器
- 内部很多操作使用 cas 优化,一般可以提供较高吞吐量
- 弱一致性
- 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍 历,这时内容是旧的
- 求大小弱一致性,size 操作未必是 100% 准确
- 读取弱一致性
遍历时如果发生了修改,对于非安全容器来讲,使用 fail-fast 机制也就是让遍历立刻失败,抛出 ConcurrentModificationException,不再继续遍历
7.10.2 ConcurrentHashMap
ConcurrentHashMap虽然每个方法都是线程安全的,但是多个方法的组合并不是线程安全的。
JDK8实现:
构造器分析:可以看到实现了懒惰初始化,在构造方法中仅仅计算了 table 的大小,以后在第一次使用时才会真正创建
get流程:
- 如果table不为空且长度大于0且索引位置有元素
- if 头节点key的hash值相等
- 头节点的key指向同一个地址或者equals
- 返回value
- 头节点的key指向同一个地址或者equals
- else if 头节点的hash为负数(bin在扩容或者是treebin)
- 调用find方法查找
- 进入循环(e不为空):
- 节点key的hash值相等,且key指向同一个地址或equals
- 返回value
- 节点key的hash值相等,且key指向同一个地址或equals
- if 头节点key的hash值相等
- 返回null
put 流程 :
- 进入for循环:
- if table为null或者长度 为0
- 初始化表
- else if 索引处无节点
- 创建节点,填入key和value,放入table,退出循环
- else if 索引处节点的hash值为MOVE(ForwardingNode),表示正在扩容和迁移
- 帮忙
- else
- 锁住头节点
- if 再次确认头节点没有被移动
- if 头节点hash值大于0(表示这是一个链表)
- 遍历链表找到对应key,如果没有,创建。
- else if 节点为红黑树节点
- 调用
putTreeVal
查看是否有对应key的数节点- 如果有且为覆盖模式,将值覆盖,返回旧值
- 如果没有,创建并插入,返回null
- 调用
- if 头节点hash值大于0(表示这是一个链表)
- 解锁
- if 再次确认头节点没有被移动
- if binCount不为0
- 如果binCount大于树化阈值8
- 树化
- 如果旧值不为null
- 返回旧值
- break
- 如果binCount大于树化阈值8
- 锁住头节点
- if table为null或者长度 为0
- 增加size计数
- return null
size 计算流程 :
size 计算实际发生在 put,remove 改变集合元素的操作之中
-
没有竞争发生,向 baseCount 累加计数
-
有竞争发生,新建 counterCells,向其中的一个 cell 累加计
- counterCells 初始有两个 cell
- 如果计数竞争比较激烈,会创建新的 cell 来累加计数
-
最后sum进行汇总,会出现弱一致性
总结
Java 8 数组(Node) +( 链表 Node | 红黑树 TreeNode ) 以下数组简称(table),链表简称(bin)
- 初始化,使用 cas 来保证并发安全,懒惰初始化 table
- 树化,当 table.length < 64 时,先尝试扩容,超过 64 时,并且 bin.length > 8 时,会将链表树化,树化过程 会用 synchronized 锁住链表头
- put,如果该 bin 尚未创建,只需要使用 cas 创建 bin;如果已经有了,锁住链表头进行后续 put 操作,元素 添加至 bin 的尾部
- get,无锁操作仅需要保证可见性,扩容过程中 get 操作拿到的是 ForwardingNode 它会让 get 操作在新 table 进行搜索
- 扩容,扩容时以 bin 为单位进行,需要对 bin 进行 synchronized,但这时妙的是其它竞争线程也不是无事可 做,它们会帮助把其它 bin 进行扩容,扩容时平均只有 1/6 的节点会把复制到新 table 中
- size,元素个数保存在 baseCount 中,并发时的个数变动保存在 CounterCell[] 当中。最后统计数量时累加 即可
JDK 7实现
它维护了一个 segment 数组,每个 segment 对应一把锁
- 优点:如果多个线程访问不同的 segment,实际是没有冲突的,这与 jdk8 中是类似的
- 缺点:Segments 数组默认大小为16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化
构造器分析:
其中 this.segmentShift 和 this.segmentMask 的作用是决定将 key 的 hash 结果匹配到哪个 segment 例如,根据某一 hash 值求 segment 位置,先将高位向低位移动 this.segmentShift 位,结果再与 this.segmentMask 做位于运算,最终得到 1010 即下标为 10 的 segment
put流程:使用的是reentrantLock锁,流程与java8类似,头插法
get流程:get 时并未加锁,用了 UNSAFE 方法保证了可见性,扩容过程中,get 先发生就从旧表取内容,get 后发生就从新表取内容
size 计算流程
- 计算元素个数前,先不加锁计算两次,如果前后两次结果如一样,认为个数正确返回
- 如果不一样,进行重试,重试次数超过 3,将所有 segment 锁住,重新计算个数返回
7.10.3 BlockingQueue
原理实现:
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
static class Node<E> {
E item;
/**
* 下列三种情况之一
* - 真正的后继节点
* - 自己, 发生在出队时
* - null, 表示是没有后继节点, 是最后了
*/
Node<E> next;
Node(E x) { item = x; }
}
}
高明之处在于用了两把锁和 dummy 节点
- 用一把锁,同一时刻,最多只允许有一个线程(生产者或消费者,二选一)执行
- 用两把锁,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
- 消费者与消费者线程仍然串行
- 生产者与生产者线程仍然串行
线程安全分析
- 当节点总数大于 2 时(包括 dummy 节点),putLock 保证的是 last 节点的线程安全,takeLock 保证的是 head 节点的线程安全。两把锁保证了入队和出队没有竞争
- 当节点总数等于 2 时(即一个 dummy 节点,一个正常节点)这时候,仍然是两把锁锁两个对象,不会竞争
- 当节点总数等于 1 时(就一个 dummy 节点)这时 take 线程会被 notEmpty 条件阻塞,有竞争,会阻塞
性能比较
主要列举 LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较
- Linked 支持有界,Array 强制有界
- Linked 实现是链表,Array 实现是数组
- Linked 是懒惰的,而 Array 需要提前初始化 Node 数组
- Linked 每次入队会生成新 Node,而 Array 的 Node 是提前创建好的
- Linked 两把锁,Array 一把锁
7.10.4 ConcurrentLinkedQueue
ConcurrentLinkedQueue 的设计与 LinkedBlockingQueue 非常像,也是
- 两把【锁】,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
- dummy 节点的引入让两把【锁】将来锁住的是不同对象,避免竞争
- 只是这【锁】使用了 cas 来实现
7.10.5 CopyOnWriteArrayList
CopyOnWriteArraySet
是它的马甲 底层实现采用了 写入时拷贝 的思想,增删改操作会将底层数组拷贝一份,更改操作在新数组上执行,这时不影响其它线程的并发读,读写分离。 以新增为例:
public boolean add(E e) {
synchronized (lock) {
// 获取旧的数组
Object[] es = getArray();
int len = es.length;
// 拷贝新的数组(这里是比较耗时的操作,但不影响其它读线程)
es = Arrays.copyOf(es, len + 1);
// 添加新元素
es[len] = e;
// 替换旧的数组
setArray(es);
return true;
}
}
其它读操作并未加锁,例如:
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
for (Object x : getArray()) {
@SuppressWarnings("unchecked") E e = (E) x;
action.accept(e);
}
}
适合『读多写少』的应用场景,会存在弱一致性问题
8 异步编程
8.1 Future接口理论
Future接口(FutureTask实现类)定义了操作异步任务执行一些方法,如获取异步任务的执行结果、取消异步任务的执行、判断任务是否被取消、判断任务执行是否完毕等。
举例:比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就去做其他事情了,忙完其他事情或者先执行完,过了一会再才去获取子任务的执行结果或变更的任务状态(老师上课时间想喝水,他继续讲课不结束上课这个主线程,让学生去小卖部帮老师买水完成这个耗时和费力的任务)。
8.2 Future接口作用
-
目的:异步多线程任务执行且返回有结果,三个特点:多线程、有返回、异步任务(班长为老师去买水作为新启动的异步多线程任务且买到水有结果返回)
-
代码实现:Runnable接口+Callable接口+Future接口和FutureTask实现类。
public class CompletableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask = new FutureTask(new MyThread());
Thread t1 = new Thread(futureTask); //开启一个异步线程
t1.start();
System.out.println(futureTask.get()); //有返回hello Callable
}
}
class MyThread implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("--------come in");
return "hello Callable";
}
}
8.3 Future编码实战和优缺点分析
-
优点:Future+线程池异步多线程任务配合,能显著提高程序的运行效率。
-
缺点:
- get()阻塞—一旦调用get()方法求结果,一旦调用不见不散,非要等到结果才会离开,不管你是否计算完成,如果没有计算完成容易程序堵塞。
- isDone()轮询—轮询的方式会耗费无谓的cpu资源,而且也不见得能及时得到计算结果,如果想要异步获取结果,通常会以轮询的方式去获取结果,尽量不要阻塞。
-
结论:Future对于结果的获取不是很友好,只能通过阻塞或轮询的方式得到任务的结果。
8.4 CompletableFuture对Future的改进
- get()方法在Future计算完成之前会一直处在阻塞状态下,阻塞的方式和异步编程的设计理念相违背。
- isDene()方法容易耗费cpu资源(cpu空转),
- 对于真正的异步处理我们希望是可以通过传入回调函数,在Future结束时自动调用该回调函数,这样,我们就不用等待结果
jdk8设计出CompletableFuture,CompletableFuture提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方。
-
接口CompletionStage
- 代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段。
- 一个阶段的执行可能是被单个阶段的完成触发,也可能是由多个阶段一起触发
-
类CompletableFuture
- 提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合CompletableFuture的方法
- 它可能代表一个明确完成的Future,也可能代表一个完成阶段(CompletionStage),它支持在计算完成以后触发一些函数或执行某些动作
四个核心方法
public class CompletableFutureBuildDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(3);
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
System.out.println(Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
},executorService);
System.out.println(completableFuture.get()); //null
CompletableFuture<String> objectCompletableFuture = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "hello supplyAsync";
},executorService);
System.out.println(objectCompletableFuture.get());//hello supplyAsync
executorService.shutdown();
}
}
CompletableFuture减少阻塞和轮询,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。
public class CompletableFutureUseDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(3);
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "---come in");
int result = ThreadLocalRandom.current().nextInt(10);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (result > 5) { //模拟产生异常情况
int i = 10 / 0;
}
System.out.println("----------1秒钟后出结果" + result);
return result;
}, executorService).whenComplete((v, e) -> {
if (e == null) {
System.out.println("计算完成 更新系统" + v);
}
}).exceptionally(e -> {
e.printStackTrace();
System.out.println("异常情况:" + e.getCause() + " " + e.getMessage());
return null;
});
System.out.println(Thread.currentThread().getName() + "先去完成其他任务");
executorService.shutdown();
}
}
/**
* 无异常情况
* pool-1-thread-1---come in
* main先去完成其他任务
* ----------1秒钟后出结果9
* 计算完成 更新系统9
*/
/**
* 有异常情况
*pool-1-thread-1---come in
* main先去完成其他任务
* java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
* 异常情况:java.lang.ArithmeticException: / by zero java.lang.ArithmeticException: / by zero
*/
CompletableFuture优点:
- 异步任务结束时,会自动回调某个对象的方法
- 主线程设置好回调后,不用关心异步任务的执行,异步任务之间可以顺序执行
- 异步任务出错时,会自动回调某个对象的方法
8.5 异步编程案例
8.5.1函数式编程
8.5.2 chain链式调用
public class CompletableFutureMallDemo {
public static void main(String[] args) {
Student student = new Student();
student.setId(1).setStudentName("z3").setMajor("english"); //链式调用
}
}
@AllArgsConstructor
@NoArgsConstructor
@Data
@Accessors(chain = true)//开启链式调用
class Student {
private Integer id;
private String studentName;
private String major;
}
8.5.3 函数式编程+Stream流
/**
* 这里面需要注意一下Stream流方法的使用
* 这种异步查询的方法大大节省了时间消耗
*/
public class CompletableFutureMallDemo {
static List<NetMall> list = Arrays.asList(new NetMall("jd"), new NetMall("taobao"), new NetMall("dangdang"));
/**
* step by step
* @param list
* @param productName
* @return
*/
public static List<String> getPrice(List<NetMall> list, String productName) {
//《Mysql》 in jd price is 88.05
return list
.stream()
.map(netMall ->
String.format("《" + productName + "》" + "in %s price is %.2f",
netMall.getNetMallName(),
netMall.calcPrice(productName)))
.collect(Collectors.toList());
}
/**
* all in
* 把list里面的内容映射给CompletableFuture()
* @param list
* @param productName
* @return
*/
public static List<String> getPriceByCompletableFuture(List<NetMall> list, String productName) {
return list.stream().map(netMall ->
CompletableFuture.supplyAsync(() ->
String.format("《" + productName + "》" + "in %s price is %.2f",
netMall.getNetMallName(),
netMall.calcPrice(productName)))) //Stream<CompletableFuture<String>>
.collect(Collectors.toList()) //List<CompletableFuture<String>>
.stream()//Stream<String>
.map(s -> s.join()).collect(Collectors.toList()); //List<String>
}
public static void main(String[] args) {
/**
* 采用step by setp方式查询
* 《masql》in jd price is 110.11
* 《masql》in taobao price is 109.32
* 《masql》in dangdang price is 109.24
* ------costTime: 3094 毫秒
*/
long StartTime = System.currentTimeMillis();
List<String> list1 = getPrice(list, "masql");
for (String element : list1) {
System.out.println(element);
}
long endTime = System.currentTimeMillis();
System.out.println("------costTime: " + (endTime - StartTime) + " 毫秒");
/**
* 采用 all in三个异步线程方式查询
* 《mysql》in jd price is 109.71
* 《mysql》in taobao price is 110.69
* 《mysql》in dangdang price is 109.28
* ------costTime1009 毫秒
*/
long StartTime2 = System.currentTimeMillis();
List<String> list2 = getPriceByCompletableFuture(list, "mysql");
for (String element : list2) {
System.out.println(element);
}
long endTime2 = System.currentTimeMillis();
System.out.println("------costTime" + (endTime2 - StartTime2) + " 毫秒");
}
}
@AllArgsConstructor
@NoArgsConstructor
@Data
class NetMall {
private String netMallName;
public double calcPrice(String productName) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return ThreadLocalRandom.current().nextDouble() * 2 + productName.charAt(0);
}
}
8.6 CompletableFuture常用方法
-
获得结果和触发计算
-
public T get()
-
public T get(long timeout,TimeUnit unit)
-
public T join() —>和get一样的作用,只是不需要抛出异常
-
public T getNow(T valuelfAbsent) —>计算完成就返回正常值,否则返回备胎值(传入的参数),立即获取结果不阻塞
-
主动触发计算
-
public boolean complete(T value) ---->是否打断get方法立即返回括号值
-
-
对计算结果进行处理
- thenApply —>计算结果存在依赖关系,这两个线程串行化---->由于存在依赖关系(当前步错,不走下一步),当前步骤有异常的话就叫停
- handle —>计算结果存在依赖关系,这两个线程串行化---->有异常也可以往下走一步
public class CompletableFutureApiDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1;
}, threadPool).thenApply(f -> {
System.out.println("222");
return f + 2;
}).handle((f, e) -> {
System.out.println("3333");
int i=10/0;
return f + 2;
// thenApply(f -> {
// System.out.println("3333");
// return f + 2;
}).whenComplete((v, e) -> {
if (e == null) {
System.out.println("----计算结果" + v);
}
}).exceptionally(e -> {
e.printStackTrace();
System.out.println(e.getCause());
return null;
});
System.out.println(Thread.currentThread().getName() + "------主线程先去做其他事情");
}
}
-
对计算结果进行消费
- 接受任务的处理结果,并消费处理,无返回结果
- thenAccept
public class CompletableFutureApi2Demo {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture.supplyAsync(() -> {
return 1;
}, threadPool).thenApply(f -> {
return f + 2;
}).thenApply(f -> {
return f + 2;
}).thenAccept(r -> {
System.out.println(r);//5
});
}
}
-
对比补充
- thenRun(Runnable runnable) :任务A执行完执行B,并且不需要A的结果
- thenAccept(Consumer action): 任务A执行完执行B,B需要A的结果,但是任务B没有返回值
- thenApply(Function fn): 任务A执行完执行B,B需要A的结果,同时任务B有返回值
public class CompletableFutureApi2Demo {
public static void main(String[] args) {
System.out.println(CompletableFuture.supplyAsync(() -> "result").thenRun(() -> {}).join());//null
System.out.println(CompletableFuture.supplyAsync(() -> "result").thenAccept(r -> System.out.println(r)).join());//result null
System.out.println(CompletableFuture.supplyAsync(() -> "result").thenApply(f -> f + 2).join());//result2
}
}
-
CompletableFuture和线程池说明
-
如果没有传入自定义线程池,都用默认线程池ForkJoinPool
-
传入一个线程池,如果你执行第一个任务时,传入了一个自定义线程池
- 调用thenRun方法执行第二个任务时,则第二个任务和第一个任务时共用同一个线程池
- 调用thenRunAsync执行第二个任务时,则第一个任务使用的是你自定义的线程池,第二个任务使用的是ForkJoin线程池
-
-
备注:可能是线程处理太快,系统优化切换原则, 直接使用main线程处理,thenAccept和thenAcceptAsync,thenApply和thenApplyAsync等,之间的区别同理。
-
对计算速度进行选用
- 谁快用谁
- applyToEither
public class CompletableFutureApiDemo {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture<String> playA = CompletableFuture.supplyAsync(() -> {
try {
System.out.println("A come in");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "playA";
}, threadPool);
CompletableFuture<String> playB = CompletableFuture.supplyAsync(() -> {
try {
System.out.println("B come in");
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "playB";
}, threadPool);
CompletableFuture<String> result = playA.applyToEither(playB, f -> {
return f + " is winner";
});
/**
* A come in
* B come in
* main-----------winner:playA is winner
*/
System.out.println(Thread.currentThread().getName() + "-----------winner:" + result.join());
}
}
-
对计算结果进行合并
- 两个CompletableStage任务都完成后,最终能把两个任务的结果一起交给thenCombine来处理
- 先完成的先等着,等待其他分支任务
public class CompletableFutureApi3Demo {
public static void main(String[] args) {
CompletableFuture<Integer> completableFuture1 = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + " 启动");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 10;
});
CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + " 启动");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 20;
});
CompletableFuture<Integer> finalResult = completableFuture1.thenCombine(completableFuture2, (x, y) -> {
System.out.println("----------开始两个结果合并");
return x + y;
});
System.out.println(finalResult.join());
}
}
9 ThreadLocal
9.1 ThreadLocal简介
ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事物ID)与线程关联起来。
用法:
class House {
int saleCount = 0;
public synchronized void saleHouse() {
saleCount++;
}
ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);
public void saleVolumeByThreadLocal() {
saleVolume.set(1 + saleVolume.get());
}
}
public class ThreadLocalDemo {
public static void main(String[] args) {
House house = new House();
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
int size = new Random().nextInt(5) + 1;
try {
for (int j = 1; j <= size; j++) {
house.saleHouse();
house.saleVolumeByThreadLocal();
}
System.out.println(Thread.currentThread().getName() + "\t" + "号销售卖出:" + house.saleVolume.get());
} finally {
house.saleVolume.remove();
}
}, String.valueOf(i)).start();
}
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + "共计卖出多少套: " + house.saleCount);
}
}
/**
* 3 号销售卖出:1
* 4 号销售卖出:3
* 5 号销售卖出:4
* 2 号销售卖出:3
* 1 号销售卖出:5
* main 共计卖出多少套: 16
*/
总结:
-
因为每个Thread内有自己的实例副本且该副本只有当前线程自己使用
-
既然其他ThreadLocal不可访问,那就不存在多线程间共享问题
-
统一设置初始值,但是每个线程对这个值得修改都是各自线程互相独立得
-
如何才能不争抢
- 加入synchronized或者Lock控制资源的访问顺序
- 人手一份,大家各自安好,没有必要争抢
9.2 ThreadLocal源码
9.2.1 Thread、ThreadLocal、ThreadLocalMap关系
-
Thread和ThreadLocal,人手一份,Thread维护了一个ThreadLocalMap
-
ThreadLocal类里面维护了一个ThreadLocalMap静态内部类
- ThreadLocalMap实际上就是一个以ThreadLocal实例为Key,任意对象为value的Entry对象
- 当我们为ThreadLocal变量赋值,实际上就是以当前ThreadLocal实例为Key,值为value的Entry往这个ThreadLocalMap中存放
总结:
- ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以ThreadLocal为Key),不过是经过了两层包装的ThreadLocal对象:
- JVM内部维护了一个线程版的Map<ThreadLocal, Value>(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当作Key,放进了ThreadLocalMap中),每个线程要用到这个T的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量。
9.3 ThreadLocal内存泄漏问题
9.3.1 强软弱虚引用
- 再回首ThreadLocalMap
- 强软弱虚引用
-
强引用:
- 对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收,当一个对象被强引用变量引用时,它处于可达状态,是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到,JVM也不会回收,因此强引用是造成Java内存泄露的主要原因之一。
-
软引用:
- 是一种相对强引用弱化了一些的引用,对于只有软引用的对象而言,当系统内存充足时,不会被回收,当系统内存不足时,他会被回收,软引用通常用在对内存敏感的程序中,比如高速缓存,内存够用就保留,不够用就回收。
-
弱引用:
-
比软引用的生命周期更短,对于只有弱引用的对象而言,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。
-
软引用和弱引用的使用场景----->假如有一个应用需要读取大量的本地图片:
-
如果每次读取图片都从硬盘读取则会严重影响性能
-
如果一次性全部加载到内存中又可能会造成内存溢出
-
此时使用软应用来解决,设计思路时:用一个HashMap来保存图片的路径和与相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,有效避免了OOM的问题
-
-
虚引用:
- 虚引用必须和引用队列联合使用,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都有可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象。
- 虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize后,做某些事情的通知机制。换句话说就是在对象被GC的时候会收到一个系统通知或者后续添加进一步的处理,用来实现比finalize机制更灵活的回收操作。
9.3.2 为什么要用弱引用?不用如何?
-
为什么要用弱引用:
- 当方法执行完毕后,栈帧销毁,强引用t1也就没有了,但此时线程的ThreadLocalMap里某个entry的Key引用还指向这个对象,若这个Key是强引用,就会导致Key指向的ThreadLocal对象即V指向的对象不能被gc回收,造成内存泄露
- 若这个引用时弱引用就大概率会减少内存泄漏的问题(当然,还得考虑key为null这个坑),使用弱引用就可以使ThreadLocal对象在方法执行完毕后顺利被回收且entry的key引用指向为null
-
这里有个需要注意的问题:
- ThreadLocalMap使用ThreadLocal的弱引用作为Key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc时,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现Key为null的Entry,就没有办法访问这些Key为null的Entry的value,如果当前线程迟迟不结束的话(好比正在使用线程池),这些key为null的Entry的value就会一直存在一条强引用链
- 虽然弱引用,保证了Key指向的ThreadLocal对象能够被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value,因此弱引用不能100%保证内存不泄露,我们要在不使用某个ThreadLocal对象后,手动调用remove方法来删除它,尤其是在线程池中,不仅仅是内存泄漏的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。
9.3.3 总结
-
ThreadLocal并不解决线程间共享数据的问题
-
ThreadLocal适用于变量在线程间隔离且在方法间共享的场景
-
ThreadLocal通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
-
每个线程持有一个只属于它自己的专属map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有他的线程访问,故不存在线程安全以及锁的问题
-
ThreadLocalMap的Entry对ThreadLocal的引用为弱引用。避免了ThreadLocal对象无法被回收的问题
-
都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法回收键为null的Entry对象的值(即为具体实例)以及entry对象本身从而防止内存泄漏,属于安全加固的方法
-
ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以ThreadLocal为Key),不过是经过了两层包装的ThreadLocal对象: