什么是乐观锁(无锁并发)

管程:悲观锁。无锁:乐观锁

1、CAS

CAS,compareAndSet。调用CAS时,它会检查该属性当前的值是否等于第一个参数,如果等于,就将它替换成第二个参数,并返回true。

它是一个原子操作,底层是一条lock cmpxchg指令,在单核、多核都能保证原子性

多核下如何保证原子性?

  • 在多核下,某个核心执行到带lock的指令时,CPU会锁住总线,直到这个核心把指令执行完毕才开启总线。
  • 这个过程不会被线程的调度机制打断,即不存在某个线程改变数据的中间状态,只会有两种情况:改了、没改。

示例

AtomicInteger是JDK提供的原子整数类型。

private AtomicInteger balance;

public void withdraw(Integer amount) {
    while(true) {
        // 需要不断尝试,直到成功为止
        while (true) {
            // 比如拿到了旧值 1000
            int prev = balance.get();
            // 在这个基础上 1000-10 = 990
            int next = prev - amount;
            /*
 			compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值不一致了,next 作废,返回 false 表示失败
 			比如,别的线程已经做了减法,当前值已经被减成了 990 那么本线程的这次 990 就作废了,进入 while 下次循环重试 一致,以 next 设置为新值,返回 true 表示成功
 			*/
            if (balance.compareAndSet(prev, next)) {
                break;
            }
        }
    }
}
  • 先获取共享变量当前的值
  • 做出自己的计算
  • 使用CAS修改共享变量的值。如果共享变量当前的值不等于“第一步时获取的值”,说明有别的线程已经修改了该变量,那么本次CAS操作失败,返回false。乐观锁的思想。
  • 注意:CAS方法是原子的,但之前这个获取值的操作,和最后的CAS之间,可能会发生线程上下文切换,所以会有CAS失败的情况。

2、CAS必须搭配volatile一起使用

上面的AtomicInteger,源码中使用了volatile的int来保存数据。

获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。

CAS必须借助volatile才能读取到共享变量的最新值,来实现“比较并交换”的效果。

3、乐观锁的特点

结合CAS和volatile可以实现无锁并发(乐观锁),适用于线程数少、CPU核心数多的情况。活跃线程数不要多于核心数量,否则肯定会发生线程上下文切换。

乐观锁是无锁+失败重试的思想,它和加锁的区别:

  • 线程不需要获取锁,也就不会被阻塞。线程可以在时间片内一直重试,减少了额外的线程上下文开销
  • 乐观锁适合竞争小的场景。如果竞争激烈,大多数的重试都会再次失败,影响效率,此时应该考虑上锁

乐观锁的问题

  • ABA问题,使用版本号解决
  • 死循环问题,CAS操作长时间不成功的话,会导致一直自旋,相当于死循环了,CPU的压力会很大
  • 只能保证一个共享原子变量的操作问题(使用AtomicReference解决,将多个对象放入CAS中操作)

4、原子整数

使用volatile定义一个整数:

volatile int a = 0;

在多线程下执行a++的操作,并不能保证线程安全。volatile只能保证每次都读取到最新的值,线程安全需要CAS来保证

JUC并发包提供了一些CAS的工具类:

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong

比如AtomicInteger,它的方法都是原子的

AtomicInteger i = new AtomicInteger();

//获取并自增,类似i++
i.getAndIncrement();

//自增并获取,类似++i
i.incrementAndGet();

//获取并自减,类似i--
i.getAndDecrement();

//自减并获取,类似--i
i.decrementAndGet();

//获取并增加
i.getAndAdd(5);

//增加并获取
i.addAndGet(5);

i.getAndUpdate(value -> value*2);

i.getAndAccumulate(10, (p,x)->p%x);
  • getAndUpdate,可以随便对原子整数做运算。如果在lambda中引入局部变量,局部变量必须是final的
  • getAndAccumulate,可以通过参数1来引入外部变量,其不在lambda中,不要求final

这些方法底层都是使用了compareAndSet的CAS操作来实现的。

5、原子引用

原子引用类型:

  • AtomicReference
  • AtomicStampedReference
  • AtomicMarkableReference

原子引用是为了保证,修改引用值时的原子性。即实现对象的替换,而不是实现对象某个属性的修改。

1、AtomicReference

比如需要使用BigDecimal,想要保证它的原子性,就需要使用原子引用类型。

