Java并发2线程安全之原子操作

线程安全之原子操作

1.首先引入2个关键字:竞态条件、临界区

public class TestCase {

    public int i = 0;

    public void incr(){//incr方法内部,就是临界区
        i++;//实际开发中,这里可能是某个计数器,某个积分计算等业务代码,而不是简单的i++
    }
}

  一般来说,多线程在同一时刻访问某一共享资源,在对共享资源做写操作时,需要对执行顺序有所要求。例如上述代码中的incr方法内部,就是临界区,多线程并发执行i++,会对执行结果产生影响。竞态条件,是在临界区内的特殊条件。换句话说,单线程环境下,不存在竞态条件。多线程执行incr方法中的i++代码,就可能产生竞态条件。
  归纳一下:线程不安全的代码,在临界区里。临界区里的代码,因为产生了竞态条件,所以线程不安全。如果破坏了竞态条件,那么线程安全。如果没有竞态条件,也根本写不出线程不安全的代码。再举个例子,如果一个对象是栈封闭的,也就是其引用是在线程的栈里,当线程运行结束后,引用也就没了,这样的对象也是线程安全的。还有一种对象也是线程安全的,那就是不可变对象,例如某个对象是final类型的,那不管什么线程,都不能够对该变量进行写操作,只能读,这样的对象也是线程安全的。

2.volatile不能保证原子性

  我们知道,volatile能够保证可见性,让变量的更改能立即被其它线程看见,但是,假设有3个线程同时进入临界区:

    public void incr(){
        //3个线程,同时进入这里,各自读取i都为0,此时A线程先进行加1操作,然后线程切换,B线程也加1,再切换,C线程也加1。
        //接下来,A线程进行写操作,令i=1。此时i的值对B、C线程可见(B、C看到i=1),但是B线程接下来并不会再加1了,而是进行写操作,令i=1,同理C线程也是写操作,i=1。
        //最终结果,i=1。程序创建3个线程,各执行1次自增操作,得到的结果却只自增了一次。这是因为volatile不能保证原子性,对一个变量修饰volatile,不代表对该变量的操作是原子的。
        i++;
    }

3.CAS操作

  CAS是硬件提供的同步原语,Compare And Swap,比较且交换,由处理器保证内存操作的原子性。CAS操作需要输入2个数值,一个旧值A,一个新值B。在赋值前先比较旧值是否和A相等,如果相等,就赋新的值B,如果不等就操作失败。这其中的比较和交换2个操作,是原子的。下面来一个CAS操作的示例。

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class TestCAS {

    static Unsafe unsafe;

    static {
        try {
            //前3行通过反射拿到Unsafe对象
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
            //获取value属性的偏移量,也就是value属性的内存地址
            valueOffset = unsafe.objectFieldOffset(TestCAS.class.getDeclaredField("value"));
        }catch (Exception e) {
            e.printStackTrace();
        }
    }

    public int value = 0;//CAS自增

    public int value2 = 0;//普通自增

    public static long valueOffset;//value在内存中的偏移量

    //每一次自增操作,都是一次循环CAS直到CAS成功。
    public void incr(){
        int temp;
        do{
            //通过value的偏移量拿到value的值,初始值是0
            temp = unsafe.getInt(this, valueOffset);
        }while (!unsafe.compareAndSwapInt(this, valueOffset, temp, temp + 1));//如果CAS失败,就循环CAS
    }

    public void incr2(){
        value2++;
    }

    public static void main(String[] args){
        TestCAS testCAS = new TestCAS();
        Thread th1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0;i<10000;i++){
                    testCAS.incr();
                    testCAS.incr2();
                }
            }
        });
        Thread th2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0;i<10000;i++){
                    testCAS.incr();
                    testCAS.incr2();
                }
            }
        });
        th1.start();
        th2.start();
        try {
            th1.join();
            th2.join();
        }catch (Exception e){

        }
        System.out.println(testCAS.value);//20000
        System.out.println(testCAS.value2);//小于20000
    }
}

  上述代码就是一个CAS底层操作示例。已经算很底层了,用的是Unsafe对象。这段代码还是我用idea开发工具写的,Eclipse我甚至不知道怎么拿到Unsafe对象。

4.JDK提供的原子操作类

  第三节提到的Unsafe,一般不用。如果想做CAS操作,JDK为我们提供了java.util.concurrent.atomic包下面的原子操作类。下面来模拟一个场景,在2秒时间里,用synchronized锁能自增多少次,用AtomicLong能自增多少次,用LongAdder能自增多少次。以此来对比这三者的性能。