并发编程四:Atomic原子类和Unsafe魔术类,ABA问题以及解决方案

1. Atomic原子类

1.1 原子操作

  1. 原子操作:原子即“不能被进一步分割的最小粒子”,原子操作(atomic operation)即”不可被中断的一个或一系列操作”
术语名称英文解释
缓存行Cache line缓存的最小操作单位
比较并交换Compare and swapCAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。底层通过cpu指令CMPXCHG来实现,该操作是一个原子操作。
CPU流水线CPU pipelineCPU流水线的工作方式就象工业生产上的装配流水线,在CPU中由56个不同功能的电路单元组成一条指令处理流水线,然后将一条X86指令分成56步后再由这些电路单元分别执行,这样就能实现在一个CPU时钟周期完成一条指令,因此提高CPU的运算速度。
内存顺序冲突Memory order violation内存顺序冲突一般是由假共享引起,假共享是指多个CPU同时修改同一个缓存行的不同部分而引起其中一个CPU的操作无效,当出现这个内存顺序冲突时,CPU必须清空流水线。(MESI造成的)

1.2 CPU原子操作的实现方式

  1. 处理器自动保证基本内存操作的原子性,如对同一个缓存行里进行16/32/64位的操作是原子的。复杂的内存操作处理器不能自动保证其原子性,比如跨总线宽度,跨多个缓存行,跨页表的访问。可以通过过MESI协议实现缓存锁、和总线枷锁

1.3 Atomic

  1. 在Atomic包里一共有12个类,四种原子更新方式,原子更新基本类型,原子更新数组,原子更新引用,原子更新字段,** Atomic包里的类基本都是使用Unsafe**实现的包装类。
  • 基本类:AtomicInteger、AtomicLong、AtomicBoolean;
  • 引用类型:AtomicReference、AtomicReference的ABA实例、AtomicStampedRerence、AtomicMarkableReference;
  • 数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
  • 属性原子修改器(Updater):AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
  1. Atomic包里面的类基本都是使用Unsafe中的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);

1.4 ABA问题

  1. 举个例子:

1、小李子是阿里巴巴的财务官。小李子偷偷拿了公司的资金去炒股。
2、小李子赚了1个亿。
3、小李子在他人没有发现的情况下,将公司的钱放了回去。
4、小李子白嫖了1个亿。

  1. ABA问题示例代码
/**
 * @Author: zkfgyzy@163.com
 * @Date: 2022/4/5 1:10
 */
public class AtomicAbaProblemTest {


    static AtomicInteger atomicInteger=new AtomicInteger(1);

    public static void main(String[] args) {

        Thread main= new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("操作线程"+Thread.currentThread().getName()+"----修改前操作数值"+atomicInteger.get());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                boolean isCasSuccess=atomicInteger.compareAndSet(1,2);

                if(isCasSuccess){
                    System.out.println("操作线程"+Thread.currentThread().getName()+"----cas修改后操作数值"+atomicInteger.get());
                }else{
                    System.out.println("CAS操作失败");
                }
            }
        },"主线程");


        Thread other = new Thread(new Runnable() {
            @Override
            public void run() {

                //进行自+
                atomicInteger.incrementAndGet();
                System.out.println("操作线程"+Thread.currentThread().getName()+"----increase后值是什么"+atomicInteger.get());
                boolean isCasSuccess=atomicInteger.compareAndSet(1,2);

                //自--
                atomicInteger.decrementAndGet();
                System.out.println("操作线程"+Thread.currentThread().getName()+"----cas修改后操作数值"+atomicInteger.get());
            }
        },"干扰线程");

        main.start();
        other.start();
    }
}
  1. ABA问题解决方案
  • 正如小李子偷挪用钱的问题,可以在钱的流水上添加版本号,如果版本号没有发生改变,说明没有人挪用钱,如果发生了变化,说明有人挪用了钱。校验版本号是否一致。
  • ABA问题的本质就是过程没办法跟踪。
  • 采用AtomicStampedReference增加版本号,处理ABA问题
/**
 * @Author: zkfgyzy@163.com
 * @Date: 2022/4/5 2:03
 */
public class AtomicStampedRerenceTest {

    private static AtomicStampedReference<Integer> atomicStampedReference=new AtomicStampedReference<>(1,0);

