多线程(锁策略,CAS,synchronized锁原理和锁优化)

锁策略

  1. 乐观锁 VS 悲观锁
    乐观锁:假设数据发生并发冲突的概率低,所以只在数据修改完之后进行提交更新时才对数据进行并发冲突检测,如果发现并发冲突,则返回给用户错误的信息.
    悲观锁:假设数据发生并发冲突的概率高,所以每次对数据进行修改时都会加上锁,当别人想拿这个数据时会产生阻塞等待.
    乐观锁相较于悲观锁而言,所做的工作更少,但就效率而言,两者的效率取决于应用的场景.当场景中数据发生冲突的可能性比较低时,乐观锁的效率比较高,因为悲观锁会在每次对数据进行修改时加锁,浪费了不必要的锁资源;当场景中数据发生冲突的可能性比较高时,悲观锁的效率比较高,因为乐观锁会持续给用户输出并发冲突的信息,会耗费更多的资源.
    对于synchronized而言,在初始时使用乐观锁,当锁竞争比较频繁的时候会自动切换成悲观锁.
    对于乐观锁而言,乐观锁需要检测出数据是否发生并发冲突,这种检测手段可以通过引入一个"版本号"来解决:我们规定数据发生修改后提交时的版本号必须大于修改前的版本号,这样当两个操作同时对一个数据进行修改时,后一个完成的操作在提交时会发现自己的版本号与修改前的版本号相同,不满足提交策略,进而推断出数据发生了并发冲突.在这里插入图片描述

  2. 读写锁 VS 互斥锁

    1. 读写锁;在线程安全问题中,只有在多个线程同时对一个数据进行"写"操作时才会发生线程不安全,如果只是"读"数据,则不会产生线程安全问题.读写锁,顾名思义是对读和写两个操作分别提供锁.因此读写锁有3中情况:
    1. 读加锁 和 读加锁:不互斥
    2. 读加锁 和 写加锁:互斥
    3. 写加锁 和 写加锁:互斥(互斥意味着线程和线程之间会发生阻塞等待.)

    Java标准库中提供了ReentrantReadnWriteLock类,可以实现读写锁

    1. ReentrantReadWriteLock.ReadLock类表示一个读锁,该对象提供了lock/unlock方法
    2. ReentrantReadWriteLock.WriteLock类表示一个写锁,该对象提供了lock/unlock方法

    读写锁适用于频繁读不经常写的场景,事实上这种场景十分常见,对于普通用户而言,其在日常 生活中访问一个软件更多的是采取读操作.
    2. 互斥锁:不区分读操作或者写操作,统一加锁且均互斥.synchronized是互斥锁

  3. 轻量级锁 VS 重量级锁
    锁的核心特性"原子性",这种特性追根溯源是CPU这样的硬件设备提供的.首先CPU提供了"原子操作指令";操作系统基于CPU的原子指令实现了mutex互斥锁;JVM基于操作系统提供的互斥锁实现了synchronized和ReentrantLock等关键字和类.

    • 重量级锁:加锁机制重度依赖了OS提供的mutex:大量的内核态和用户态的转换以及容易引发线程的调度.重量级锁其实是悲观锁的一种具体体现.
    • 轻量级锁:加锁机制尽量不使用mutex,而是在用户态完成:少量的内核态和用户态的切换以及不容易引发线程的调度.轻量级锁其实是乐观锁的一种具体体现.
      synchronized锁在开始时是一把轻量级锁,当锁竞争频繁时会自动切换成重量级锁.
  4. 挂起等待锁 VS 自旋锁

    1. 挂起等待锁:指的是当发生并发冲突时,其他线程会产生阻塞等待(阻塞等待不占用CPU资源),挂起等待锁其实是重量级锁的一种具体实现方式.
    2. 自旋锁:当线程抢锁失败后不会产生阻塞等待,而是继续尝试抢占该锁,当占据锁的其他线程释放锁后,该线程就会在第一时间内抢占到锁.一直尝试抢占锁会持续消耗CPU资源,但这种消耗相比较与挂起等待后然后再调度该线程去抢占锁小号的资源要小的多.自旋锁其实是轻量级锁的一种具体实现方式.
  5. 公平锁 VS 不公平锁

    1. 公平锁:当多个线程去抢占一把锁时,处于阻塞等待的多个线程会根据其"先来后到"的规则去抢占锁,即当A,B,C三个线程去抢占一把锁且A抢占到锁以后,B,C线程谁先抢占到该锁取决于B,C那个线程先执行.在这里插入图片描述

    2. 非公平锁:和公平锁相反,处于阻塞等待的多个线程并不遵守"先来后到"的规则,所有线程抢占到该锁的概率是均等的(随机的).在这里插入图片描述

  6. 可重入锁 VS 不可重入锁

    1. 可重入锁:指的是一个线程可以多次嵌套获取到同一把锁而不会产生死锁的情况.在Java中关于锁的关键字和类都是可重入锁
    2. 不可冲入锁:指的是一个线程不可以多次嵌套获取同一把锁,否则会产生死锁的情况,在Linux中的mutex是不可冲入锁.

