java多线程(四)—— 乐观锁和悲观锁

java多线程(四)—— 乐观锁与悲观锁

  我们在学习java多线程的过程中,总是听到别人说什么乐观锁和悲观锁,那到底什么是乐观锁?什么是悲观锁呢?我们在多线程的并发编程中会遇到对共享资源的操作,而多个线程并发的对共享资源操作会发生线程安全问题,也就是使得共享资源产生混乱,最终得到的结果值与预期值是不一致的,为了解决这种问题,就提供了两种思想来解决,一种是悲观锁,另一种是乐观锁,所以它们只是解决对共享资源操作时产生不一致问题的两种解决思想而已。下面将介绍乐观锁和悲观锁的概率,以及具体在java中的实现。

1、悲观锁

  悲观锁由名字我们就能探究一二,“悲观”表示的它就是一个悲观主义者,它对共享资源的使用是持悲观的态度,它觉得在它使用共享资源的过程中一定会被其他线程更改,所以为了防止线程安全问题的发生,在它使用共享资源的时候,会对共享资源加锁,不允许其他的线程进行使用,待它自己使用完以后,释放掉锁,其他线程才能去使用该共享资源,这就是悲观锁的思想。
  在java中通过使用synchronized、ReentrantLock等可独占锁来实现悲观锁这种思想,它是通过对共享资源加琐的形式来实现的,具体对于synchronized、ReentrantLock的使用以及原理可以参考我以往的文章:
  java多线程(一)—— 基础使用篇
  java多线程(二)—— synchronized锁原理
  悲观锁及其实现会有如下的两个缺点:

  1. 多读操作时效率较低

  悲观锁的思想使得不管线程对共享资源的操作是读或是写,都要对资源进行加锁,此时其他线程只能等着线程释放完锁以后才能去使用,试想一下,如果多个线程对于共享资源的操作仅仅是读,那其实这种情况并不会造成线程安全问题,使用悲观锁的思想,就得让一个线程等待上一个线程使用完以后自己才能够使用,这样会使得运行效率低下。所以如果对共享资源的操作方式是多读,使用这种思想会降低程序的运行速度,而在多写的情况下使用这种思想就会比较划算,相对于乐观锁的方式对性能的损耗就不会太大。

  1. synchronized频繁的线程切换,比较耗时,造成运行速率较低

  在悲观锁的实现synchronized的使用过程中,synchronized是通过一个monitor对象来对共享资源进行加锁,当多个线程竞争一个共享资源,monitor会将没有竞争到共享资源的线程置于一个阻塞队列,等待获得共享资源的使用权;而竞争到共享资源的线程由于缺少某些运行条件,monitor就会将该线程置于一个等待队列,待唤醒以后就会进入到阻塞队列;这一系列的线程状态的转化,需要操作系统从用户态转换为内核态,这个过程是比较耗费cpu资源,所以也就使得synchronized的效率较低。

2、乐观锁

  乐观锁这种思想表达的也就是一个乐观主义的思想,它觉得在自己读取共享资源的过程中,该资源是不会被其他线程所修改,所以它会放心大胆的使用,不会对该资源进行加锁,而在自己将要对该资源进行修改也即写操作时,会检查该资源和自己读取时的值是否一致,如果一致自己就直接进行写操作,否则,得要去重新读取该资源,进行一系列处理,直至自己进行写操作时,该值和读取时的值一致才真正的进行写操作;这种思想也很好理解,读取时大胆的读取,修改时小心翼翼,如果自己修改的时候该值和刚读取时的值是一样的,证明在自己使用这个资源的过程中,并没有被其他线程所修改过,所以自己就可以进行写操作了,这样就能够保证线程安全了。因此,不管是悲观锁还是乐观锁都能够解决线程安全问题。
  乐观锁的优点:

  1. 乐观锁的实现会显得比较轻量,它没有悲观锁中去维护一个臃肿的锁对象,和进行线程切换的负担,所以于悲观锁相比,就比较轻量,执行的效率也要高一点。
  2. 很显然乐观锁比较适合多线程多读的情况,因为多个线程都能同时读取到资源,而少量的写操作耗费的时间也不算多,这就使得当多线程对共享资源进行读多写少的操作时,使用乐观锁时效率就比较搞了。

  乐观锁的缺点:

  1. 当多线程对共享资源写操作的情况多一点,就会使得乐观锁的方式速度大大降低,因为乐观锁再要更改共享资源时,会先去判断该共享资源和自己刚读取的时候是否一致,如果一致,就修改,但如果不一致,就会去重新读取该共享资源,再去做一系列的处理,修改时又重复以上的过程;那如果是一个频繁进行写操作的情况下,那可能写操作可能总是成功不了,这样就会一直去循环读资源、资源处理、不能修改资源的这个流程,就会一直在耗费资源,且使得执行速度降低。
  2. 会出现ABA问题,就是在我判断能不能进行写操作时,共享资源和读取时的值是一样的,但是该值其实是被修改过的,只是它的值又被修改回来了而已;就比如一个线程读取时是A,然后被其他线程修改为B,又被另一个线程更改会了A,那当前线程将要做写操作判断时就得到结论是没有被修改过,因而就可以进行写操作了,但是其实此值非彼值了,当然,这种情况对最终的结果是没有影响的。

  乐观锁的实现方式有两种:版本号控制和cas算法来进行实现,java中对这两种方式的实现都有,但其中最重要的方法是cas算法,接下来我会介绍一下在java中的具体实现。

3、CAS算法及java实现

  接下来介绍一下到底什么是CAS算法,它在java中又是如何实现的呢?