    public static void main(String[] args) {

        Thread main= new Thread(new Runnable() {
            @Override
            public void run() {
                int stamp = atomicStampedReference.getStamp();
                System.out.println("操作线程"+Thread.currentThread().getName()+"stamp="+stamp+",初始值a="+atomicStampedReference.getReference());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                boolean isCasSuccess=atomicStampedReference.compareAndSet(1,2,stamp,stamp+1);

                if(isCasSuccess){
                    System.out.println("操作线程"+Thread.currentThread().getName()+"stamp="+stamp+",CAS操作后的结果"+isCasSuccess);
                }else{
                    System.out.println("CAS操作失败");
                }
            }
        },"主线程");


        Thread other = new Thread(new Runnable() {
            @Override
            public void run() {

                //进行自+
                int stamp = atomicStampedReference.getStamp();

                atomicStampedReference.compareAndSet(1,2,stamp,stamp+1);
                System.out.println("操作线程"+Thread.currentThread().getName()+"stamp="+stamp+",[increment],值"+atomicStampedReference.getReference());
                stamp = atomicStampedReference.getStamp();

                //自--
                atomicStampedReference.compareAndSet(2,1,stamp,stamp+1);
                System.out.println("操作线程"+Thread.currentThread().getName()+"stamp="+stamp+",[increment],值"+atomicStampedReference.getReference());
            }
        },"干扰线程");

        main.start();
        other.start();
    }
}

1.5 使用Atomic修改数组

  • 注意:Atomic会clone数组,打印的值是不同的
/**
 * @Author: zkfgyzy@163.com
 * @Date: 2022/4/5 3:06
 */
public class AtomicIntegerArrayTest {



    static int[] value=new int[]{1,2};
    static AtomicIntegerArray aiArray=new AtomicIntegerArray(value);


    public static void main(String[] args) {
        aiArray.getAndSet(0,3);
        System.out.println(aiArray.get(0));
        System.out.println(value[0]);
    }
}

1.6 使用Autmic原子类修改对象中的某个字段

/**
 * 
 * 使用Atomic原子操作修改对象的某个属性值
 * @Author: zkfgyzy@163.com
 * @Date: 2022/4/5 10:49
 */
public class AtomicIntegerFieldUpdateTest {


    static AtomicIntegerFieldUpdater aifu=AtomicIntegerFieldUpdater.newUpdater(Student.class,"old");

    public static void main(String[] args) {

        Student zkf = new Student("zkf", 18);
        System.out.println(aifu.getAndIncrement(zkf));
        System.out.println(aifu.get(zkf));

    }

    static class Student{
        private String name;
        public volatile int old;

        public Student(String name, int old) {
            this.name = name;
            this.old = old;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getOld() {
            return old;
        }

        public void setOld(int old) {
            this.old = old;
        }
    }
}

2. Unsafe魔法类

2.1 Unsafe类的获取方法

  1. 第一种,把类A所在的jar包路径最加到Bootstrap路径中,使用A被引导类加载器加载

1、从getUnsafe方法的使用限制条件出发,通过Java命令行命令-Xbootclasspath/a把
调用Unsafe相关方法的类A所在jar包路径追加到默认的bootstrap路径中,使得A被
引导类加载器加载,从而通过Unsafe.getUnsafe方法安全的获取Unsafe实例。

java -Xbootclasspath/a:${path} // 其中path为调用Unsafe相关方法的类所在jar包路径
  1. 通过反射获取单例对象Unsafe
/**
 *
 * @Author: zkfgyzy@163.com
 * @Date: 2022/3/10 0:15
 */
public class UnsafeInstance {


    public static Unsafe reflectGetUnsafe(){
        try{

            Field field =Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
        }catch (Exception ex){
            ex.printStackTrace();
        }
        return null;
    }
}

2.2 使用Unsafe魔术类修改对象中的属性值

  1. 步骤

1、获取Unsafe类
2、获取要修改对象偏移量
3、调用Unsafe类的compareAndSwapInt方法进行修改属性值,compareAndSwapInt参数为:要修改的对象,要修改字段的偏移量,老值,新值

/**
 * 模拟Atomic实现调用Unsafe进行原子操作修改对象的某个属性值
 * @Author: zkfgyzy@163.com
 * @Date: 2022/4/5 10:48
 */
public class AtomicStudentAgeUpdater {



    private String name;
    private volatile int age;


    private static final Unsafe unsafe= UnsafeInstance.reflectGetUnsafe();

    private static final long valueOffset;

    static {


        try {
           valueOffset= unsafe.objectFieldOffset(AtomicStudentAgeUpdater.class.getDeclaredField("age"));
               System.out.println("valueOffset---------"+valueOffset);
        } catch (Exception ex) { throw new Error(ex); }
    }


    public void compareAndSwapAge(int old,int target){
        //要修改的对象,偏移量,老值,目标新值
        unsafe.compareAndSwapInt(this,valueOffset,old,target);
    }


    public AtomicStudentAgeUpdater(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public static void main(String[] args) {
        AtomicStudentAgeUpdater updater=new AtomicStudentAgeUpdater("zkf",18);
        updater.compareAndSwapAge(18,19);
        System.out.println("真实年龄---------"+updater.getAge());
    }
}

2.3 对象中的偏移量理解

