目录
常见的锁策略
下面所说的锁是“锁的一种特性”,“一类锁”,不是具体的一把锁
乐观锁 和 悲观锁
悲观乐观是根据对后续锁冲突是否激烈(频繁)给出的预测
乐观锁:如果预测接下来的锁冲突的概率不大,就可以少做一些工作,就称为乐观锁
悲观锁:如果预测接下来的锁冲突的概率很大,就应该多做一些工作,就称为悲观锁
重量级锁 和 轻量级锁
重量级锁:锁的开销比较大
轻量级锁:锁的开销比较小
乐观锁通常也就是轻量级锁,悲观锁通常也就是重量级锁 (一个是预测锁冲突的概率,一个是实际的开销)
自旋锁(Spin Lock) 和 挂起等待锁
自旋锁:属于是一种轻量级的典型表现,往往在用户态实现。(比如使用一个while循环,不停检查锁是否被释放,如果没被释放就继续等待,释放了就获取锁,从而结束循环,忙等,消耗cpu资源但是换来的是更快的响应)
挂起等待锁:属于是重量级锁的一种典型表现,需要借助系统api来实现,一旦出现锁竞争了,就会在内核中触发一系列动作(比如让这个线程进入阻塞的状态,暂时不参与cpu调度,阻塞的开销是比较大的)
读写锁
把加锁,分成了两种,读加锁,写加锁
两个线程加锁过程中: 1,读锁和读锁之间,不会产生竞争
2,读锁和写锁之间,有竞争
3,写锁和写锁之间,也会由竞争
可重入锁 和 不可重入锁
同一个线程,对一把锁进行连续两次加锁,不会出现死锁,就是可重入锁,会死锁,就是不可重入锁。
公平锁 和 非公平锁
当很多线程去尝试加一把锁的时候,一个线程能够拿到锁,其他线程阻塞等待,一旦第一个线程释放锁之后,接下来应该哪个线程能够拿到这把锁呢?
公平锁:按照”先来后到“的顺序
非公平锁:则是剩下的线程以”均等“的概率,来重新竞争锁。
操作系统默认提供的加锁api是 "非公平锁"。如果想实现公平锁,还需要引入额外的的队列,来维护这些线程饿加锁顺序。
在Java中synchronized属于的特点:
1.对于”悲观乐观“,”重量轻量“,”自旋 挂起等待“是自适应的,初始情况下,synchronized会预测当前锁的锁冲突概率不大,此时以乐观锁模式来运行(此时也是轻量级锁,基于自旋方式实现的),在实际过程中,如果发现锁冲突的情况比较多,synchronized就升级成悲观锁(也就是重量级锁,基于挂起等待实现的)
2.不是读写锁
3.是可重入锁
4.是非公平锁
CAS (Compare and swap)
比较交换的是 内存 和寄存器,例如现在有一个内存M,还有两个寄存器A,B,构成CAS(M,A,B)
如果 M 和 A 的值相同的话,就把M 和 B里的值进行交换(交换的本质是为了把B赋值给M),同时整个操作返回true。
如果M 和 A的值不同的话,无事发生,同事整个操作返回false。
下面是一段伪代码,供参考
CAS其实是一个cpu指令,一个cpu指令,能完成上述比较交换的逻辑。
单个的cpu指令,因为是原子的,那么通过CAS完成一些操作,进一步代替“加锁”,减少加锁带来的资源消耗,提高程序运行效率。
基于CAS实现线程安全的方式也称为“无锁编程”
优点:保证线程安全,同时避免阻塞,提升效率
缺点:1.代码更复杂,不好理解
2.只能够适合一些特定场景,不如加锁方式更普适
注意:Java中的原子类就是基于CAS实现的
CAS的ABA问题
CAS进行操作的关键,是通过值 “没有发生变化” 来作为 “没有其他线程穿插执行” 判定依据
但是在这种判定方式下,不够严谨,更极端的情况下,可能有另一个线程穿插进来,把值从A -> B ->A
针对第一个线程来说,看起来好像是这个值没变,但是实际上已经被穿插执行了。
ABA问题通常不会有bug,但是极端情况下就不好说了。例如下面这个场景:
假设我去ATM取钱,我本身账户1000,我想取500,但在取钱的过程中,出现bug了,我按下取钱按钮没反应,我就又按了一下,此时,就产生了两个线程进行扣款操作!
本来正常的两个线程操作可能没什么问题,但就在同一时刻,t2线程已经扣款后,某人又给我转了500,t3线程就对我的余额又增加了500,此时再回到t1线程,判断出value 和 oldvalue 相等,就又导致扣款成功,此时,我预期取五百,实际上却扣了一千!!!
解决措施:
针对这种情况,解决办法是引入一个额外的变量-版本号,约定每次余额,都要让版本号自增,此时在使用CAS判定的时候,就不是直接判定余额了,而是判定版本号,看版本号是否变化了,如果版本号不变,注定没有线程穿插执行。
结论:大部分情况下ABA问题其实都不是什么大事,但是在极端情况下,还是会有bug,只要让判定的数值,按照一个方向增长即可,不要反复横跳,如果有增有减,就可能出现ABA
synchronized原理
synchronized的重要机制
1.锁升级:无锁 —> 偏向锁 —>自旋锁(轻量级锁) —>重量级锁
偏向锁概念:偏向锁不是真的加锁,而是做了一个标记,当锁冲突出现的时候,偏向锁就会升级成轻量级锁。因为加锁是需要资源开销的,偏向锁的核心思想,就是 “懒汉模式” 的另一种体现,能不枷锁,就尽量不加锁,加锁意味着有开销。
结论:锁升级的过程就是在 性能 和 线程安全 之间进行权衡。
2.锁消除
锁消除也是一个编译器优化的手段
编译器会自动针对你当前写的 加锁的代码,做出判定,如果编译器觉得这个场景,不需要加锁,此时就会把你写的synchronized优化掉。
Java中有两个类:StringBudiler 和 StringBuffer , 前者不带锁,后者带锁。
如果在单个线程使用StringBUffer , 此时编译器就会自动的把synchronized给优化掉。
消锁条件:编译器只会在自己非常有把握的时候,才会进行消除。
3.锁粗化
锁的粒度:synchronized里头,代码越多,就任务锁的粒度越粗,代码越少,锁的粒度越细。
粒度细的时候,能够并发执行的逻辑更多,更有利于利用cpu资源,但是粒度细的锁,被反复加锁解锁,可能实际效果还不如粗的锁(涉及到反复的锁竞争)。