3.1、CAS算法

  CAS的全称为compare and swap,即比较和替换,也叫做compare and set,即交换和设置,它的算法流程大概如下图所示,通过这个流程图就可以明白cas算法到底在干什么了。
在这里插入图片描述
  大概的伪代码为如下:

 while(true){
            int oldR = r;//读取共享资源r
            /*
            做一些操作
             */
            int result = oldR + 1;//准备将result写入到r当中,即准备将r值设置为result
            /*
            做一些操作
             */
            if(compareAndSwap(oldR,result)){//compareAndSwap这个方法就是上面流程中标注的那三个步骤,
            								//重新读取r的值,再与oldR比较,如果相等就将r的值修改为result返回true,跳出循环
            								//否则不能修改r的值,返回false,继续循环。
            								//注意compareAndSwap这个方法的执行过程必须满足原子性,
            								//一般是通过操作系统的原子操作来实现
                break;
            }
 }

  以上就是cas的算法流程了,通过以上的流程我们来分析分析为什么乐观锁适合于多读少写的情况,如果在我们的程序中对共享资源的写比较频繁,那当我们比较共享资源的旧值和旧值,也就是比较oldR和currentR是否相等,因为对于共享资源的修改的次数比较读,所以在比较时很大概率上是不相等的,那此时又要重新循环做一遍重复的操作,循环开销就变大了,也就是如果写的次数比较多,那循环的次数也就会增加,使得浪费cpu资源,造成执行效率低下,所以乐观锁比较适合于多读少写的情况。

3.1、cas在java中的实现:原子变量类

原子变量类可以分为 4 组:

  1. 基本类型
  • .AtomicBoolean - 布尔类型原子类
  • AtomicInteger - 整型原子类
  • AtomicLong - 长整型原子类
  1. 引用类型
  • AtomicReference - 引用类型原子类
  • AtomicMarkableReference - 带有标记位的引用类型原子类
  • AtomicStampedReference - 带有版本号的引用类型原子类
  1. 数组类型
  • AtomicIntegerArray - 整形数组原子类
  • AtomicLongArray - 长整型数组原子类
  • AtomicReferenceArray - 引用类型数组原子类
  1. 属性更新器类型
  • AtomicIntegerFieldUpdater - 整型字段的原子更新器。
  • AtomicLongFieldUpdater -长整型字段的原子更新器。
  • AtomicReferenceFieldUpdater - 原子更新引用类型里的字段。
3.1.2、AtomicInteger的简单使用

  我们就以AtomicInteger为例,来介绍原子类的使用以及它的实现原理。我们实现上面流程图的实例,将共享资源进行r进行加1操作。

方法一:自己写循环的逻辑

 public static void main(String[] args) {
       AtomicInteger r = new AtomicInteger(4);
        System.out.println("pre:  " + r.get());
       while (true){
           int oldR = r.get();
           int result = oldR + 1;
           if(r.compareAndSet(oldR,result)){
               break;
           }
       }
        System.out.println("after: " + r.get());
    }

结果:
在这里插入图片描述

方法二:使用原理类提供的一些计算函数

  public static void main(String[] args) {
       AtomicInteger r = new AtomicInteger(4);
        System.out.println("pre:  " + r.get());
        int v = r.incrementAndGet();//++r
//        r.getAndIncrement();//r++
        System.out.println("after: " + v);
    }

结果:
在这里插入图片描述

以下是AtomicInteger类常用的一些方法:

public final int get() // 获取当前值
public final int getAndSet(int newValue) // 获取当前值,并设置新值,满足原子性
public final int getAndIncrement()// 获取当前值,并自增,满足原子性
public final int getAndDecrement() // 获取当前值,并自减,满足原子性
public final int getAndAdd(int delta) // 获取当前值,并加上预期值,满足原子性
boolean compareAndSet(int expect, int update) // 如果输入值(update)等于预期值,将该值设置为输入值,满足原子性
public final void lazySet(int newValue) // 最终设置为 newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

3.1.2、AtomicInteger的原理分析

在这里插入图片描述
  我们可以看到AtomicInteger类中的真实值是存储在属性value中,该属性前增加了volatile关键字,也就代表了对该属性值的操作能够保证可见性和有序性,那它的原子性将有什么来实现呢?很显然,就得考unsafe这个属性去实现对value值操作的原子性,那我们来看看它的原子性具体是如何实现的。
  compareAndSet(int expect,int update)方法:
  我们来看看AtomicInteger中compareAndSet是如何实现的,他是通过调用unsafe中的compareAndSwapInt方法来实现,valueOffset表示的是在AtomicInteger对象中内存的偏移量,把一个AtomicInteger对象存储在内存中,然后通过valueOffset来表示value值在该对象内存中的位置,就可以得到value的值,所以expect表示的是oldR旧值,而通过传入valueOffset来获取当value的当前值。
在这里插入图片描述
  unsafe实现compareAndSwapInt(this,valueOffset,expect,update)方法直接是通过一个本地方法,也就是c语言实现了该方法。这也能够理解,因为我们的cas算法中,要使得value的旧值与当前值的比较、更改value的值的过程具备原子性,这必须得通过操作系统的原子操作才能够实现,所以就只能使用本地方法实现了。
在这里插入图片描述
  incrementAndGet()方法:
  incrementAndGet也就是实现++i的操作,也是通过unsafe兑现来实现。它的一个实现流程如下:首先通过unsafe的getAndAddInt()方法将AtomicInteger对象中的value值进行加1,但是返回的是加1之前的value值,所以最终返回的值应该要在getAndAddInt()后加上1。
在这里插入图片描述

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值