17原子引用(乐观锁)

Posted zxhbk

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了17原子引用(乐观锁)相关的知识,希望对你有一定的参考价值。

什么是原子引用

解决ABA 问题,引入原子引用! 对应的思想:乐观锁!

带版本号 的原子操作!每次对值进行修改时,都会对比版本号,判断这个值是否被修改过。

如果没有修改则对其进行修改,如果修改过了,那么就会导致修改不成功。

 

判断值是否有修改过

查看官方文档中,原子引用的类

技术图片

 可以设置带版本的原子操作,一般常用AtomicStampedReference

技术图片

具体代码

//AtomicStampedReference:和乐观锁的性质相同,可以记录每一次的修改,并且通过版本判断是否被修改过
import java.io.UnsupportedEncodingException;
import java.text.ParseException;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

public class MyTest {
    public static void main(String[] args) throws UnsupportedEncodingException, ParseException, ClassNotFoundException, ExecutionException, InterruptedException {
        // 创建原子引用对象,设定初始值为1, 版本号为1
        // 在真实业务中,一般使用具体的对象比如:User
        AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(1, 1);

        // 捣乱线程
        new Thread(()->{
            int stamp = reference.getStamp();   // 获取版本号
            System.out.println(Thread.currentThread().getName() + "=> 获取到的版本号:" + stamp);
            try {
                TimeUnit.SECONDS.sleep(2);  // 睡2秒,保证两个线程的版本号都输出一下,并且当前线程先执行
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 判断原子引用的版本号,和当前版本号是否相同,如果相同修改值,并对版本号+1,修改成功返回true
            boolean result1 = reference.compareAndSet(1, 2, reference.getStamp(), reference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "=> 将值从1修改为2,修改结果:" + result1 + ",当前值:" + reference.getReference() + ",当前版本号:" + reference.getStamp());

            // 判断原子引用的版本号,和当前版本号是否相同,如果相同修改值,并对版本号+1,修改成功返回true
            boolean result2 = reference.compareAndSet(2, 1, reference.getStamp(), reference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "=> 将值从2修改为1,修改结果:" + result2 + ",当前值:" + reference.getReference() + ",当前版本号:" + reference.getStamp());
        }, "B").start();

        // 正常修改线程
        new Thread(()->{
            int stamp = reference.getStamp();   // 版本号
            System.out.println(Thread.currentThread().getName() + "=> 获取到的版本号:" + stamp);
            try {
                TimeUnit.SECONDS.sleep(3);  // 这里睡3秒,将获取到的版本号输出,并且保证捣乱的线程先执行
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 判断原子引用的版本号,和当前版本号是否相同,如果相同修改值,并对版本号+1,修改成功返回true
            boolean result = reference.compareAndSet(1, 3, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName() + "=> 将值从1修改为3,修改结果:" + result + ",当前值:" + reference.getReference() + ",当前版本号:" + reference.getStamp());
        }, "A").start();

    }
}

技术图片

注意

Integer 使用了对象缓存机制,默认范围是 -128 ~ 127 ,推荐使用静态工厂方法 valueOf 获取对象实 例,而不是 new,因为 valueOf 使用缓存,而 new 一定会创建新的对象分配新的内存空间;技术图片

// 下面是AtomicStampedReference这个类的源码
private static class Pair<T> {
    final T reference;    
    final int stamp;
    private Pair(T reference, int stamp) {
        this.reference = reference;
        this.stamp = stamp;
    }
    static <T> Pair<T> of(T reference, int stamp) {
        return new Pair<T>(reference, stamp);
    }
}
public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
    Pair<V> current = pair;
    return
        expectedReference == current.reference &&    // 这两个都是对象,所以==比较的是地址,如果地址不一样直接返回false
        expectedStamp == current.stamp &&
        ((newReference == current.reference &&
          newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));
}

再次测试

  • 将值设置为2020,查看是否可以修改
import java.io.UnsupportedEncodingException;
import java.text.ParseException;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

public class MyTest {
    public static void main(String[] args) throws UnsupportedEncodingException, ParseException, ClassNotFoundException, ExecutionException, InterruptedException {
        // 这里使用Integer对象,如果值是-128~127之间,那么比较的时候比较的是值
        // 如果不在这个区间,那么就会在堆内存中创建对象,那么比较的时候比的就是地址了
        AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(2020, 1);

        // 捣乱线程
        new Thread(()->{
            int stamp = reference.getStamp();   // 获取版本号
            System.out.println(Thread.currentThread().getName() + "=> 获取到的版本号:" + stamp);
            try {
                TimeUnit.SECONDS.sleep(2);  // 睡2秒,保证两个线程的版本号都输出一下,并且当前线程先执行
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 这里将值从2020改为2021,但是传入的2020实际上是创建了一个对象new Integer(2020),reference这个对象中的2020在堆内存中是另一个对象
            // 虽然值相同,但是地址不同,所以修改不成功
            boolean result1 = reference.compareAndSet(2020, 2021, reference.getStamp(), reference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "=> 将值从2020修改为2021,修改结果:" + result1 + ",当前值:" + reference.getReference() + ",当前版本号:" + reference.getStamp());

            boolean result2 = reference.compareAndSet(2021, 2020, reference.getStamp(), reference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "=> 将值从2021修改为2020,修改结果:" + result2 + ",当前值:" + reference.getReference() + ",当前版本号:" + reference.getStamp());
        }, "B").start();

        // 正常修改线程
        new Thread(()->{
            int stamp = reference.getStamp();   // 版本号
            System.out.println(Thread.currentThread().getName() + "=> 获取到的版本号:" + stamp);
            try {
                TimeUnit.SECONDS.sleep(3);  // 这里睡3秒,将获取到的版本号输出,并且保证捣乱的线程先执行
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            boolean result = reference.compareAndSet(2020, 2022, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName() + "=> 将值从2020修改为2022,修改结果:" + result + ",当前值:" + reference.getReference() + ",当前版本号:" + reference.getStamp());
        }, "A").start();

    }
}

技术图片

 都修改失败,所以在使用时需要注意!

 

以上是关于17原子引用(乐观锁)的主要内容,如果未能解决你的问题,请参考以下文章

第五章 - 乐观锁 无锁方案

JUC学习之无锁---乐观锁(非阻塞)

JUC学习之无锁---乐观锁(非阻塞)

CAS 与原子操作

CAS乐观锁使用AtomicStampedReference版本号控制手动实现原子计数

CAS乐观锁使用AtomicStampedReference版本号控制手动实现原子计数