synchronized常见锁策略

目录

乐观锁和悲观锁

乐观锁如何检测冲突???

synchronized属于哪种???

普通的互斥锁和读写锁

synchronized属于哪种???

重量级锁和轻量级锁

synchronized属于哪种???

自旋锁和挂起等待锁

自旋锁的优缺点

公平锁和非公平锁

synchronized是哪种锁 ???

可重入锁和不可重入锁

 synchronized是哪种锁 ???

synchronized常见锁策略总结

CAS

CAS的应用场景

CAS中的ABA问题

synchronized锁升级过程

锁的其他优化

锁消除

锁粗化


乐观锁和悲观锁

悲观锁 : 悲观锁对发生并发冲突总假设最坏情况,预测锁冲突概率会很大,所以每次拿数据时都要进行加锁,如果其他线程想要获取此资源就要进行阻塞等待.----悲观锁往往要进入内核态--使当前线程阻塞等待.

乐观锁: 乐观锁则认为一般情况下不会发生并发冲突,而是当需要进行更新数据时,才来检测是否发生锁冲突,如果发生冲突交给用户处理----(纯用户态操作)

乐观锁和悲观锁举例 :

比如有小李和小明刚刚高考完,小李下了考场就感觉这次考试凉了,立马就想要复读(典型的悲观锁),而小明则认为是正常发挥,不管咋地高考完了先玩几天,等考试结果出来了再做打算(典型的乐观锁)

乐观锁如何检测冲突???

乐观锁一般是利用版本号来检测当前乐观锁是否发生冲突.

乐观锁的策略 : 当提交版本的版本号大于当前记录的版本号则更新成功,否则操作失败

 当线程1和线程2进行修改操作时,同时版本号也增加1,线程1修改时发现内存里版本号为1,则可以更新,当线程2再次写回内存时就会发现版本号一致则无法更新,操作失败,发生冲突.

synchronized属于哪种???

synchronized既属于乐观锁又属于悲观锁,是一种自适应锁.

刚开始锁冲突/竞争小时为乐观锁,随着锁冲突/竞争激烈,就变为悲观锁

普通的互斥锁和读写锁

互斥锁:像synchronized就是一种普通的互斥锁,当加锁时,另外的线程想要获取资源就要阻塞等待,等待释放锁.

读写锁 : 读写锁是将读和写分离开来;

  • 读加锁和读加锁之间不会产生互斥
  • 读加锁和写加锁之间会产生互斥
  • 写加锁和写加锁之间会产生互斥

读写锁适合频繁的读操作.

synchronized属于哪种???

synchronized不是读写锁,是普通的互斥锁

重量级锁和轻量级锁

  •  CPU为操作系统提供了一些原子操作的指令
  • 操作系统为JVM提供了mutex互斥锁---重量级锁
  • JVM为Java代码提供了两把锁 ReentractLock 和synchronized

重量级锁 : 重量级锁一般是指操作系统提供的mutex互斥锁,需要在内核态和用户态之间频繁的切换,容易引发线程调度.  ---- 常常是悲观锁

轻量级锁 : 一般是在用户态中进行操作,很少使用mutex互斥锁 ----常常会是乐观锁

synchronized属于哪种???

synchronized既是重量级锁又是轻量级锁.

刚开始为轻量级锁,随着锁竞争越激烈就转为重量级锁---自适应锁.

自旋锁和挂起等待锁

轻量级锁的内部实现是自旋锁. 重量级锁的内部实现是挂起等待锁'

自旋锁 : 当发生锁冲突时,获取锁失败后,会继续尝试获取锁(处于自旋等待),无限循环,直到锁被释放后,第一时间获取到锁.

挂起等待锁 : 当发生锁冲突时,会阻塞等待,不会第一时间释放锁

自旋锁的优缺点

当锁释放的时候,自旋锁会第一时间获取到锁,缺点是在自旋等待时,时刻占CPU,会导致大量CPU资源浪费

而挂起等待锁,在锁释放的时候不会第一时间获取到锁,但是在阻塞等待时,会释放CPU资源

公平锁和非公平锁

