无锁机制下的原子性操作

Posted huanStephen

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了无锁机制下的原子性操作相关的知识,希望对你有一定的参考价值。

  通常使用volatile关键字修饰字段可以实现多个线程的可见性和读写的原子性,但是对于字段的复杂性操作就需要使用synchronize关键字来进行,例如:

public class Counter {

    private volatile int count = 0;
    
    public synchronized int getAndIncr() {
        return this.count ++;
    }
    
}

这里可以看到,对于字段的简单设置和获取,volatile可以应付,但是我们想每次获取后自增加1,这样的操作就只能交给synchronize来做,这样做虽然可以但是性能较低,所以我们这里介绍一种新的无锁操作CAS。

 

什么是CAS(Compare And Swap)

  比较并且交换内存地址的数据,由CPU在指令级别上进行操作的原子性。

CAS包含三个参数:1、变量所在的地址A   2、变量应该的值V1   3、我们需要改的值V2。

如果说变量地址A上的值为V1,就用V2进行复制;如果值不是V1,则什么都不操作,返回V1的值。

自旋操作:在一个死循环里不停的进行CAS的操作,直到成功为止。

public class AtomicityTest implements Runnable {
    private int i = 0;
    public int getValue() { return i; }
    private synchronized void evenIncrement() { i++; i++; }
    public void run() {
        while(true)
            evenIncrement();
    }
    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        AtomicityTest at = new AtomicityTest();
        exec.execute(at);
        while(true) {
            int val = at.getValue();
            if(val % 2 != 0) {
                System.out.println(val);
                System.exit(0);
            }
        }
    }
}

 

输出:

 1

 

上面这段代码中,主线程循环获取线程AtomicityTest中递增的值,但是递增每次增加2(在java里i++这样的操作也不是原子性操作,为了方便演示效果自增2),主线程循环获取并判断,如果是奇数则退出循环。

最终结果是输出了奇数1,说明在evenIncrement方法中当自增1次之后被主线程获取了值,才会导致这样的情况发生。这里只有evenIncrement方法是synchronized,而getValue方法不是所以导致主程序退出。按照原先的方法只需在getValue方法上也加上synchronized即可解决。但现在有了原子操作类,可以通过以下方法得以解决:

public class AtomicIntegerTest implements Runnable {
    private AtomicInteger i = new AtomicInteger(0);
    public int getValue() { return i.get(); }
    private void evenIncrement() { i.addAndGet(2); }
    public void run() {
        while(true)
            evenIncrement();
    }
    public static void main(String[] args) {
        new Timer().schedule(new TimerTask() {
            public void run() {
                System.err.println("Aborting");
                System.exit(0);
            }
        }, 5000);
        ExecutorService exec = Executors.newCachedThreadPool();
        AtomicIntegerTest ait = new AtomicIntegerTest();
        exec.execute(ait);
        while(true) {
            int val = ait.getValue();
            if(val % 2 != 0) {
                System.out.println(val);
                System.exit(0);
            }
        }
    }
}

 

这里使用原子操作类AtomicInteger,然后在evenIncrement方法里使用addAndGet方法实现自增2,可以看出该方法也没有使用synchronized关键字,因为方法里只有原子自增,所以不需要synchronized关键字。

由于主线程是死循环,我这里使用Timer类在5秒后退出。

 

CAS实现原子操作的三大问题

  1. 当线程想把地址A的值V1改为V3时,当线程取到V1的值后再进行比较时,地址A的值从V1->V2->V1进行了改变,虽然V3可以正常赋值,但是比较的V1值已经不是取出来的V1了。
  2. 循环时间很长的话,CPU的负荷较大。
  3. 对一个变量进行操作可以,同时操作多个共享的变量有些麻烦。

 

CAS线程安全

  通过硬件层面的阻塞实现原子操作的安全性。

 

常用的原子操作类

  • 更新基本类型类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
  • 更新数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
  • 更新引用类:AtomicReference,AtomicReferenceFieldUpdater,AtomicMarkableReference
  • 原子更新字段类:AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicStampedReference
public class AtomicArray {

    static int[] value = new int[]{1, 2};
    static AtomicIntegerArray aia = new AtomicIntegerArray(value);
    
    public static void main(String[] args) {
        aia.set(0, 3);
        System.out.println(aia.get(0));
        System.out.println(value[0]);
    }
}

输出:

3
1

可以看出AtomicIntegerArray对象将int数组复制了一份,他的改变并没有影响到原有数组。

public class AtomicRef {

    static class User {
        private String name;
        private int age;
        
        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public int getAge() {
            return age;
        }
        public void setAge(int age) {
            this.age = age;
        }
    }
    
    static AtomicReference<User> userAtomicRef = new AtomicReference<AtomicRef.User>();
    
    public static void main(String[] args) {
        User user = new User("Mark", 25);
        userAtomicRef.set(user);
        User updUser = new User("Mark", 28);
        userAtomicRef.compareAndSet(user, updUser);
        System.out.println(userAtomicRef.get().getName());
        System.out.println(userAtomicRef.get().getAge());
    }
    
}

输出:

Mark
28

这里会首先进行比较,然后更新不一样的字段。

这里对CAS三大问题的第一个问题进行解决:

AtomicMarkableReference这个类会在数据被修改之后,标记位会由true变为false,判断这个标记位就可得知。

AtomicStampedReference这个类会在数据上添加时间戳,如果有数据修改时间戳会变掉,从而判断数据是否被修改。

 


 

本文索引关键字:

CAS:http://www.cnblogs.com/huanStephen/p/8213678.html#CAS

欢迎大家索引!

以上是关于无锁机制下的原子性操作的主要内容,如果未能解决你的问题,请参考以下文章

JMMsynchronized原理可见性有序性happens-beforeCAS(无锁并发)原子性 synchronized的优化

JMMsynchronized原理可见性有序性happens-beforeCAS(无锁并发)原子性 synchronized的优化

linux多线程--双buffer“无锁”设计

Linux下的原子操作

操作系统中啥是“原子操作”

C++11多线程 原子操作概念及范例