欢迎欢迎欢迎老铁观看!!!
文章目录
- JavaEE & 多线程进阶问题 & 锁策略and 死锁,CAS操作,Synchronized原理
- 1. 锁策略
- 1.1 乐观锁 vs 悲观锁
- 1.2 轻量级锁 vs 重量级锁
- 1.3 自旋锁 & 挂起等待锁
- 1.4 synchronized属于哪种锁?
- 1.5 互斥锁 vs 读写锁
- 1.6 公平锁 vs 非公平锁
- 2. 死锁
- 2.1 可重入锁 vs 不可重入锁
- 2.2 两个线程两把锁
- 2.3 N个线程,M把锁
- 2.3.1 哲学家就餐问题
- 2.3.2 死锁的基本特点,四个必要条件
- 2.3.3 死锁的处理
- 3. CAS操作
- 3.1 CAS介绍
- 3.2 CAS操作实现原子类
- 3.2.1 AtomicInteger类
- 3.2.2 AtomicInteger使用
- 3.3 CAS操作实现自旋锁
- 3.3.1 aba问题
- 3.3.2 解决aba问题缺陷
- 4. Synchronized原理
- 4.1 锁升级
- 4.2 锁消除
- 4.3 锁粗化
锁的实现者通过锁的冲突概率,做出相应的决策
了解概念即可~
虽然和乐观锁悲观锁不是一回事,但是有部分概念重合~
这跟轻重锁有密切联系
对于自旋锁:
对于挂起等待锁
自旋 ==> 纯用户态操作,不需要经过内核态,时间相对更短
挂起等待 ==> 需要通过内核的机制来实现挂起等待,时间相对更长
对于读写锁:
读写锁约定:
读锁和读锁之间,不会产生锁竞争 — 多线程读操作,线程安全~
写锁和写锁之间,就会产生锁竞争
读锁和写锁之间,也会产生锁竞争
【非必要不加锁】,适用于一写多读的情况
虽然synchronized并非读写锁,但是Java标准库有~
ReentrantReadWriteLock的源码发现,
其内部含有ReadLock和WriteLock这两个类,
代表ReentrantReadWriteLock拥有的一对读锁 和写锁,
而如果是非公平锁,只要锁释放,线程234都有可能抢到锁
而如果是公平锁,锁释放后,按照先后顺序分配锁
实现公平锁的话,加个队列记录顺序即可~
对于不可重入锁:
日常开发是很容易触发这些代码的,例如:
synchronized void A() {
·······
}
synchronized void B() {
A();
·······
}
这样,就会出现,一个线程对同一把锁的,连续加锁两次~
然而,出现这些情况就会触发死锁bug吗?
显然是不会的,synchronized就是可重入锁
没错,JVM就是这么牛
假设五个哲学家,同时拿起左手边的筷子,那么他们就会固执的嗦不了粉
四个条件缺一不可!
死锁是一个很严重的bug,我们要如何处理呢?
其实,死锁的产生就是因为加锁链是交叉的
例如,两个线程两把锁:
依照这个思路,我们只需要规定一个顺序,只要一层层加锁的时候,都严格按照同一个顺序来加锁,就可以巧妙的防止出现交叉,防止死锁的出现!
例如以下的,四个线程,四把锁
总结就是,注意写代码时,按照加锁顺序!!!
那么,无论谁先开始拿(随机调度)
最终,至少会导致一个哲学家拿不到一根筷子,
那么就至少会有一个哲学家拿到两根筷子!
那么的那么,这个哲学家嗦完粉后,就可以放下筷子,这样其他等待的哲学家就可以嗦粉了
CAS ==> Compare-And-Swap
即,比较 和 交换
作用就是:
boolean CAS(M, A, B)
对于这个操作,我们更关心,M对应的值被赋值~
boolean CAS(M, A, B) {
if (*M == A) {
*M = B;
return true;
}
return false;
}
重点:
意义:打开新世界大门,不加锁也能线程安全!
public static void main(String[] args) throws InterruptedException {
AtomicInteger atomicInteger = new AtomicInteger(0);
//只要引用指向不改变,就可以被lambda表达式捕获~
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
atomicInteger.getAndIncrement();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
atomicInteger.getAndIncrement();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(atomicInteger.get());//获取对应值~
//不过这个原子类的打印方法,就是打印其对应的值
}
源码看不懂_(¦3」∠)_
看伪代码~
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue + 1) != true) {
oldValue = value;
}
return oldValue;
}
}
解析:
如下为伪代码:
public class SpinLock {
private Thread owner = null;
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就自旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
乐观锁—锁冲突概率小的,适用自旋锁~
我们在CAS操作的原理中看出compare操作起到的作用
问题:我们肯定不能强制这个数据只能增只能减啊!!!
但是我们可以添加一个成员属性,版本号
而这个成员是只能增 / 只能减的
这样,每次CAS操作,对比的就不是数据本身,而是对比版本号~
伪代码:
class V {
int number = 100;
int version = 1;
}
if(CAS(version, old, old + 1)) {
number++;//键值别忘了变~
}
以版本号为基准,而不是以变量数值为基准!!!
对于synchronized
以后遇到其他锁,可以依照这些特定对号入座~
而偏向锁是这里唯一没讲到过的。
主要还是为了尽可能优化速率
对于自旋锁变为挂起等待锁
主流的JVM的实现,只能锁升级,不能锁降级~
非必要不加锁,但是我们之前的是在代码运行层面的不加锁
编译时,智能检测当前代码,是否是多线程或者是是否有必要加锁
synchronized不应该被滥用的,开销大~
粒度: 即细化的程度。被封锁的对象的粒度。例如数据项、记录、文件或整个数据库,锁粒度越小事务的并行度越高。
但是,如果在一个需要频繁加锁解锁的场景下,粒度细会导致开销更大!
这样,编译器就会优化你的代码,粗化代码粒度,减少加锁解锁
这样也可以保证一个任务的完整性,毕竟线程调度随机,一个任务可能会因为部分环节延后了。
一样的,不会影响最终结果~
文章到此结束!谢谢观看可以叫我 小马,我可能写的不好或者有错误,但是一起加油鸭!
后续会有一篇兄弟文章:JavaEE & Callable接口(NO.6线程创建方法) & JUC的常见类 & 与线程安全有关集合类
敬请期待!