尚硅谷-JUC篇
狂神说-JUC:https://www.cnblogs.com/meditation5201314/p/14940976.html
尚硅谷-JUC:https://www.bilibili.com/video/BV1ar4y1x727?p=166&vd_source=510ec700814c4e5dc4c4fda8f06c10e8
狂神的内容偏基础,属于抛砖引玉的那种
尚硅谷偏向更多原理和能力提升。(但是周阳老师对一些名词讲得好晦涩,明明可以用简单几句话讲明白的,愣是能凑成一节课)
以后学完一门课花个脑图,便于记忆回顾总结
线程流程控制:
0. 线程串行化:CompletableFutre.thenapply获取其他线程返回值
1. 中断线程:循环+interrupted控制线程中断跳出
2. 取消线程:futuretask.cacel,或者直接抛出异常,处理
3. 全部完成再返回:CompletableFuture的allof
4. 线程交互:不同线程对同一个同步对象进行this.wait, this.notify(属于Object上方法)
5. 线程变量的感知:volatile,synchronized, atomic
6. 线程池执行返回值:CompletableFuture放入池中即可
🔥1. 基本概念
1.1. 基本概念
1.1.1 线程Start
内部是调用C++源码,C++底层是操作系统分配一个线程启动
private native void start0();
1.1.2 基本名词
1.1.2.1 一锁/二并/三程
一锁、二并、三程
1. synchronized锁
1. 并发和并行
3. 进程、线程、管程
1. 管程:就是motitor锁,就是进入、退出一个对象或代码收到保护,保证同步。
1.1.2.2 用户线程/守护线程
用户线程:系统工作的线程,类似于主人
守护线程:为其他线程服务,类似于影子,用户线程都没了,守护线程也没
main方法启动时,就有一个主线程在执行(也是用户线程)
1.1.3 CompletableFuture
1.1.3.1 Future接口
Future可以fork另一个子线程,获取子线程结果,取消子线程执行,判断任务是否被取消,判断任务是否执行完毕等。
1.1.3.2 Get源码
get会循环阻塞,查看某个变量,属于在自旋,需要配合FutureTask类和run()方法一起看,当run结束后,会改变里面的一个变量,然后get就能收到变更了。
1.1.3.3 底层源码
是实现了Future异步和CompletionStage(分多个阶段触发)
1.1.3.4 基本使用
感觉周阳老师这里讲的不是很系统,推荐雷神-谷粒商城讲的CompletableFuture
https://www.bilibili.com/video/BV1np4y1C7Yf?p=195&vd_source=510ec700814c4e5dc4c4fda8f06c10e8
1.1.4 锁
1.1.4.1 乐观锁/悲观锁
乐观锁:默认不添加锁,只有在更新的时候判断一下,适合读操作多。判断规则:
1. 利用version判断。update XXX set version = version + 1 where version = X
1. CAS算法。Atomic原子类递增操作就是CAS自旋实现
悲观锁:获取数据就加锁,防止其他线程修改,适合写多读少操作。synchronized,Lock都是悲观锁
1.1.4.2 八锁
推荐狂神讲的八锁问题,感觉都差不多,就是锁对象、锁类、普通方法的区别。
https://www.cnblogs.com/meditation5201314/p/14940976.html#2八锁现象synchronizedstatic
1.1.4.3 synchronized锁--持有方式
1. static synchronize Method(){}:锁类
1. 由ACC_STATIC,ACC_SYNCHRONIZED判断,然后进去monitirEnter,exit逻辑
2. synchronized Method(){}:锁对象
1. 先由ACC_SYNCHRONIZED判断,然后再进入monitorEnter,monitorExit逻辑
3. synchronized(this.Obj){}:同步代码块
1. 底层就是monitorEnter和monitoerexit构成
1.1.4.4 synchronized锁--底层原理
一个对象Object创建就附带了objectMonitor对象监视器,用来记录哪个线程哪个锁、锁了几次等相关信息。synchronized锁对象、锁类的时候是管程monitor就变更了对象ObjectMonitor信息。
1.1.4.5 公平/非公平锁
公平锁:尽量按照顺序来申请(会有线程切换的开销)
非公平:让线程争抢
1.1.4.6 可重入锁
一个线程内部多个流程可重复获取同一把锁,不会产生死锁。synchronized(隐式可重入锁,内部有objectMonitor锁计数器进行加一减一),reentreenLock(显示可重入锁,lock一次,就必须unlock一次)都是可重入锁。
1.1.4.7 Synchronized锁--执行原理
/*
1. synchronized的字节码指令monitorEnter对对象里面的objectMonitor字段修改。判断是锁对象是否被调用,加锁几次,然后进行加锁
2. 加完锁后处理正常业务,其他线程获取锁对象时失败会进去阻塞队列
3. 执行完后monitorExit进行数据-1,解锁
*/
synchronized(this){
}
1.1.5 线程中断
1.1.5.1 实现方式
线程中断应有由线程自己决定中断
- thread自带中断api实现
- volatile 全局变量,对各个线程可见
- atomicBoolean 全局变量,对各个线程可见
1.1.5.2 源码分析
- 正常活动线程:interrupted会标志为中断,中断后并不会结束运行,过一段时间后才能结束。中断已经结束的线程,不会产生任何影响,isTreeupted还是false
- 阻塞状态线程(sleep,wait.join):当其他线程或本线程调用该线程的interrupted,中断状态将清除,变成false,并报错interruptedException
用户查看线程是否中断
1.1.5.3 静态方法VS实例方法
Thread.interrupted():静态方法,返回线程中断状态并置为false
Thread.currentThread().isInterrupted():实例方法,返回线程中断状态
Thread.currentThread().interrupt():中断某个线程。不会让线程结束,可以配合循环使用
1.1.5 LockSupport
1.1.5.1 等待唤醒--实现方式(生产者-消费者)
用来优化之前的线程唤醒和阻塞,比其他唤醒方法功能更强(下面3个都只能有一个,多次的话会失效)
1. Object->wait(), notify, notifyAll
1. wait,notify必须放在同步对象里面
2. wait,notify先后顺序不能错,否则先唤醒后wait无法唤醒
2. Condition-> await(), signal()
1. await,signal必须放在同一个lock,unlock里面
2. await,signal先后顺序也不能乱
3. LockSupport-> park阻塞、unpark唤醒线程
1. park阻塞线程,unpark(线程)。就类似给了一个许可证,以后永久可用。先后顺序可以不一致,都能唤醒。许可证只有一个,即一个park,一个unpark
1.1.6 Java内存模型--JMM
1.1.6.1 基本概念
Java Memory Mode:用来实现操作系统和硬件之间内存访问的差异,实现在各个平台内存访问速度一致的效果。
原子性:操作不可打断
可见性:多个线程对主内容中数据进行拷贝一份变量副本,对主内存中共享变量的修改对其他线程可见。(每个线程自己有工作内存存变量副本,不同线程之间不可互相访问)
有序性:指令会重排序,但执行结果一定一致
1.1.6.2 JMM规范--先行发生原则
正是因为有了先行发生原则,所以实际代码才少了很多volatile, synchronized定义
- 线程A操作一个变量后对变量变更需要B操作可见,
- A操作里面的各种代码子操作顺序可以随着编译器优化而有所不同,只要最终结果不变就行
- 顺序性:代码先写的比代码后写的快、对象创建后才能finalize、lock后才能unlock,start()先于线程操作前、线程修改变量后对后续线程获取变量可见(不安全的情况就是普通变量两个线程同时获取在修改)
1.2 代码名词
1.2.1 Volatile
1.2.1.1 基本特点
-
可见性:修改某个变量对其他线程都要可见
-
有序性:禁止指令重排
-
没有原子性:volatile i++不能保证原子性
1.2.1.2 内存屏障
1. 对volatile写操作都立刻刷到主内存中
1. 对volatile读操作都先把线程内部的内存变量定义无效,然后从主内存中读取
1. 在读写操作前后加入了组织指令重排的约束
注:感觉老师这里对内存交互讲的不是很好,还是狂神讲的简单易懂,学习链接:https://www.bilibili.com/video/BV1B7411L7tE?p=30&vd_source=510ec700814c4e5dc4c4fda8f06c10e8
1.2.1.3 存在问题
DCL:doucle check lock双端检测锁:
//如果不加volatile,那么当new对象的时候,由于内部是先创建对象位置,然后指令重排,然后user指向空对象,然后在给对象放入对象位置。当user指向空对象的时候,多线程下第一次if判断就会发生问题
volatile User user = null;
if (user ==null){
synzhronized(UserTest.class){
if(user == null){
user = new User;
}
}
}
1.2.2 CAS
1.2.2.1 基本概念
Compare and swap:内部原理是乐观锁,如果共享内存值不一样,就放弃,或者自旋重试。
1.2.2.2 Atomic自增--线程安全
通过Unsafe类(内部都是native方法)操作硬件保证非阻塞的原子性操作,属于CPU指令原语。
内部AtomicInteger是利用CAS自旋 + volatile + native方法来保证原子性
--unsafe类中方法
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//重新获取volatile变量
var5 = this.getIntVolatile(var1, var2);
//比较与预期值,不同就自旋
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
1.2.2.3 存在问题
1. 自旋开销大
1. ABA问题: compareStampReference解决(可以包装基本类型和对象)
1.2.3 原子类
1.2.3.1 基本分类
1. 基本类型、数组类型
1. 引用类型:AtomicReference(普通对象包装) AtomicStampedReference(版本标记), atomicMarkableReference(一次性标记)
1. 线程安全--类属性修改:AtomicIntegerFieldUpdater, AtomicReferenceFieldUpdater
1.2.3.2 LongAdder--基本概念
内置一个base变量和cell[]数组
Atomic:底层是原语和自旋,一次只允许一个线程操作,高并发下性能下降
LongAdder:就是采用cell[]数组存,然后获取的时候把base变量和cell[]数组值加起来
1.2.3.3 LongAdder--源码分析(略)
总的来说,LongAdder可用,get并发下获取数据性能好,但是sum精度不准确
1.2.4 ThreadLocal
1.2.4.1 基本概念
给每个线程一个变量副本。对应JMM中的线程变量副本。
之前synchronized,volatile, cas都是对共享变量的操作,现在ThreadLocal是对线程私有变量的操作
1.2.4.2 基本使用
// 1.初始化
ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);
// 2. 使用后remove
// 3.线程池开启后要shutdown,否则会一直开启
// 4.使用static修饰,每次new对象都只会有一份
1.2.4.3 底层原理
Thread 内置ThreadLocal,ThreadLocal内置ThreadLocalMap。
当赋值的时候,不存在就创建(和hashmap类似),赋值就是给map<threadLocal实例,值>这么赋值
1.2.4.4 强/软/弱/虚引用
这几个引用, finalize都是Object下面的,
- 强引用:普通的引用对象
- 软引用:内存不足会回收
- 弱引用:GC发现就会回收
- 虚引用:必须被回收,get是null,就是对象回收后的一种通知机制,配合引用队列使用,和finallize类似
1.2.4.5 key-弱引用
1. 回收key: GC发现线程死亡了,threadLocal的key就被回收,避免内存泄漏(类似人和身份证)
1. 回收value: 当GC回收了threadLocal的key后,任意每次调用ThreadLocal的get,set,remove都会回收key=null的value值
1. 所以全靠调用get,set,remove也会存在内存泄漏,每次用完都要remove
1.2.5 内存布局
一个对象object在内存中存下来分为
- 对象头
- 运行时元数据(markword):记录了锁的状态(无锁、偏向、轻量、重量)
- 类型指针
- 实例数据
- 填充数据
1.2.6 锁升级
1.2.6.1 基本概念
锁升级:无锁-> 偏向锁->轻量->重量。 (写锁叫独占锁,读锁叫共享锁)
对于每一个对象object,内部都有monitor,当线程用synchronized修饰成重量级锁时,线程需要用户态到内核态的切换,非常耗时。(线程阻塞、唤醒也会有用户态、内核态的切换)
1.2.6.2 锁升级
-
无锁(标记位001):对象默认无锁
-
偏向锁(默认开启 标记位101 需要项目启动后等一会对象创建就是偏向锁了):对象的对象头中markWord存储的是偏向线程id。
- 概念:单线程竞争下获取锁时,同一个线程频繁获取一个锁,后续会自动获得该锁(类似熟人一直来某个地方)
- 实现:对象头记录了对象是否被某个线程抢到,抢到了就标记线程id,后续就能标记该线程和其他线程,若其他线程来竞争成功,就利用CAS替换新线程id,若其他线程竞争失败,就需要进行锁升级
- 对于锁升级还是竞争成功替换线程id,这是由原线程是否执行完synchronized,如果执行完同步代码块,就能替换,刚进入,就要锁升级)
-
轻量级锁:markWord存线程栈中Lock Record指针
- 概念:2个(多个)线程交替获取对象的锁
- 实现:A线程获得,B线程竞争,竞争成功,对象偏向锁改为B,B竞争失败,对象偏向锁升级为轻量级锁,B自旋等待获取A持有的轻量级锁。(之所以存在线程栈中,就是把对象markword复制到线程栈帧中了,释放轻量级锁也是把栈帧中写回到markword中)。B线程如果自旋到一定次数(自适应次数,因为更多线程来了)后会升级成重量级锁
-
重量级锁:存堆中monitor指针
- 概念:synchronized重量级锁,基于Monitor存储线程id来保证其他线程无法获取
轻量级锁和偏向锁
轻量级锁竞争失败就会自旋竞争,偏向只有一个线程
轻量级锁每次退出同步代码块就要释放锁,而偏向锁只有多线程竞争才会释放锁,因为默认是一个线程
1.2.6.3 锁--HashCode存储
无锁有Hashcode,计算了之后就无法进入偏向锁,进行锁升级成轻量级锁
处于偏向锁还要计算HashCode,就会变成重量级锁获取Hashcode
轻量、重量锁都有HashCode
1.2.6.4 Synchronized锁总结
1. Synchronized本质就是先自旋,再阻塞,内部实现用对象头的标记实现无锁、偏向、轻量、重量的升级。(改变了jdk1.6之前都弄成重量级锁的压力)
1.2.6.5 锁消除/锁粗化
锁消除:多线程获取同步代码块的时候,里面每次重新new对象,synchronized(对象)就会有JIT优化成直接new对象。对应JVM里面JIT逃逸分析-同步省略
锁粗化:多个synchronized包同一个对象,就会优化成一个synchronized包这个对象
//锁粗化
Object o = new Object();
//优化成synchronized(o){sout(1), sout(2)}
synchronized (o){
System.out.println("1");
}
synchronized (o){
System.out.println("2");
}
无锁:自由自在
偏向:唯我独尊
轻量:楚汉争霸
重量:群雄逐鹿
1.2.7 AQS
1.2.7.1 基本概念
AbstractQueuedSynchronizer:抽象队列同步器
1. 底层是有一个FIFO的CLH等待队列(类似候客厅)和int型state来标记资源是否被获得
2. 由AQS实现的上层:reentranlock, countdownlatch, semaphore
1.2.7.2 内部实现
AQS = state + CLH队列
Node = waitStatud + Thread
- CLH是由一个Node结点来实现队列
- Node结点内部有标记线程是共享/独享状态,等待状态
- 有个头尾指针指向上面Node结点
- volatile int state由于标记资源状态
class Node{}
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
Node结点内部属性说明
1.2.7.3 源码剖析
ReentranLock源码
- 非公平就先抢占state独占,然后调用nonfairTryAcquire到CLH队列,后续循环
- 公平一来就调用公平锁的队列,先进先出
非公平锁和公平锁内部acquire方法
1. tryAcquire(): 公平锁有个排队函数,其他都和非公平锁一样,尝试CAS自旋获取state状态
1. acquireQueued,addwaiter: addwaiter根据当前线程创建结点,acquireQueued: 根据上一个Node结点中volatile waitStatus判断线程是否需要LockSupport.park()暂停,就类似于阻塞在等待队列中
3. unlock解锁:
1. 把类似于办理好业务的线程窗口置为null, state置为0,waitStatus修改,并唤醒unpark正在等待的线程
2. unpark的线程要调用各自的tryAcquire(),用于区分公平锁和非公平锁
1.2.8 读写锁
1.2.8.1 基本概念
读写锁:适合读多写少的情景,运行同时多个读,但是读写,写写互斥。底层也是AQS的Sync
演变流程就是从无锁->独占锁->读写锁->邮戳锁
1.2.8.2 锁降级
readWriteLock锁降级:由写锁降级为读锁(类似linux中写权限比读权限高)。就是先加写锁,在加读锁,再释放写锁,再释放读锁。
不同线程操作同一对象时读写是互斥的,但是同一个线程内容是可以锁降级。
作用:当程序中想实现写后读的话,就用到这种锁降级方法,否则写了释放写锁再获取读锁就可能被其他写线程给获取到修改了。
1.2.8.3 StampedLock邮戳锁
之前读锁未释放前都不允许插入写锁,所以可能存在多个读锁,一个写锁情况下,发生写锁无法插入,这就是锁饥饿。所以为了保证读没有完成的时候写锁可以介入,从而引入了stampedLock
实现方式:
1. 读模式:和之前readwritelock一样
1. 写模式:和之前readwritelock一样
1. 乐观模式:就是在读的时候用乐观锁判断,如果发现被改过,就悲观读
1.3 个人总结--脑图
以前一直想不通为什么线程要区分这么细,后来才发现,实际开发中,不同用户就是一个线程请求,如何把各个请求处理好,保证系统性能和安全性,才是程序员需要考虑的问题
我曾七次鄙视自己的灵魂:
第一次,当它本可进取时,却故作谦卑;
第二次,当它在空虚时,用爱欲来填充;
第三次,在困难和容易之间,它选择了容易;
第四次,它犯了错,却借由别人也会犯错来宽慰自己;
第五次,它自由软弱,却把它认为是生命的坚韧;
第六次,当它鄙夷一张丑恶的嘴脸时,却不知那正是自己面具中的一副;
第七次,它侧身于生活的污泥中,虽不甘心,却又畏首畏尾。