public class DecimalAccountCas implements DecimalAccount{

    private AtomicReference<BigDecimal> balance;

    public DecimalAccountCas(BigDecimal balance) {
        this.balance = new AtomicReference<>(balance);
    }

    //查看余额
    @Override
    public BigDecimal getBalance() {
        return balance.get();
    }

    //取款
    @Override
    public void withdraw(BigDecimal amount) {
        while (true) {
            BigDecimal prev = balance.get();
            BigDecimal next = prev.subtract(amount);
            if (balance.compareAndSet(prev, next)){
                break;
            }
        }
    }
}

注意使用步骤:

  • 使用AtomicReference类型定义变量,把变量的类型作为泛型
  • 定义构造器,给AtomicReference类型赋值,new一个AtomicReference,将变量的数据作为参数传入
  • 后续操作变量,就可以使用CAS方法。

2、ABA 问题

使用原子引用时,可能会有ABA问题:

问题描述:

  • 一个线程想要把A改成C,中途有线程把A修改成了B,又修改回了A。
  • 之前的线程接着执行,发现此时的值依然是A,就成功改成了C。

发生的原因:

  • 因为compareAndSet方法的逻辑是,只要当前值等于第一个参数,就能执行成功

  • 可能一个线程对引用做了多次修改,但引用的值最后又修改回去了

产生的影响:

  • 一般不会造成问题,但是存在隐患。
  • 合理的做法是,检测到其他线程对变量做了修改,就放弃这次操作,重新获取当前值,然后尝试CAS。

如何解决:使用AtomicStampedReference类,引入版本号机制。

3、AtomicStampedReference

AtomicStampedReference可以给原子引用加上版本号,可以知道引用变量被修改了几次。

做法是:

  1. 定义AtomicStampedReference类型,把引用类型作为泛型。后面指定引用变量的初始值、初始版本号。
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);

     2.每个修改操作,同时检查引用和版本号,操作成功就对版本号+1

while (true) {
    //获取当前引用值
    String prev = ref.getReference();
    //获取当前版本号
    int stamp = ref.getStamp();
    if (ref.compareAndSet(prev, "C", stamp, stamp + 1)){
        break;
    }
}
  • 提前获取引用、版本号
  • CAS时,只有当前引用、版本号都一样,才能成功执行

4、AtomicMarkableReference

有时候只需要关注,引用变量是否被修改过,而不关心被修改了几次,就可以使用AtomicMarkableReference。

AtomicMarkableReference使用boolean变量来表示,是否发生过修改。这样用起来比较方便

做法:

  1. 定义AtomicMarkableReference类型,把引用类型作为泛型。第二个参数代表一个标记
    AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);
  2. CAS操作时,比如设置成只有标记为true才能执行,然后把标记置为false

GarbageBag prev = ref.getReference();
ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);

6、原子数组

有这些:

  • AtomicIntegerArray(数组内部是Integer类型)
  • AtomicLongArray(数组内部是Long类型)
  • AtomicReferenceArray(数组内部是引用类型)

用法:用AtomicIntegerArray类型替换int[]

new AtomicIntegerArray(10);

7、字段更新器

可以实现原子性修改对象的某个属性值。注意,要修改的字段必须加上volatile,否则会出现异常

  • AtomicReferenceFieldUpdater(字段类型是引用)
  • AtomicIntegerFieldUpdater(字段类型是Integer)
  • AtomicLongFieldUpdater(字段类型是Long)

用法:

public class Dog {
    private volatile int field;

    public static void main(String[] args) {
        Dog dog = new Dog();
        AtomicIntegerFieldUpdater fieldUpdater = AtomicIntegerFieldUpdater.newUpdater(Dog.class, "field");
        fieldUpdater.compareAndSet(dog, 0, 10);
    }

}

注意:

  • 通过AtomicIntegerFieldUpdater类的静态方法newUpdater创建字段更新器对象,传入类的Class文件、要修改的字段名称

9、Unsafe

Unsafe 对象提供了非常底层的操作内存、线程的方法。Unsafe的含义是,比较底层,比较危险,注意:但并不是说这个对象方法是线程不安全的。

Unsafe 对象不能直接new出来,因为它是单例的,只能通过反射获得。

获得Unsafe:

public class UnsafeAccessor {
    static Unsafe unsafe;
    static {
        try { 
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new Error(e);
        }
    }
    static Unsafe getUnsafe() {
        return unsafe;
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值