公平锁 : 这里的公平锁遵守 "先来后到"的原则,谁先加锁的就谁先获取到锁

非公平锁 : 这里的非公平 指的是 "同等竞争" 

synchronized是哪种锁 ???

synchronized是非公平锁.

没有任何限制的锁都是非公平锁,如果想要变为公平锁就需要额外的数据结构.

可重入锁和不可重入锁

可重入锁 : 表面意思可以理解为再次进入,"某个线程可以加多把锁",当加锁的线程与持有锁的线程是同一个的时候,不阻塞等待,可以直接获取锁,锁内部会记录是哪个线程加了锁,同时内部还有一个计数器来记录加锁的次数,通过这个计数器也可以知道什么时候释放锁.

不可重入锁 : 一个线程不能加锁多次,如果加了锁的线程和持有锁线程是同一个就会导致死锁状态.

即第一个锁的释放依赖第二个锁的获取,第二个锁的获取依赖第一个锁的释放--导致死循环

 synchronized是哪种锁 ???

synchronized是可重入锁

synchronized常见锁策略总结

  • synchronized既是乐观锁也是悲观锁,最开始为乐观锁,随着锁冲突/竞争激烈就会转为悲观锁
  • synchronized既是重量级锁也是轻量级锁,最开始为轻量级锁,随着锁冲突激烈,转为重量级锁
  • 轻量级锁的内部实现是基于自旋锁,重量级锁内部实现基于挂起等待锁
  • synchronized是普通的互斥锁,不是读写锁
  • synchronized是可重入锁
  • synchronized是非公平锁

CAS

什么是CAS ???

CAS(compare and swap)是一种原子的操作指令,有操作系统/硬件为JVM提供的一种硬件指令.

CAS伪代码 :

boolean CAS(address, expectValue, swapValue) {
    if (&address == expectedValue) {
        &address = swapValue;
        return true;
     }
        return false;
}

CAS的会执行如下的操作:

  • 将旧的预期值与内存中原有数据进行比较
  • 如果相等,就将新的值与内存数据交换.
  • 返回更新成功 否则返回失败

注意 : CAS上述3个操作都是原子的.---是一种硬件指令

多个线程操作CAS时只会有一个线程成功,其余线程不会阻塞,但是都会接收操作失败的信号.

CAS的应用场景

  • CAS实现原子类

伪代码如下 :

class AtomicInteger {
    private int value;
    public int getAndIncrement() {
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue+1) != true) {
            oldValue = value;
        }
        return oldValue;
    }
}

  • 线程1先进行操作,如果与内存原有数据相等就+1之后与内存数据交换.
  • 线程2在进行操作,与内存数据进行比较,发现不相等就进入循环,把内存数据赋值给线程2.
  • 线程2再次进行操作,与内存数据进行比较,发现相等,就对线程2的值+1然后与内存数据交换.

到此就完成了两个线程都对内存执行了两次++操作.

在Java类中也有原子类,能进行原子的++,--操作------>AtomicInteger

public static void main(String[] args) {
     AtomicInteger atomicInteger = new AtomicInteger();
     atomicInteger.getAndIncrement();//后置++;
     atomicInteger.incrementAndGet();//前置++;
     atomicInteger.decrementAndGet();//前置--;
     atomicInteger.getAndIncrement();//后置--
}
  •  CAS实现自旋锁

伪代码如下 :

public class SpinLock {
private Thread owner = null;
    public void lock(){
    while(!CAS(this.owner, null, Thread.currentThread())){

    }
}
    public void unlock (){
        this.owner = null;
    }
}

刚开始Thread线程对象owner为null;然后一直与null值进行比较.

如果为null,证明当前线程没有占用锁,就退出循环,把owner设为当前持有锁线程

如果不为null,证明当前线程占用了锁,进入循环,自旋等待.

CAS中的ABA问题

我们在执行CAS的操作时,就会拿A与内存中的数据进行比较,如果相等就进行交换/更新,但是这里就会出现问题,我们拿的A是始终是A,还是从A->B->A,CAS就会认为A始终没有变化,这就会导致问题.