  1. 一个实例对象包含3部分:对象头,实例数据,对齐填充位。我们可以根据对象的头部地址根据偏移量获取对象某个字段的地址空间位置。

对象偏移量计算

2.4 Unsafe魔术类的功能

Unsafe魔术类

  1. Synchronized只能实现在同一个方法里面加锁,不能跨方法加锁。如果实现跨方法加锁可以通过Unsafe的对象锁进行跨方法加锁。类似事务的跨方法事务。
/**
 * @Author: zkfgyzy@163.com
 * @Date: 2022/4/5 11:33
 */
public class ObjectMonitorTest {


    static Object object=new Object();
    public void method1(){

    }
    public void method2(){

    }

    public static void main(String[] args) {
        Unsafe unsafe = UnsafeInstance.reflectGetUnsafe();
        unsafe.monitorEnter(object);

        //中间逻辑


        unsafe.monitorExit(object);
    }
}

  1. 线程调度,包括线程挂起、恢复、锁机制等方法。
//取消阻塞线程 
public native void unpark(Object thread); 
//阻塞线程 
public native void park(boolean isAbsolute, long time); 
//获得对象锁(可重入锁) 
@Deprecated 
public native void monitorEnter(Object o); 
//释放对象锁 
@Deprecated 
public native void monitorExit(Object o); 
//尝试获取对象锁 
@Deprecated 
public native boolean tryMonitorEnter(Object o);

方法park、unpark即可实现线程的挂起与恢复,将一个线程进行挂起是通过park方法实现的,调用park方法后,线程将一直阻塞直到超时或者中断等条件出现;unpark可以终止一个挂起的线程,使其恢复正常。

典型应用

Java锁和同步器框架的核心类AbstractQueuedSynchronizer,就是通过调用LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的,而LockSupport的park、unpark方法实际是调用Unsafe的park、unpark方式来实现。

  1. 内存屏障
  • 在Java 8中引入,用于定义内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序
//内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏 障后,屏障后的load操作不能被重排序到屏障前 
public native void loadFence(); 
//内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后, 屏障后的store操作不能被重排序到屏障前 
public native void storeFence(); 
//内存屏障,禁止load、store操作重排序 
public native void fullFence();
  • 在Java 8中引入了一种锁的新机制——StampedLock,它可以看成是读写锁的一个改进版本。StampedLock提供了一种乐观读锁的实现,这种乐观读锁类似于无锁的操作,完全不会阻塞写线程获取写锁,从而缓解读多写少时写线程“饥饿”现象。由于StampedLock提供的乐观读锁不阻塞写线程获取读锁,当线程共享变量从主内存load到线程工作内存时,会存在数据不一致问题,所以当使用StampedLock的乐观读锁时,需要遵从如下图用例中使用的模式来确保数据的一致性。

StampedLock

  • 如上图用例所示计算坐标点Point对象,包含点移动方法move及计算此点到原点的距离的方法distanceFromOrigin。在方法distanceFromOrigin中,首先,通过tryOptimisticRead方法获取乐观读标记;然后从主内存中加载点的坐标值 (x,y);而后通过StampedLock的validate方法校验锁状态,判断坐标点(x,y)从主内存加载到线程工作内存过程中,主内存的值是否已被其他线程通过move方法修改,如果validate返回值为true,证明(x, y)的值未被修改,可参与后续计算;否则,需加悲观读锁,再次从主内存加载(x,y)的最新值,然后再进行距离计算。其中,校验锁状态这步操作至关重要,需要判断锁状态是否发生改变,从而判断之前copy到线程工作内存中的值是否与主内存的值存在不一致。
  • 下图为StampedLock.validate方法的源码实现,通过锁标记与相关常量进行位运算、比较来校验锁状态,在校验逻辑之前,会通过Unsafe的loadFence方法加入一个load内存屏障,目的是避免上图用例中步骤②和StampedLock.validate中锁状态校验运算发生重排序导致锁状态校验不准确的问题。
    public boolean validate(long var1) {
        //增加内存屏障
        U.loadFence();
        return (var1 & -128L) == (this.state & -128L);
    }
  1. Unsafe类操作具有很大危险性,建议只操作CAS、内存屏障、线程调度三块内容

2.6 park和unpark的使用

/**
 * @Author: zkfgyzy@163.com
 * @Date: 2022/4/5 11:56
 */
public class ThreadParkTest {


    public static void main(String[] args) {


       Thread t= new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread - is runing~~~~");
                LockSupport.park();
                System.out.println("thread is over~~~~~");

            }
        });

        t.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        LockSupport.unpark(t);
        System.out.println("main thread is over");



      /*  LockSupport.unpark(Thread.currentThread());

        System.out.println("im runing step 1");
        LockSupport.park();
        System.out.println( "main thread is over");*/
    }
}
  • park和unpark方法可以颠倒使用,类似进地铁和出地铁的票据问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值