如何保证在Java多线程中的原子性操作?

       在单线程的模式下,我们针对某个变量的修改是不会产生数据的脏读和脏写的,因为它只有一个操作来对变量进行读写操作,但是在多线程模式下就不一样了,如果多个线程对一个变量进行修改操作,那么到底哪一个线程修改的值才是最后的值呢?同时是不是我们需要的值呢?那么我们如何来保证多个线程修改变量值的时候保证变量的原子操作?简单说明下原子操作就是一个步骤要么操作全部成功,要么失败。我们看下i++这个操作,对于不了解这个i++操作生成的二进制文件的具体同学来说,可能会认为它是只有一个操作步骤的运算操作,但是实际情况肯定不是的,它首先会去获取字段i,然后在获取i的值,然后在进行加1操作,最后对i进行赋值,这里就进行了4个步骤,如果在第2个步骤的时候另外一个线程也来进行了i++操作,那么可想而知最终得到的结果可能就不是自己想要的了。那么我们如何保证i++的原子操作呢?这个时候肯定大家也能想到用同步啊,synchronized关键字和Lock锁都可以进行同步的控制,当然这也是正确的,但是我们今天所说的多线程原子操作是通过jdk提供的JUC包实现的原子操作,如AtomicInteger,AtomicReference等原子操作类,在JUC包中的原子类操作基本都实现了CAS(compare and swap)操作,当对某个值进行改变时CAS会先对当前值进行比较,如果正确才进行值的交换即更新值,CAS操作是在硬件资源上直接进行的操作,是由处理器提供的内存操作指令,由其保证原子性。通过查看相关的JUC包,可以看到很多Unsafe类方法的使用,这个类提供了很多native方法的使用,因为是不安全的类,所以自己想使用的话,必须自己去通过反射进行获取来进行操作。

        虽然说JUC包提供了很多原子的CAS操作,但是它一次只能针对一个变量进行CAS操作,不能进行多个变量的操作,同时因为要不停的进行CAS的判断和交换值操作,所以线程会一直处于高频的运行中,如果其中一个线程处理时间过长,那么就会带来CPU的高性能消耗,同时CAS操作还会出现一种叫做ABA的问题,即当前值是A,将它换做B,然后又由B换做A,那么就无法判断A是否在这期间发生过变化,因为最终值没有变化,如果在AB的时候有其他线程来进行了操作A操作,而该线程在休眠时间内又从B变回了A,而此时A的指向已经发生改变,那这种改变带来的结果是其他线程对A的操作已经发生改变,得到的结果也可能发生变化,那么怎么来解决这个ABA的问题呢?我们可以采用类似版本号的控制机制来实现避免ABA这种问题,在JUC包中提供了AtomicStampedReference类,它提供了一个版本控制的判断,我们使用以下代码运行后可以看到不同的结果

public class AtomicStampedReferenceTest {
 
    public static void main(String [] args){
        AtomicStampedReference reference = new AtomicStampedReference(0,0);
        new Thread(() ->{
            reference.compareAndSet(0,1,0,1);
            System.out.println(reference.getReference() + "--->" + reference.getStamp());
        }).start();
 
        new Thread(() ->{
            reference.compareAndSet(0,1,1,2);
            System.out.println(reference.getReference() + "--->" + reference.getStamp());
        }).start();
 
        new Thread(() ->{
            reference.compareAndSet(1,2,1,2);
            System.out.println(reference.getReference() + "--->" + reference.getStamp());
        }).start();
    }

 

Guess you like

Origin www.iteye.com/blog/357029540-2443860