举个例子

比如小明现金有1000元,小明要取钱,一不小心按下了两次取钱操作,这时就会出现两个线程,线程1与银行卡的前进行比较1000与1000相同,这时就扣款成功银行卡中只剩下500,此时线程2的CAS又对其执行扣款,线程2原来有1000与500进行比较扣款失败,此时没有问题,符合预期,但是当线程2读取时,又有小明的朋友转了500元,有来了个线程3,线程3中的500与银行卡中的500进行比较相等然后打钱成功,此时银行卡又剩下1000元,线程2读取时发现1000与1000相等此时又进行了一次扣款. 这就导致扣款两次,导致500元转丢了.我们并不知道银行卡中的500.始终是500还是500->1000->500

那如何解决CAS中ABA问题呢???

我们可以通过引入版本号来解决此问题.比较值的同时也要比较版本号,每次进行修改时,都要对版本号进行+1操作,如果版本号不相同则操作失败

此时引入版本号,再次进行推演上个例子

还是小明有1000元(版本号为1),此时要取款,线程1进行比较,1000(版本号为1)与1000(版本号为1),发现相等就扣款成功此时银行卡中有500(版本号2),线程2在读取时,线程3又对其打钱500,此时变为1000(版本号3),在线程2读取时又要扣款,线程2(版本号1,1000元)与线程3(版本号3,1000)不一致则扣款失败.

CAS还会有一些问题 :

循环时间开销大;

只能保证一个共享变量的原子操作.

synchronized锁升级过程

JVM有时会根据情况对synchronized进行锁升级;

基本的升级过程为  无锁===>偏向锁===>轻量级锁===>重量级锁

  • 无锁

没有加锁的状态

  • 偏向锁

当要进行加锁时,从无锁转为偏向锁.

注意理解偏向锁 :

偏向锁不是真正的加锁,而是给对象头标记为偏向锁状态.如果接下来不会产生锁冲突,就不会进行锁升级.偏向锁的目的就是尽量能不加锁就不加锁,延迟加锁,避免了加锁的开销.当发生锁冲突时,就取消偏向锁状态,进行锁升级--升级为轻量级锁.

  • 轻量级锁

这个轻量级锁时基于CAS实现的,CAS为其更新内存,更新成功,则加锁成功,否则处于自旋状态(始终占CPU).

  • 重量级锁

当竞争在激烈时就会转为重量级锁,此时会进入内核态,会看当前锁是否被占用,如果没有被占用就切换为用户态.如果被占用就需要阻塞等待,等待其他线程都释放锁之后,才尝试加锁


  • 总结

刚开始为无锁状态,随着要进行加锁,进入偏向锁状态,偏向锁不是真正的加锁,只是在对象头上做了个标记,标记为偏向锁状态,如果不产生锁冲突就一直保持当前状态,如果产生锁冲突,就需要进行锁升级,升级为轻量级锁.此轻量级锁基于CAS实现的,CAS会为其更新内存,如果更新内存成功,则加锁成功,否则进入自旋等待状态.此时自旋状态相当于CPU空转,浪费CPU资源.随着锁冲突/竞争激烈时,又转为重量级锁,此时会进入内核态.会看当前锁是否被占用,如果没有被占用,则切换为用户态加锁成功,如果当前锁被占用,会一直阻塞等待,等待其他线程释放锁,才尝试获取锁.


锁的其他优化

锁消除

比如StringBuilder的append方法是使用了synchronized来保证线程安全状态,而当我们在单线程使用append方法时,不涉及到线程安全问题,此时JVM和编译器就会对其优化,将其锁消除

锁粗化

这里是表示synchronized代码块的范围.

当代码块的范围越大时,锁的粒度就越粗.当代码块的范围越小时,锁的粒度就越细.

有时代码范围越小就导致反复的加锁释放锁,这时就会扩大代码块的范围,避免反复的加锁释放锁.

举例 :

比如向领导汇报工作,我是三件事情分成三次时间向领导汇报工作这样做不仅我烦,领导也会很烦,所以我就一次性的把三件事情全部汇报清楚.

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值