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

9次阅读

共计 2442 个字符,预计需要花费 7 分钟才能阅读完成。

线程安全之原子操作

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 能自增多少次。以此来对比这三者的性能。

正文完
 0