CAS

  1. 概念:CAS全称为Compare and Swap,即比较和交换,是一种通过硬件实现并发安全的技术.一个CAS涉及如下操作:比较原数据X与旧的预期值Y是否相等,如果相等将新的预期值Z和原数据X进行交换,然后返回交换操作是否成功发生**.CAS是一种原子性操作,由一个原子性的硬件指令完成**.因此当多个线程对某个数据进行CAS操作不会产生线程安全问题,因此操作失败的线程不会阻塞,只会收到操作失败的信号.(类似于乐观锁).
    在这里插入图片描述
  2. CAS的实现:针对不同的操作系统,JVM用到了不同的CAS实现原理,简单来讲就是:
    1. Java的CAS利用是UnSafe这个类提供的CAS操作.
    2. UnSafe的CAS依赖了JVM针对不同的操作系统实现的Atomic::cmpxchg(CPU中的比较和交换指令);
    3. Atomic::cmpxchg的实现使用了汇编的CAS操作,并使用CPU硬件提供的lock机制保证其原子性.
      简单来说,是因为通过硬件的支持,软件层面才能实现CAS.
  3. CAS的应用
    1. 自旋锁:在之前我们已经介绍过自旋锁.这里主要介绍一下自旋锁是如何基于CAS操作实现的.在这里插入图片描述
      首先通过CAS看这个锁是否被其他线程占有,如果没有占,则将当前线程与null进行交换;如果已经被占有,则其他线程不不会进行阻塞等待,而是继续进行"忙等".

    2. 原子类:Java中的java.util.Cuurent,atomic包中的类均为原子类.常见的原子类有AtomicInteger,AtomicLong,AtomICBoolean等等.其中最常用的就是AtomicInteger类,其中的getAndIncrement操作相当于i++操作(当然是线程安全的)

	 //用CAS实现的原子类,既能保证线程安全,同时也能保证运行效率(不会产生阻塞等待,double)
        AtomicInteger a = new AtomicInteger(0);
        Thread t1 = new Thread(()->{
            for(int i = 0; i < 50000;i++){
                a.getAndIncrement();
            }
        });
        Thread t2 = new Thread(() ->{
            for(int i = 0; i < 50000; i++){
                a.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(a.get()); //运行结果为10_0000

上述代码是基于原子类实现的多线程自增操作,既保证了线程安全,同时也提高了运行效率(不用阻塞).下面我们来了解一下基于CAS实现的原子类为什么能够保证线程安全.
伪代码:
在这里插入图片描述
在这里插入图片描述
4. CAS的ABA问题

  1. ABA问题介绍:假设有两个线程1,2分别对同一个数据X进行修改,我们初始目的是如果数据X没有发生改变,我们就通过线程1将X改为A.首先线程1先读取数据X的值,然后进行CAS操作,但在线程1进行CAS之前,线程2将数据X改成了Y,然后又改成了X.然后线程1进行CAS操作,但这个时候我们还能认为数据X是没有发生过改变吗?
  2. ABA问题的解决方案**:引入版本号**.线程在读取数据的同时也要读取版本号.在进行CAS操作的同时要判断当前版本号和读到的版本号是否相同,如果相同则进行CAS操作,并将版本号加1;当读到的版本号小于当前的版本号时,就返回操作失败.现在我们重新来看待1中的ABA问题:首先线程1和2都读取数据X到旧值中并读取到版本号为1,线程1在进行CAS操作之前,线程2成功进行了两次CAS操作,所以当前的版本号加2变为3.轮到线程1进行CAS操作,虽然旧值和X相同,但是由于读到的版本号小于当前的版本号,因此返回操作失败.

synchronized原理及优化

  1. 基本特征
    1. 初始时是乐观锁,如果锁竞争变频繁就自适应成悲观锁.
    2. 初始时是轻量级锁,如果锁被持有的时间变长就自适应成重量级锁.
    3. 实现轻量级锁的时候大概率用的是自旋锁.
    4. 是一种不公平锁.
    5. 是一种可重入锁.
    6. 是一种互斥锁.
  2. 加锁工作原理
    JVM(jdk1.8以后)将synchronized锁分为无锁,偏向锁,轻量级锁,重量级锁状态,会根据情况,进行依次升级.
    1. 偏向锁:第一个尝试加锁的线程优先进入偏向锁状态.偏向锁不是真的"加锁",而是在对象头中加一个"偏向锁的标记",纪录这个锁属于哪个线程.如果后续没有其他线程来竞争这把锁,则不需要进行一系列加锁的流程,进而节省了资源.如果后续有其他线程来竞争该锁,因为之前已经在锁对象的对象头中纪录了这把锁属于哪个线程),所以可以很容易地退出当前的"偏向锁状态"从而进入下一个"轻量级锁状态". 偏向锁实际是一种"延迟加锁":能不加锁就不加(类似于懒汉模式)
    2. 轻量级锁:线程之间开始竞争锁,这里的轻量级锁就是通过自旋锁实现的.
    3. 重量级锁:当线程之间的锁竞争变频繁,自旋不能获取到锁状态,就会膨胀为重量级锁.
  3. 加锁优化操作
    1. 锁膨胀:锁膨胀就是随着锁竞争的逐步加剧逐步自适应成加锁力度更大的锁.
    2. 锁消除:在某些代码中,程序其实并没有进入多线程状态但是却被加上了锁,如StringBuffer.这时候编译器和JVM就可以通过优化将必要的锁给消除.
    3. 锁粗化:当一段代码中出现不必要的频繁的加锁释放锁的过程时,为了节省资源消耗,JVM就会把这一整段代码粗化,减少加锁释放锁的次数.这里的粗化指的是锁的粒度.锁粗化程度越高,隔离性越强;锁细化程度越高,并发性越强.

总结

本篇文章主要介绍了常见的锁策略以及CAS和CAS中的ABA问题还有synchronized的原理以及优化.这些都是面试中常考的多线程的知识,希望大家能够有所收获!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

囚蕤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值