基础篇
synchronized和lock的区别
1.从本质上:synchronized是Java内的一个关键字,lock是一个接口。
2.从代码的形式上:
synchronized在发生异常时会主动释放锁,lock需要我们在finally语句中释放,不然会死锁;
通过lock可以知道锁有没有获取成功,synchronied不行
3.从性能上:在1.6前没提出锁升级过程时,重量级锁 在被系统检测到后会 阻塞 尝试获取锁的线程 线程的阻塞和唤醒 都要操作系统来帮忙,这需要从用户态转到内核态,转换费时。
虚假唤醒
if(condition){
this.wait; //t线程在此等待,再次被唤醒,就不会再判断,直接向下执行了。改为while即可
}
针对集合类线程不安全的 “写时复制”技术
就是 对写加锁 读不加锁,允许随便读。进程创建子进程 时 常用。
Callable对比Runnable,FutureTask,CompletableFuture
Callable它有返回值 能抛异常
FutureTask 同时实现了两者,是常用的创建的多线程的方式,核心方法 get(),isDoneCompleteableFuture是对FutureTask的优化:当我们想获取futureTask的结果时,get()会阻塞线程,轮询isDone也会等待,为此CompleteableFuture可以自动回调 我们传入的回调对象的回调方法。
最后我们注意CompletableFuture的命名规则:xxx():表示该方法将继续在已有的线程中执行;
xxxAsync():表示将异步在线程池中执行。
为什么需要阻塞队列,核心方法
我们不用关心 什么时候 去阻塞 唤醒线程,实现了自动化
抛异常:add remove
ture false,null: offer poll
阻塞:put take
超时时间:offer(time,unit).poll(time,unit)
为什么出现了线程池,工作流程、拒绝策略、参数设置
1.为什么:
充分利用资源 有任务就跑 任务少就关几个线程 自动管理
2.工作流程:
执行一个任务 正在工作的线程数
小于corePoolSize,直接创建线程执行;
大于等于corePoolSize,放入队列中;
队列都满了,创建非核心线程数;
队列满了,也达到最大线程数,执行拒绝策略3.拒绝策略:
AbortPolicy:丢弃并 抛异常
CallerRunsPolicy:不丢弃 退回给调用者
DiscardOldestPolicy:抛弃排队最久的
DiscardPolicy:直接丢弃 也不抛异常4.参数设置:
在流量平均的前提下,套公式即可coreSize = tps*time,每秒任务数*单线程需要处理一个任务需要的时间
当流量经常是有峰值波动的,就可以考虑 动态化线程池,做监控 预警 消息通知 修改配置中心。
注:为什么要自定义?
不自定义的话 用工具类的话 队列长度和最大线程数上限都是 Integer.MaxValue 导致请求积压或过多线程,导致OOM;
Java锁-保证线程安全篇
1.synchronized
1.1实现原理
在类加载时 获取monitor锁对象 通过monitorEnter 和 monitorExit 两个命令实现的,流程的话:锁对象里有 锁计数器和指向它的线程的指针。
当执行moniterenter时,计数器为零,说明没有其他线程持有 该线程则就可以持有该锁对象 并将计数器加一 ;计数器不为零,就判断锁的持有线程是否时当前线程,若不是就等待,若是计数器加一
当执行moniterexit时,锁对象的计数器减一。
1.2对象头和锁升级
对象头 分 类型指针和MarkWord
MarkWord在 不同锁状态下 存储 的东西不一样,比如
无锁: hashcode 分代年龄 偏向锁标识 锁标识位;
偏向锁: 线程id 偏向锁标识 锁标识位
轻量锁:指针 锁标识位
重量锁:指针 锁标识位记忆规律:记住 无锁状态下 后面的锁状态存的东西依次递减 根据名字回忆即可
锁升级:
偏向锁(适用场景:一个线程):第一次获取 到markword 发现是无锁,通过原子的cas设置操作 将markword设置为 偏向锁状态下的markword(线程id设置为当前线程,偏向锁标识设置为1 锁标识位设置为01)该线程 下次获取锁对象 就不用cas设置了 比较线程id即可。
轻量锁(适用场景:两个线程):获取锁之前 会读取markword 发现是偏向锁且 线程id不是自己,尝试cas设置去修改markword(修改指针 和 锁标识位)
重量级锁(适用场景:2个线程以上):线程获取锁(设置markword) 就不是 自旋了,进入队列 阻塞了。
注:cas (compare and swap) 保证修改操作的原子性
1.通过地址读取到原a值
2.在正式修改为b值前 比较 1或取的a值和实时值
一样就修改,不一样就说明有其他线程在此期间修改了该值,当前线程的修改操作就不“原子”了,重复步骤1(重新读取)
2.lock
从源码看为什么lock是由cas和aqs构成的?
// 非公平锁
static final class NonfairSync extends Sync {
...
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
...
}1.先判断cas设置sate从0到1是否成功,成功就修改持有线程为当前线程
2.不成功 通过 继承来的AQS方法acquire获取锁
2.1CAS
原理:
1.硬件层面:通过cpu的cmpxchg指令去实现的,若是多核cpu,则先会对总线加锁,只有加锁成功的cpu能执行cas操作
2.代码层面:比如AutomicInteger 静态代码块里 通过unsafe提供的方法 获取到了value的内存地址偏移量valueoffset,然后就可以用该地址 去对value进行cas原子操作了注:unsafe顾名思义“不安全”,它为java提供了直接操作内存资源的方法。
缺点:
1.可能一直自旋 消耗cpu
2.ABA问题:
首先 a b 都观察值是1,然后b 修改 为 2 又 改回1,最后a 比较操作成功。小结:a虽然修改成功 但并不是原子的。可能的问题:当ab 观察的是一个对象的一个属性,但b不止修改了一个属性,a前后观察不是同一个对象了。解决:加版本号。
2.2AQS
数据结构:主要是由int 类型的state 和 FIFO双向链表构成,需要等待的线程封装成node
加锁的过程:
与minitorEnter 的过程类似 通过逐步比较锁对象的计数器和指针来决定 获取是否
入队等待。
常见问题:
1.简述入队addWaiter()?
cas原子化的去更新tail为当前线程节点,当失败或读取的原tail为null就会 自旋的去尝试设置tail。2.为什么需要一个虚拟头节点?
3.队列中 是如何有机会获取锁的?
最好画图
只有当你的前一个节点是head你才有机会获取锁资源保证了队列先进先出的特点,当你有资格获取锁却一直失败或没资格,你就会被park中断,当持有资源的线程解锁时,会唤醒head后的第一个不为null且非canceled状态的节点(队列的节点都是自旋获取锁资源的)。4.如果处于排队等候机制中的线程一直无法获取锁,需要一直等待么?还是有别的策略来解决这一问题?
不需要 可以执行cancel方法(双向链表删除节点的操作)。
参考:
阳哥视频
https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html
Java内存模型和volatile篇
概念:java针对cpu操作内存和缓存的统一规范,主要体现在多线程的 原子性,有序性(指令重排),可见性。
场景(什么规范呢):cpu,缓存,主内存。多线程之间沟通缓存不直接可见,只能通过主内存沟通。
保证操作间的可见性和有序性之happen-before原则
概念:当一个操作happen-before于另一操作,那么该操作会先执行和保证结果的可见性于第二个。
(两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。 如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
)
******************例子*****************
x = 5 线程A执行
y = x 线程B执行
上述称之为:写后读
问题?
y是否等于5呢?
如果线程A的操作(x= 5)happens-before(先行发生)线程B的操作(y = x),那么可以确定线程B执行后y = 5 一定成立;
如果他们不存在happens-before原则,那么y = 5 不一定成立。
这就是happens-before原则的威力。-------------------》包含可见性和有序性的约束
***************************************方法:如何确定操作间是否有happen-before?
有8个细则,忽略不考,凭借经验即可
volatile
一、概念:volatile读,缓存失效,直接从主内存中读最新的;volatile写,立即将缓存中的刷回主内存。
二、与JMM的关系:
保证可见性:volatile写。
保证有序性:禁止cpu层指令重排,
1.第一个是volatile读,后面 无论是什么类型读写 都不能重排到 此之前。
2..第二个是volatile写,前面无论是什么类型读写 都不能重排到 此之后。
3.第一个是volatile写且第二个是volatile读,也不能重排。
不能保证原子性:例如在多线程的时候,i++,volatile读会使缓存失效,导致刚在缓存+1的值丢失了。
三、底层实现:
内存屏障:
概念:粗分为读屏障--读之前插入屏障,缓存失效从主内存中读。
写屏障--写操作后插入屏障,写完立刻刷回主内存。内存屏障起作用的例子:
在volatile读后加 loadload(第一个读和第二个读不能交换顺序)和 loadstore
volatile的使用
1.多线程任务的相互通知,需要修改玩flag后,立即对其他线程可见。
2.双重检查锁的场景:
singleton = new SingleTon();
实际上涉及三个指令,
1.分配内存空间,
2.new(),初始化对象
3.将地址赋值给singleton。
重排后 会将 2 3交换。就会造成线程A走完 1 3 ,线程B此时就可以通过 为空检查,但值是空对象。