java中如何实现悲观锁与乐观锁

发布时间:2021-06-30 17:54:16 作者:Leah
来源:亿速云 阅读:1215

今天就跟大家聊聊有关java中如何实现悲观锁与乐观锁,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。

1、 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。在Java语言中synchronized关键字的实现就悲观锁。

2、乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS( Conmpare And Swap 比较并交换)实现的。

从上面的描述我们可以看出2种锁其实各有优劣,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

    悲观锁其实没什么好讲,这里主要讲解写乐观锁。

    JAVA的乐观锁主要采用CAS算法即 compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。正因为不断重试所以如果长时间不成功,会给CPU带来非常大的执行开销。

这样说或许有些抽象,我们来看一个例子:

1.在内存地址V当中,存储着值为10的变量。

java中如何实现悲观锁与乐观锁

2.此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。

java中如何实现悲观锁与乐观锁

3.在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。

java中如何实现悲观锁与乐观锁

4.线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。

java中如何实现悲观锁与乐观锁

5.线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。

java中如何实现悲观锁与乐观锁

6.这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。

java中如何实现悲观锁与乐观锁

7.线程1进行SWAP,把地址V的值替换为B,也就是12。

java中如何实现悲观锁与乐观锁

在JAVA中是通过Unsafe类提供的一系列compareAndSawp*方法来实现

首先我们先研究下Unsafe,初始化Unsafe用到Unsafe.getUnsafe() ;

通过查看源码我们发现这个类我们不能直接使用

@CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        //判断调用类是否BootstrapClassLoader加载,Unsafe是系统Jar包按JAVA双亲委派模式,这个类是由BootstrapClassLoader加载的。而普通项目的类是由CustomClassLoader加载
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }

我们继续看源码发现有3个CAS操作方法

var1: 要修改的对象 

var2: 对象中field的偏移量 

var4: 期望值(预期的原值) 

var5: 更新值 

返回值 true/false

这里可以看到这几个方法都是native方法,低层都是调操作系统的方法,这里不深入研究

    public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

    public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

因为Unsafe不方便调用(当然我们可以通过反射勉强也可以用),所以我们只能拿AtomicInteger来研究下。我们new AtomicInteger()时,会获取AtomicInteger对像Value字段的偏移量

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
           // 通过Unsafe方法获取value字段的偏移量(可以理解为C++的指针)
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    private volatile int value;
    ...
}

我们再来看下atomicInteger.getAndIncrement()这个方法的实现

/**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

它最终调用的是unsafe的getAndAddInt方法,我们继续往下跟踪

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

由上图可知,var1是AtomicInteger对象,var2是AtomicInteger对象value字段的偏移量,var5是期望值expected,var5+var4=var5+1。这么说可能比较清楚点,例如:

AtomicInteger atomicInteger = new AtomicInteger(2);
        int a = atomicInteger.getAndIncrement() ;

则var5为2,var5+var4=2+1=3。我们从上上图可以看出,这里用了个循环也就是说当期望值不是2时会一直循环尝试。相等就把x值赋值给offset位置的值,不相等,就取消赋值,方法返回false。这也是CAS的思想,及比较并交换。用于保证并发时的无锁并发的安全性。

这里有同学可能会担心死循环问题,其实不会的大家可以看下AtomicInteger那个类是设置成volatile类型,也就是内存可见所有线程获取到的值都是最新的。但是用循环却会产生ABA的问题。 即如果在此之间,V被修改了两次,但是最终值还是修改成了旧值V,这个时候,就不好判断这个共享变量是否已经被修改过。

java中如何实现悲观锁与乐观锁

为了防止这种不当写入导致的不确定问题,原子操作类提供了一个带有时间戳的原子操作类。带有时间戳的原子操作类AtomicStampedReference  CAS(V,E,N)当带有时间戳的原子操作类AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳。当AtomicStampedReference设置对象值时,对象值以及时间戳都必须满足期望值,写入才会成功。因此,即使对象值被反复读写,写回原值,只要时间戳发生变化,就能防止不恰当的写入。

以下是AtomicStampedReference类的compareAndSet方法

/**
* Params:
expectedReference – 当前值
newReference – 修改后的值
expectedStamp – 当前时间戳
newStamp – 修改后的时间戳
return true/false
*
*/ 
public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp)

以下是我的测试例子

import java.util.concurrent.atomic.AtomicStampedReference;

public class CASTest {
    public static void main(String[] args) {
        int initialStamp = 1;

        AtomicStampedReference<String> atomicStringReference = new AtomicStampedReference<String>( "value1", initialStamp);


        boolean exchanged1 = atomicStringReference.compareAndSet("value1", "value2", initialStamp, initialStamp+1);
        System.out.println("exchanged: ">

看完上述内容,你们对java中如何实现悲观锁与乐观锁有进一步的了解吗?如果还想了解更多知识或者相关内容,请关注亿速云行业资讯频道,感谢大家的支持。

推荐阅读:
  1. 面试必备之悲观锁与乐观锁
  2. 悲观锁与乐观锁怎么利用Hibernate实现

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

java

上一篇:powercat的使用方法

下一篇:JAVA中如何使用枚举

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》