【JavaEE】常见锁策略、CAS

目录

常见的锁策略

[乐观锁 vs 悲观锁](#乐观锁 vs 悲观锁)

[重量级锁 vs 轻量级锁](#重量级锁 vs 轻量级锁)

自锁锁和挂起等待锁

读写锁

[可重入锁 vs 不可重入锁](#可重入锁 vs 不可重入锁)

[公平锁 vs 非公平锁](#公平锁 vs 非公平锁)

CAS

ABA问题

synchronized几个重要的机制

1、锁升级

2、锁消除

3、锁粗化


常见的锁策略

乐观锁 vs 悲观锁

乐观锁和悲观锁是锁的一种特性,不是一把具体的锁。

悲观和乐观,是对后续锁冲突是否频繁给出的预测:

  • 如果预测接下来锁冲突的概率不大,就可以少做一些工作,称为乐观锁
  • 如果预测接下来锁冲突的概率很大,就可以多做一些工作,称为悲观锁

重量级锁 vs 轻量级锁

轻量级锁,锁的开销比较小

重量级锁,锁的开销比较大

这两种锁和刚才的乐观悲观有关,乐观锁通常也就是轻量级锁,悲观锁通常也就是重量级锁。

自锁锁和挂起等待锁

自旋锁,属于轻量级锁的一种典型实现,往往是纯用户态实现,比如使用一个while循环,不停的检查当前锁是否被释放,如果没释放,就继续循环,释放了就获取到锁,从而结束循环(忙等,虽然消耗了cpu,但是换来了更快的响应速度)。

挂起等待锁,属于重量级锁的一种典型实现,要借助系统api来实现,一旦出现锁竞争了,就会在内核中触发一系列的动作(比如让这个线程进入阻塞状态,暂时不参与cpu调度)

读写锁

两个线程加锁过程中:

  • 读和读之间,不会产生竞争
  • 读和写之间,会产生竞争
  • 写和写之间,会产生竞争

把加锁分成两种:

  • 读加锁,读的时候,别的线程能读,但是不能写
  • 写加锁,写的时候,其他线程不能读也不能写

可重入锁 vs 不可重入锁

一个线程针对同一把锁,连续加锁2次,不会死锁,就是可重入锁,会死锁就是不可重入锁。

可重入锁会记录加锁线程信息,以便线程二次加锁,同时引入计数器,每加锁一次就+1,直到计数器为0才释放锁

公平锁 vs 非公平锁

当很多线程去尝试加一把锁的时候,一个线程能够拿到锁,其他线程阻塞等待,一旦第一个线程释放锁之后,接下来是哪个线程能够拿到锁呢?

公平锁:按照"先来后到"的顺序

非公平锁:剩下的线程以"均等"的概率,来重新竞争锁

操作系统提供的加锁api,默认情况就属于"非公平锁",如果要想实现公平锁,还需要引入额外的队列,维护这些线程的加锁顺序

上述这些锁策略都是描述了一把锁的基本特点的,synchronized属于哪种锁呢?

  • 对于"悲观乐观",自适应的
  • 对于"重量轻量",自适应的
  • 对于"自旋 挂起等待",自适应的
  • 不是读写锁
  • 是可重入锁
  • 是非公平锁

所谓自适应就是,初始情况下,synchronized会预测当前的锁冲突的概率不大,此时以乐观锁的模式来运行(此时也就是轻量级锁,基于自旋锁的方式来实现)

在实际使用过程中,如果发现锁冲突的情况比较多,synchronized就会升级成悲观锁(也就是重量级锁,基于挂起等待的方式来实现)

CAS

什么是CAS

CAS:全称Compare and swap,字面意思"比较并交换",一个CAS涉及到以下操作:

假设内存中存在原数据V,旧的预期值A,需要修改的新值B

  1. 比较A与V是否相等
  2. 如果比较相等,将B写入V
  3. 返回操作是否成功

比较交换的也就是内存和寄存器 ,如下例子:

有一个内存 M,两个寄存器 A,B

CAS(V,A,B)

  • 如果M和A的值相同,把M和B的值进行交换,同时返回true
  • 如果M和A的值不同,无事发生,返回false

CAS其实就是一个cpu指令,一个cpu指令就能完成上述比较交换的逻辑,单个的cpu指令,是原子的,就可以使用CAS完成一些操作,进一步的替代"加锁"。

基于CAS实现线程安全的方式,也称为"无锁编程"

java中AtomicInteger和其他原子类,就是基于CAS的方式对int进行了封装 ,进行++就是原子的了。

public class Main {

public static AtomicInteger count=new AtomicInteger(0);

public static void main(String[] args) throws InterruptedException {

Thread t1=new Thread(()->{

//count++

count.getAndIncrement();

//++count

count.incrementAndGet();

//count--

count.getAndDecrement();

//--count

count.decrementAndGet();

});

Thread t2=new Thread(()->{

//count++

count.getAndIncrement();

//++count

count.incrementAndGet();

//count--

count.getAndDecrement();

//--count

count.decrementAndGet();

});

t1.start();

t2.start();

}

}

前面的"线程不安全"本质上是进行自增的过程中,穿插执行了

CAS也是让这里的自增,不要穿插执行,核心思路和加锁是类似的

  • 加锁是通过阻塞的方式,避免穿插
  • CAS则是通过重试的方式,避免穿插

基于CAS实现自旋锁

public class spinLock{

private Thread owner=null;

public void lock()

{

while(!CAS(this.owner,null,Thread.currentThread())) {

}

}

public void unLock(){

this.owner=null;

}

}

ABA问题

CAS进行操作的关键,是通过值"没有发生变化"来作为"没有其他线程穿插执行的判断依据",但是这种判断方式不够严谨,更极端的情况下,可能有另一个线程穿插进来,把值从A->B->A,针对第一个线程来说,看起来好像是这个值没变,但是实际上已经被穿插执行了。

要避免ABA问题,我们可以让判定的数值,按照一个方向增长即可,有增有减就可能出现ABA,只是增加,或者只是减少就不会出现ABA,但是一些情况下,本身就应该要能增能减,我们可以引入一个额外的变量,版本号,约定每次修改余额,都会让版本号自增,此时在使用CAS判定的时候,就不是直接判定余额了,而是判定版本号,看版本号是否是变化了,如果版本号不变,注定没有线程穿插执行了。

synchronized几个重要的机制

1、锁升级

JVM将synchronized锁分为无锁,偏向锁,轻量级锁,重量级锁状态,会依据情况,进行依次升级。

无锁->偏向锁->自旋锁(轻量级锁)->重量级锁

锁升级的过程是单向的,不能再降级了。

偏向锁:不是真的加锁,只是做了一个标记,核心思想就是"懒汉模式"的另一种体现,如果有别的线程竞争锁再升级成轻量级锁。

2、锁消除

锁消除是编译器优化的手段,编译器会自动针对你当前写的加锁代码,做出判定,如果编译器觉得这个场景不需要加锁,此时就会把你写的synchronized给优化掉。

比如StringBuilder不带synchronized,StringBuffer带有synchronized,如果在单个线程中使用StringBuffer,此时编译器就会自动的把synchronized给优化掉

3、锁粗化

锁的粒度,synchronized里头,代码越多,就认为锁的粒度越粗,代码越少锁的粒度越细。

粒度细的时候能够并发执行的逻辑更多,更有利于充分利用好多核CPU资源,但是如果粒度细的锁,被反复进行加锁解锁,可能实际效果还不如粒度粗的锁。

相关推荐
Yan.love5 分钟前
开发场景中Java 集合的最佳选择
java·数据结构·链表
椰椰椰耶8 分钟前
【文档搜索引擎】搜索模块的完整实现
java·搜索引擎
大G哥8 分钟前
java提高正则处理效率
java·开发语言
VBA633719 分钟前
VBA技术资料MF243:利用第三方软件复制PDF数据到EXCEL
开发语言
轩辰~21 分钟前
网络协议入门
linux·服务器·开发语言·网络·arm开发·c++·网络协议
小_太_阳30 分钟前
Scala_【1】概述
开发语言·后端·scala·intellij-idea
向宇it30 分钟前
【从零开始入门unity游戏开发之——unity篇02】unity6基础入门——软件下载安装、Unity Hub配置、安装unity编辑器、许可证管理
开发语言·unity·c#·编辑器·游戏引擎
智慧老师39 分钟前
Spring基础分析13-Spring Security框架
java·后端·spring
lxyzcm40 分钟前
C++23新特性解析:[[assume]]属性
java·c++·spring boot·c++23
古希腊掌管学习的神1 小时前
[LeetCode-Python版]相向双指针——611. 有效三角形的个数
开发语言·python·leetcode