🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343
🏵️热门专栏:🍕 Collection与数据结构 (90平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482
🧀Java EE(93平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482
🍭MySql数据库(93平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482
感谢点赞与关注~~~
1.几种不同的锁策略
常见的锁策略不仅仅是局限于Java中,任何语言都适用.
1.1 乐观锁vs悲观锁
- 乐观锁:加锁的时候,预测锁冲突的概率比较小,所以在接下来的时间里做的事情就比较少,也就是加锁的开销就比较小.但是具体是怎么判断加锁之后锁冲突的概率的,这就和JVM的源代码有关联了.
- 悲观锁:加锁的时候,预测锁冲突的概率比较大,所以在接下来的时间里做的事情就比较多,也就是加锁的开销就比较大.
举例说明:找老师问问题
现在有A和B两个同学:同学A采用悲观锁的策略,同学B采用乐观锁的策略
同学A就会想:"我去了老师办公室之后,老师也不一定有时间回答."所以就会给老师提前发微信,询问老师是否有时间得到肯定答复之后才会来问问题.
同学B就会想:"我去了之后老师肯定有时间回答."所以就会直接找老师帮忙,如果老师比较闲,那么问题便得已解决,如果老师比较忙,也不会打扰到老师.
什么时候使用乐观锁策略,什么时候使用悲观锁策略,还是要看具体的场景.
就如上述的例子,如果老师确实比较忙,使用悲观锁就是很好的策略,如果使用乐观锁,就是白跑一趟.
老师确实比较闲的时候,使用乐观锁就是很好的策略,如果使用悲观锁,就会额外消耗加锁的开销.
1.2 重量级锁vs轻量级锁
一般来说,轻量级锁对应的就是乐观锁,重量级锁对应的就是悲观锁,两组概念经常被混起来用.
我们现在来解释一下,重量级锁究竟比轻量级锁多做了哪些事情,就是轻量级锁值通过用户态代码来加锁,而重量级锁就是通过系统内核提供的mutex来加锁,也就是通过内核态实现加锁.
1.3 自旋锁vs挂起等待锁
自旋锁是轻量级锁的典型实现方式,挂起等待锁是重量级锁的典型实现方式.
- 自旋锁: 按之前的方式,线程抢占锁失败就会进入阻塞等待状态,放弃CPU.
但实际上,大部分情况下,虽然当前抢占锁失败,但是过了不久之后,锁就会被立刻释放掉,现在就没必要放弃CPU,这时候就可以使用自旋锁的方式来解决上述问题
void locker(){while(true){if(锁是否被占用){continue;}获取到锁;break;}
}
从上述伪代码中,我们可以看到自旋锁采用了忙等的策略,如果获取锁失败,就立即再尝试获取锁,无限循环,直到获取到锁为止.
这种锁的优点就是:没有放弃CPU,不涉及阻塞和调度,一旦锁被释放,就可以第一时间获取到锁.
缺点就是:消耗了更多的CPU资源,其他线程可能吃不到CPU资源.
- 挂起等待锁:就是在一个线程与其他线程产生锁冲突的时候,就会挂起等待,线程不再采用忙等的方式,而是阻塞等待,直到这个锁被释放,然后系统可以唤醒线程,再去尝试重新获取锁.
优点:不会占用CPU资源,可以把CPU资源让给其他的线程.
缺点:需要系统去唤醒该线程,再去获取锁,这样就不可以第一时间获取到锁.
举例说明:
有请老朋友:钟离,达达利亚,荧
如果达达利亚和荧去表白,但是现在荧告诉他,他现在有男朋友,但是达达利亚是个死皮赖脸的人,一直每天给荧发消息问候寒暄.一旦荧和现男友分手,达达利亚就立马可以上位.
如果钟离和荧去表白,但是现在荧也告诉他,他现在有男朋友,由于钟离比较收敛,不再去天天烦荧.但是有一天他从别人口中听说荧分手了,此时钟离才有机会上位.
1.4 公平锁vs非公平锁
假设三个线程A,B,C.A先尝试获取锁,获取成功.然后B再尝试获取锁,获取失败,阻塞等待;然后C也尝试获取锁,C也获取失败,也阻塞等待.
- 公平锁: 遵守先来后到的原则,B比C等待的时间长,在A释放锁之后,B就可以优先加锁.
- 非公平锁: 不遵循先来后到的原则,B和C获取到锁的概率是相等的.就看谁的竞争能力比较强.
举例说明:
荧和他的男朋友在谈恋爱,突然有一天分手了,这时候达达利亚和钟离都有机会追到荧.但是现在达达利亚却比钟离追荧的时间长.
如果是公平锁的话:由于达达利亚追荧的时间比较长,那么达达利亚可以优先追到荧.
如果是非公平锁的话:这时就和追的时间没有关系了,这时候两个人追到荧的概率就是相等的,就各凭本事了.
注意:
- 操作系统由于线程是随机调度的,这时候不做任何限制的话,那么锁就是非公平的.如果想要实现公平锁的话,必须借助额外的数据结构来判断等待时间.
- 公平锁和非公平锁没有好坏之分,主要看使用场景.
1.5 不可重入锁vs可重入锁
前面提级过,不再赘述.
https://blog.csdn.net/2301_80050796/article/details/138041540?spm=1001.2014.3001.5501
1.6 读写锁
一个线程对于数据的访问,主要存在两种操作:读数据和写数据.
- 两个线程都只是读一个数据,此时并没有线程安全问题,直接并发读取即可.读加锁和读加锁之间并不会产生锁互斥.
- 如果两个线程有一个读,有一个写,此时有线程安全问题,写加锁和读加锁之间会有锁互斥.
- 如果两个线程都在写一个数据,此时就有线程安全问题,写加锁和写加锁之间会产生锁互斥.
读写锁非常适合用在,频繁读,不频繁写的场景中.
2. synchronized原理
2.1 基本特点
- 一开始是轻量级锁(乐观锁),如果锁冲突比较频繁,就升级为重量级锁(悲观锁)
- 实现轻量级锁的时候大概率是使用自旋锁的策略.
- 是一把不公平锁
- 是可重入锁
- 不是读写锁
2.2 synchronized的锁升级
JVM将synchronized锁分为无锁、偏向锁、轻量级锁、重量级锁状态.会根据情况,进⾏依次升级.
- 偏向锁(重点)
第一个尝试加锁的线程,先不会对线程真正加锁.而是先进入偏向锁状态.
偏向锁不是真正的加锁,而只是给对象头中做一个"偏向锁的标记",记录这个锁属于哪个线程,如果后期没有其他线程来与该线程进行锁竞争,那么就一直保持这个状态,就减少了加锁的开销.
偏向锁就突出了一个字"懒",能不加锁就不加锁,避免了加锁带来的不必要的开销.但是这个偏向锁的标记又不得不做,否者分不清何时哪个线程需要真正加锁. - 轻量级锁
当有其他线程尝试与该线程进行锁竞争的时候,此时偏向锁就会升级为轻量级锁, - 重量级锁
当锁冲突不断加深的时候,就会升级为重量级锁.
举例说明:
此时比如荧和达达利亚是游走在朋友和男女朋友之间的关系,这时候如果没有其他人来与达达利亚竞争荧的时候,就一直可以保持这种关系(一直保持偏向锁),就避免了官宣这种开销比较大的操作,将来如果达达利亚想分手的时候,一句话直接让荧闭嘴:“你又不是我女朋友”.
如果有一天钟离也对荧有了感情,想要追荧,这时候达达利亚就可以立即和荧官宣,这种开销就比较大了(加锁操作),就意味着达达利亚要对荧负责了,将来如果想分手的时候,也非常麻烦,需要达达利亚一顿组合技才可以把荧踹掉.
2.3 synchronized锁优化
2.3.1 锁消除
编译器+JVM判断锁是否可消除.如果可以,就直接消除.
那么什么是锁消除呢?
有一些程序的代码中,虽然使用了synchronized关键字,但是是在单线程状态下的,比如下面这个例子.
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
上述的StringBuffer操作中,每一个append方法都会涉及加锁和解锁操作,但是这是在单线程状态下进行的,这时候就没有必要再进行加锁操作,系统就会把这层锁优化掉.
2.3.2 锁粗化
⼀段逻辑中如果出现多次加锁解锁,编译器+JVM会⾃动进⾏锁的粗化.
- 锁的粒度
主要看该加锁范围中包含代码的多少,包含的代码越多,就认为锁的粒度越粗,反之越细.
在我们实际开发中,使用细粒度的锁是为了避免在没有加锁的情况下,避免与其他的线程产生冲突,从而产生线程安全问题.
但是在实际上可能并没有其他线程来抢占这个锁,这时候JVM就会自动把这几个锁粗化成更少的锁,从而减少加锁解锁的系统开销.
举例说明:
有请助教:滑稽老铁
滑稽⽼哥当了领导,给下属交代⼯作任务:⽅式⼀: • 打电话,交代任务1,挂电话. • 打电话,交代任务2,挂电话. • 打电话,交代任务3,挂电话.⽅式⼆: • 打电话,交代任务1,任务2,任务3,挂电话.显然,方式二是更高效的方案.
我们通过上面的解释可以看到,synchronized背后做的事情是非常多的,目的就是为了让Java程序员们即使不懂这些东西也可以写出高质量的代码.Java的大佬真的是为我们操碎了心!!