乐观锁和悲观锁

这两个锁属于锁的设计思想的概念(和语言无关,不是说java独有的东西,不是说仅仅属于多线程)设计思想其实本质上来自于使用的场景。

悲观锁

悲观锁:永远都觉得有人会和我抢锁,所以就对我要用的进行加锁。( 悲观,总是觉得会有人和我抢)

使用场景:

同一个时间点,经常有多个线程操作共享变量,适合悲观锁

java的具体实现:

synchronized的关键字
存在的问题:在多线程的竞争下,加锁和释放锁会存在着比较多的上下文切换(就会 涉及到操作系统用户态和内核态的切换)和调度延时,会引发相关的性能问题。

乐观锁

乐观锁:尝试去拿资源(永远都觉得没有人和自己抢占资源-乐观的心态),如果资源没有线程占用自己就用,如果资源人占着,就结合不同的实现方式。乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。

使用场景:

同一个时间点,经常只有一个线程操作共享变量,适合乐观锁。

原理:

  • 加锁的方式:直接加锁,要么直接加锁成功,要么加锁失败(线程安全的操作)
  • api调用方法的返回值可以知道加锁是否成功。

在java中的具体实现

基于CAS的方式,是一种轻量级的锁。

CAS

compare and swap 比较并交换,理解CAS的核心就是:CAS是原子性的

CAS有3个操作数: 内存值V 旧的预期值A 要修改的新值B

检查工作内存刚开始读取的值 和现在主内存的值是否还是一样的
如果一样,说明没人线程更改过。我就去改,把主内存的值改成我想要的样子。
如果不一样,说明其他线程改动了,那我就失败了。

当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值(A和内存值V相同时,将内存值V修改为B),而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试(或者什么都不做),这里就涉及到CAS失败之后的具体策略问题了。

我们可以发现CAS有两种情况:
如果内存值V和我们的预期值A相等,则将内存值修改为B,操作成功!
如果内存值V和我们的预期值A不相等,一般也有两种情况:

  • 重试(自旋) 分为有条件的自旋和无条件的自旋
  • 什么都不做
CAS失败后重试-自旋

自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。本身的java代码对于自旋锁并没有加锁的机制,只是说因为一直while循环,所以把它叫自旋锁,本身不是锁!不是锁!不是锁!只是这么叫!

获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。

  • 无条件的自旋,自旋锁(while !(cas(V,O,N)) ) 就是说我更改失败了,之后我的处理方式就是一直死等,等到我可以执行了。这种就是无条件的死等。显然,如果别的线程长期持有该锁,那么你这个线程就一直在 while while while 地检查是否能够加锁,浪费 CPU 做无用功。

  • 有条件的自旋:例如可中断的自旋(自旋的时候判断线程的中断标志位后在执行(比如获得锁的线程被阻塞住了 )或者是自旋次数/时间来控制
    具体这里的细节问题很深,这里不做过多阐述。

CAS失败后什么也不做

在这里插入图片描述

CAS出现的问题ABA

ABA问题 就是一个值从A变成了B又变成了A,而这个期间我们不清楚这个过程 。主存被改动然后又被改回到初始的状态,这个期间我不知道。(我们的目的是要保证没有被修改过,而现在的做法其实是看值有没被修改过)

解决的方式

加上版本号,每对主存中的值进行修改一次,版本号就加一

为什么要有这个锁?synchronized锁不够吗?

原子性的并发包下的api

是JUC并发包下的一个包 java.util.concurrent.atomic, 基于CAS的并发包和API。原子性并发包就是可以保证原子性。

问题引入:我们使用count++是不安全的,count++是非原子性的操作,但是如果用synchronized加锁,效率低下,所以有了一个原子性的并发包解决阻塞问题,保证了线程安全。

public class AtomicIntegerTest {
  private static AtomicInteger atomicInteger = new AtomicInteger();
  public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
      new Thread(new Runnable() {
        @Override
        public void run() {
          for (int i1 = 0; i1 < 200; i1++) {
            //但是由于本身线程的任务并不是耗时的但是线程不断地在切换状态所以这样的做法并不好
            atomicInteger.incrementAndGet();
              //之前的加锁方法
             /*synchronized (AtomicIntegerTest.class){
              count ++;
            }*/
          }
        }
      }).start();
    }
    while (Thread.activeCount() >1){
      Thread.yield();//main线程让步
    }
    System.out.println(atomicInteger.get());
  }
}

实现类:AtomicInteger,AtomicLong,AtomicBoolean等

原理:基于unsafe类实现的,使用CAS和自旋结合的方式
Atomic包的类的实现绝大调用Unsafe的方法,而Unsafe底层实际上是调用C代码,C代码调用汇编,最后生成出一条CPU指令,完成操作。这也就为什么CAS是原子性的,因为它是一条CPU指令,不会被打断。

对比synchronzied的好处:线程不需要在用户态和内核态切换,一直处于运行态,效率更高(需要同步的代码执行的时间很短,适合CAS。对于同步代码块执行时间比较短的代码来说,如果不用CAS,用synchronizaed 状态切换的耗时都比任务执行还要耗时,是一件很不划算的事情。)

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