多线程与JUC
- 1.进程与线程
- 1.1 进程与线程的概念及对比
- 1.2并行与并发的概念
- 2. Java线程
- 2.1 线程的分类
- 2.2 线程的创建方式
- 2.3 查看进程线程的方法
- 2.4 线程的相关方法
- 2.4.1 start和run方法
- 2.4.2 sleep和yield方法(不会释放锁)
- 2.4.3 join()方法
- 2.4.4 interrupt()方法
- 2.5 线程的生命周期
- 3 线程同步(共享模型之管程)
- 3.1 Synchronized的使用方法
- 3.2 线程八锁
- 3.3 变量的安全问题分析
- 3.4 Monitor(非公平的重量级锁,悲观锁)
- 3.5 轻量级锁
- 3.5.1 锁膨胀
- 3.5.2 自旋优化
- 3.6 偏向锁
- 3.6.1撤销偏向锁的操作
- 3.6.2 批量重偏向
- 3.6.3 批量撤销
- 3.7 锁消除
- 3.8 锁粗化
- 3.9 wait/notify
- 3.10 lockSupport类中的park()和unpark()方法
- 3.11 重新理解线程的六种状态
- 3.12 多把锁
- 3.13 线程活跃性
- 3.13.1 死锁
- 3.13.2 活锁
- 3.13.3 饥饿
- 3.14 ReentrantLock
- 3.14.1可重入
- 3.14.2 可打断
- 3.14.3 锁超时
- 3.14.4 条件变量
- 3.15 同步模式之交替打印
- 4 共享模型之内存(JMM)
- 4.1 Java内存模型
- 4.2 可见性
- 4.3 有序性
- 4.4 synchronized和volatile的对比
- 4.5 volatile的实现原理
- 4.6 happens-before规则
- 5 共享模型之无锁(乐观锁CAS)
- 5.1 CAS原理及特点
- 5.2 原子整数
- 5.3 原子引用
- 5.4 原子数组
- 5.5 字段更新器
- 5.6 字段累加器LongAdder
- 5.7 总结
- 6 共享模型之不可变
- 6.1 不可变定义
- 6.2 不可变设计
- 6.3 final的实现原理
- 6.4 无状态
- 7 共享模型之工具
- 7.1 线程池
- 7.2 Fork/Join线程池
- 7.3 AQS原理
- 7.4 ReentrantLock实现原理
- 7.4.1非公平锁实现原理
- 7.4.2可重入原理
- 7.4.3 可打断原理
- 7.4.4 公平锁实现原理
- 7.4.5 条件变量Condition实现原理
- 7.5 读写锁(ReentrantReadWriteLock)
- 7.6 StampedLock
- 7.7 Semaphore
- 7.8 CountdownLatch
- 7.9 CyclicBarrier
- 7.10 线程安全集合类
- 7.10.1 概述
- 7.10.2 ConcurrentHashMap
- 7.10.3 BlockingQueue
- 7.10.4 ConcurrentLinkedQueue
- 7.10.5 CopyOnWriteArrayList
- 8 异步编程
- 8.1 Future接口理论
- 8.2 Future接口作用
- 8.3 Future编码实战和优缺点分析
- 8.4 CompletableFuture对Future的改进
- 8.5 异步编程案例
- 8.5.1函数式编程
- 8.5.2 chain链式调用
- 8.5.3 函数式编程+Stream流
- 9 ThreadLocal
- 9.1 ThreadLocal简介
- 9.2 ThreadLocal源码
- 9.2.1 Thread、ThreadLocal、ThreadLocalMap关系
- 9.3 ThreadLocal内存泄漏问题
- 9.3.1 强软弱虚引用
- 9.3.2 为什么要用弱引用?不用如何?
- 9.3.3 总结
**进程:**可以看做是一个正在运行的程序,进程就是用来加载指令、管理内存、管理 IO 的程序
**线程:**线程则是一段指令流,一个进程包含多个线程
管程:Monitor(锁),Monitor其实是一种同步机制,它的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码
区别对比:
**并行:**并行是多条线程在同一时刻执行,通常是多核CPU
**并发:**并发是多条线程在同一段时间内执行,通常是单核CPU,需要切换时间片
直接继承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);
linux
JVM
方法 | 功能 | 说明 |
---|---|---|
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() | 判断是否被打 断 | 不会清除 打断标记 |
调用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 中的代码
sleep方法:
yield方法:
当某个程序执行流中调用其他线程的 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("结束");
}
操作系统定义了线程的五种状态 :新建,就绪,运行,阻塞,死亡
JDK中用Thread.State类定义了线程的六种状态 :new,runnable,waiting,timed_waiting,blocked,terminated
runnable包括就绪,运行,阻塞
同步方法 :锁为,静态方法(类名.class) 、 非静态方法(this)
public synchronized void show (String name){
......
}
同步代码块 :锁obj可以使用任何对象作为锁,很多时候也是指定为this或类名.class
synchronized (obj){
// 需要被同步的代码;
}
什么时候会释放锁?
不会释放锁的操作:
线程八锁讨论的问题就是八个案例是否拿住的是同一把锁
本质看否被共享
常见的安全类:String,Integer包装类,StringBuffer,HashTable,JUC包类
Java对象存放在堆中,分为对象头,实例数据,padding填充
对象头包括:
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
轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized
假设有两个方法同步块,利用同一个对象加锁
轻量级锁通过栈帧中的锁记录充当锁
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
过程:
一个线程Thread0刚开始上是轻量级,但是另外有其他的线程Thread1尝试加轻量级锁时进行cas比较失败
失败后则需要进行锁膨胀,即需要申请Monitor重量级锁,object对象不再指向Thread0栈帧创建的所记录,而是指向Monitor对象,然后自己进入EntryList的阻塞队列中
针对重量级锁,当线程进行锁争夺时,若没有抢到锁则需要自己进行阻塞,但可以通过让当前线程进行重复自旋,等待锁释放,不过会消耗CPU资源
对象头的格式;
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是否是自己即可
对于偏向锁被多个线程在不同的时间访问,但是不存在竞争关系,如果降为轻量级锁显然不太合适,则可以用重偏向解决
批量重偏向操作对于当新的线程想访问偏向锁,如果撤销偏向锁超过一定的阈值20,JVM觉得自己偏向的又问了,就可以将偏向锁的ThreadID更换为新线程的ThreadID
当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向,比如很多线程都想访问锁对象。于是整个类的所有对象 都会变为不可偏向的,新建的对象也是不可偏向的
加锁对象进行逃逸分析没有逃逸,不是一个共享对象,则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++;
}
}
}
锁粗化:假如方法中首尾相接,前后相邻的都是同一个锁对象,那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();
}
}
原理:
某个线程必须获得了锁才能调用这些方法
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()的区别对比:
方法api使用:
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
原理:每个线程都会有一个parker对象,分为 _condition, _mutex, _counter
New->Runnable 线程调用start()方法
Runnable<->Waiting
Runnable<->Waiting
Runnable<->waiting
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
通过将锁的粒度缩小,提高不同互不干扰的业务之间的性能
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查看进程中线程的具体信息定位死锁
死锁的四要素:
破坏死锁:
活锁:线程没有发生阻塞,但是都执行不下去,例如两个线程都改变对方的结束条件,就可能谁也无法结束
饥饿指的是线程因无法访问所需资源而无法执行下去的情况:
解决方案:
与synchronized的区别与对比:
相同点:都是可重入的
基本语法:
// 获取锁
ReentrantLock reentrantLock =new ReentrantLock() ;
//哪个线程调用lock()方法谁就拥有了锁
reentrantLock.lock()
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
略
使用lock.lockInterruptibly()方法
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();
}
使用lock.tryLock()方法,当前线程去尝试获得锁,失败则直接终止等待下一个时刻去获取锁,成功了则获取锁继续执行
非公平锁与公平锁:非公平锁指的是当锁持有者线程释放锁后,阻塞队列的线程都随机的去抢锁,而不是公平锁先到先得的策略
相比于synchronized相当于只有一个条件变量waitSet,无论哪个持有锁的线程调用wait()方法,都进入waitSet等待,而reentrantlock可以设置多个条件变量,使得线程可以进入不同的waiSet等待被唤醒
使用要点 :
static ReentrantLock lock = new ReentrantLock();
static Condition waitCigaretteQueue = lock.newCondition();
static Condition waitbreakfastQueue = lock.newCondition();
waitCigaretteQueue.await()
waitCigaretteQueue.signal() //或者signalAll()
/**
* @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;
}
}
}
}
/**
* @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();
}
}
JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。
针对共享变量临界区资源访问JMM的三个特性:
可见性指的是不同线程之间对于修改过的值对另外一个线程可见
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关键字,会使得线程必须从主存中操作和读取共享变量
保证有序性是为了由于Jvm会对代码进行执行指令重排,有可能导致结果错误,可以使用volatile关键字禁止指令重排
volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
实现原理之可见性:
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;
}
}
实现原理之有序性:
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;
}
}
happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结
CompareAndSet比较并交换,并且配合volatile一起使用,保持不同线程之间对共享变量的可见性
核心思想是通过比较当前获得的共享变量最新值是否被其他线程修改过,若被修改过,则更改值失败,需要通过不断尝试
CAS 的特点 :
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
J.U.C 并发包提供了:
为什么需要原子引用类型?
实际开发的过程中我们使用的不一定是int、long等基本数据类型,也有可能时BigDecimal这样的类型,这时就需要用到原子引用作为容器。原子引用设置值使用的是unsafe.compareAndSwapObject()
方法。原子引用中表示数据的类型需要重写equals()
方法。
**ABA问题:**若某一个线程想把A->C,但是这时其他两个线程先把对A进行了修改,其中一个线程把A->B,另外一个线程又把B->A,导致A值的修改并没有变,使得A进行compareAnsSet时也能成功,事实上当前线程没有感知到A的改变
解决方法:将AtomicReference改用AtomicStampedReference ,加入版本号机制,若关注点并不是想要知道改变的版本号,而是只关注共享变量是否被被改变,则可以使用AtomicMarkableReference
若只想修改引用类型里的具体数值,而不是引用本身,那么可以使用原子数组,仅修改数组中某些具体的值
AtomicIntegerFieldUpdater
:原子更新对象中int类型字段的值AtomicLongFieldUpdater
:原子更新对象中Long类型字段的值AtomicReferenceFieldUpdater
:原子更新对象中引用类型字段的值利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现 异常DoubleAccumulator
:一个或多个变量,它们一起保持运行double使用所提供的功能更新值DoubleAdder
:一个或多个变量一起保持初始为零double总和LongAccumulator
:一个或多个变量,一起保持使用提供的功能更新运行的值long ,提供了自定义的函数操作LongAdder
:一个或多个变量一起维持初始为零long总和(重点),只能用来计算加法,且从0开始计算相比原子基本类型做累加操作,使用字段累加器性能更快
性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加 Cell[1]… 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性 能。
LongAdder为什么这么快?
LongAdder在无竞争的情况下,跟AtomicLong一样,对同一个base进行操作,当出现竞争关系时则是采用化整为零分散热点的做法,用空间换时间,用一个数组cells,将一个value值拆分进这个数组cells。多个线程需要同时对value进行操作的时候,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cells的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组cells的所有值和base都加起来作为最终结果
5.6 Unsafe对象
上述所有原子类都是基于底层Unsafe对象实现的,Unsafe是一个单例模式,并且需要通过反射获得Unsafe对象
AtomicLong
原理:CAS+自旋
场景:低并发下的全局计算,AtomicLong能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性问题
缺陷:高并发后性能急剧下降----AtomicLong的自旋会成为瓶颈(N个线程CAS操作修改线程的值,每次只有一个成功过,其他N-1失败,失败的不停自旋直至成功,这样大量失败自旋的情况,一下子cpu就打高了)
LongAdder
原理:CAS+Base+Cell数组分散-----空间换时间并分散了热点数据
场景:高并发下的全局计算
缺陷:sum求和后还有计算线程修改结果的话,最后结果不够准确
指的是多线程操作同一个共享对象时需要保证该对象结果一致,例如多线程使用SimpleDataFormat转换时间日期时会发生转换错误
解决办法:
举例: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
// ...
}
说明:
hash()
方法的时候才被赋值,除此之外再无别的方法修改。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)
设置 final 变量的原理
理解了 volatile 原理,再对比 final 的实现就比较简单了
发现 final 变量的赋值也会通过 putfield 指令来完成,同样在这条指令之后也会加入写屏障,这样对final变量的写入不会重排序到构造方法之外,保证在其它线程读到 它的值时不会出现为 0 的情况。普通变量不能保证这一点了。
读取final变量原理
jvm对final变量的访问做出了优化:另一个类中的方法调用final变量是,不是从final变量所在类中获取(共享内存),而是直接复制一份到方法栈栈帧中的操作数栈中(工作内存),这样可以提升效率,是一种优化。
总结:
在 web 阶段学习时,设计 Servlet 时为了保证其线程安全,都会有这样的建议,不要为 Servlet 设置成员变量,这 种没有任何成员变量的类是线程安全的 。
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)
工作方式
当阻塞队列是有界队列并且队列满时,核心线程也在被任务占用时,那么线程池会创建maximumPoolSize -corePoolSize个救急线程来处理新的任务
拒绝策略:
根据这个构造方法,JDK Executors 类中提供了众多工厂方法来创建各种用途的线程池。
newFixedThreadPool
newCachedThreadPool
newSingleThreadExecutor
希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程 也不会被释放。
区别:
提交任务:
// 执行任务
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线程池
Tomcat 线程池扩展了 ThreadPoolExecutor,行为稍有不同
Fork/Join 是 JDK 1.7 加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的 cpu 密集型 运算
所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计 算,如归并排序、斐波那契数列、都可以用分治思想进行求解
Fork/Join 在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运 算效率
Fork/Join 默认会创建与 cpu 核心数大小相同的线程池
**概述:**全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架
特点:
子类主要实现这样一些方法(默认抛出 UnsupportedOperationException)
要点
ReentrantLock默认使用非公平锁
//NonfairSync继承自AbstractQueuedSynchronizer
public ReentrantLock() {
sync = new NonfairSync();
}
加锁解锁流程原理:
加锁调用lock
,尝试将state从0修改为1
acquire
->tryAcquire
->nonfairTryAcquire
,判断state=0则获得锁,或者state不为0但当前线程持有锁则重入锁,以上两种情况tryAcquire
返回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竞争成功,那么它又会被阻塞住
不可打断原理:当某个线程正在aqs等待队列等待,有其他线程使用unpark去唤醒该线程,只会将打断标志设置为true,并且还是需要进入acquireQueued方法继续进入for循环尝试获得锁,只有真正获得锁时才会被打断
可打断原理:同样进入acquireQueued方法继续for循环,如果有其他线程使用unpark去唤醒该线程,那么直接抛出异常跳出循环,线程被打断
简而言之,公平与非公平的区别在于,公平锁中的tryAcquire方法被重写了,新来的线程即便得知了锁的state为0,也要先判断等待队列中是否还有线程等待,只有当队列没有线程等待式,才获得锁。
每个条件变量其实就对应着一个等待队列,其实现类是 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
ReentrantReadWriteLock:当读操作远远高于写操作时,这时候使用读写锁
让读-读
可以并发,提高性能。 类似于数据库中的select ... from ... lock in share mode
提供一个数据容器类
内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法
读锁与读锁可以并发,读写,读读会阻塞
注意事项
读写锁实现原理:与ReentrantLock实现原理几乎一致,在lock与unlock的过程区别在于当线程释放锁时会唤醒dummyHead的所有后继读锁结点
,并将state的标志位全部需要加一,原理很简单,因为读锁支持并发,都可以拿到锁
该类自 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)){
// 锁升级
}
[ˈ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
尝试获取锁
setHeadAndPropagate
,将当前节点设为头节点,之后又调用doReleaseShared
,唤醒后继节点。shoudParkAfterFailure
,第一次调用返回false,并将前驱节点改为-1,第二次循环如果再进入此方法,会进入阻塞并检查打断的方法解锁流程总结:
release
->sync.releaseShared(1)
->tryReleaseShared(1)
,只要不发生整数溢出,就返回true
doReleaseShared
,唤醒后继节点。用来进行线程同步协作,等待所有线程完成倒计时。
其中构造参数用来初始化等待计数值,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适合任务没有返回值的场景。
CountdownLatch的缺点在于不能重用
循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置『计数个数』,每个线程执 行到某个需要“同步”的时刻调用 await() 方法进行等待,当等待的线程数满足『计数个数』时,继续执行
注意
- CyclicBarrier 与 CountDownLatch 的主要区别在于 CyclicBarrier 是可以重用的 CyclicBarrier 可以被比 喻为『人满发车』
- CountDownLatch的计数和阻塞方法是分开的两个方法,而CyclicBarrier是一个方法。
- CyclicBarrier的构造器还有一个Runnable类型的参数,在计数为0时会执行其中的run方法。
线程安全集合类可以分为三大类:
Hashtable
,Vector
Collections
装饰的线程安全集合,如:
Collections.synchronizedCollection
Collections.synchronizedList
Collections.synchronizedMap
Collections.synchronizedSet
Collections.synchronizedNavigableMap
Collections.synchronizedNavigableSet
Collections.synchronizedSortedMap
Collections.synchronizedSortedSet
重点介绍java.util.concurrent.*
下的线程安全集合类,可以发现它们有规律,里面包含三类关键词: Blocking、CopyOnWrite、Concurrent
遍历时如果发生了修改,对于非安全容器来讲,使用 fail-fast 机制也就是让遍历立刻失败,抛出 ConcurrentModificationException,不再继续遍历
ConcurrentHashMap虽然每个方法都是线程安全的,但是多个方法的组合并不是线程安全的。
JDK8实现:
构造器分析:可以看到实现了懒惰初始化,在构造方法中仅仅计算了 table 的大小,以后在第一次使用时才会真正创建
get流程:
put 流程 :
putTreeVal
查看是否有对应key的数节点
size 计算流程 :
size 计算实际发生在 put,remove 改变集合元素的操作之中
没有竞争发生,向 baseCount 累加计数
有竞争发生,新建 counterCells,向其中的一个 cell 累加计
最后sum进行汇总,会出现弱一致性
总结
Java 8 数组(Node) +( 链表 Node | 红黑树 TreeNode ) 以下数组简称(table),链表简称(bin)
JDK 7实现
它维护了一个 segment 数组,每个 segment 对应一把锁
其中 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 计算流程
原理实现:
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 节点
线程安全分析
性能比较
主要列举 LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较
ConcurrentLinkedQueue 的设计与 LinkedBlockingQueue 非常像,也是
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);
}
}
适合『读多写少』的应用场景,会存在弱一致性问题
Future接口(FutureTask实现类)定义了操作异步任务执行一些方法,如获取异步任务的执行结果、取消异步任务的执行、判断任务是否被取消、判断任务执行是否完毕等。
举例:比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就去做其他事情了,忙完其他事情或者先执行完,过了一会再才去获取子任务的执行结果或变更的任务状态(老师上课时间想喝水,他继续讲课不结束上课这个主线程,让学生去小卖部帮老师买水完成这个耗时和费力的任务)。
目的:异步多线程任务执行且返回有结果,三个特点:多线程、有返回、异步任务(班长为老师去买水作为新启动的异步多线程任务且买到水有结果返回)
代码实现: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";
}
}
优点:Future+线程池异步多线程任务配合,能显著提高程序的运行效率。
缺点:
结论:Future对于结果的获取不是很友好,只能通过阻塞或轮询的方式得到任务的结果。
jdk8设计出CompletableFuture,CompletableFuture提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方。
接口CompletionStage
类CompletableFuture
四个核心方法
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优点:
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;
}
/**
* 这里面需要注意一下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>
.collect(Collectors.toList()) //List>
.stream()//Stream
.map(s -> s.join()).collect(Collectors.toList()); //List
}
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方法立即返回括号值
对计算结果进行处理
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() + "------主线程先去做其他事情");
}
}
对计算结果进行消费
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
});
}
}
对比补充
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
传入一个线程池,如果你执行第一个任务时,传入了一个自定义线程池
备注:可能是线程处理太快,系统优化切换原则, 直接使用main线程处理,thenAccept和thenAcceptAsync,thenApply和thenApplyAsync等,之间的区别同理。
对计算速度进行选用
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());
}
}
对计算结果进行合并
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());
}
}
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不可访问,那就不存在多线程间共享问题
统一设置初始值,但是每个线程对这个值得修改都是各自线程互相独立得
如何才能不争抢
Thread和ThreadLocal,人手一份,Thread维护了一个ThreadLocalMap
ThreadLocal类里面维护了一个ThreadLocalMap静态内部类
总结:
强引用:
软引用:
弱引用:
比软引用的生命周期更短,对于只有弱引用的对象而言,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。
软引用和弱引用的使用场景----->假如有一个应用需要读取大量的本地图片:
如果每次读取图片都从硬盘读取则会严重影响性能
如果一次性全部加载到内存中又可能会造成内存溢出
此时使用软应用来解决,设计思路时:用一个HashMap来保存图片的路径和与相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,有效避免了OOM的问题
虚引用:
为什么要用弱引用:
这里有个需要注意的问题:
ThreadLocal并不解决线程间共享数据的问题
ThreadLocal适用于变量在线程间隔离且在方法间共享的场景
ThreadLocal通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
每个线程持有一个只属于它自己的专属map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有他的线程访问,故不存在线程安全以及锁的问题
ThreadLocalMap的Entry对ThreadLocal的引用为弱引用。避免了ThreadLocal对象无法被回收的问题
都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法回收键为null的Entry对象的值(即为具体实例)以及entry对象本身从而防止内存泄漏,属于安全加固的方法
ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以ThreadLocal为Key),不过是经过了两层包装的ThreadLocal对象: