[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.处理器如何实现原子操作

  一般是通过锁总线,或者锁CPU缓存。

  锁总线:如果多个CPU同时读改写共享变量(例如i++),其中某个CPU通过一个LOCK信号,把总线资源据为己有,就能实现原子操作了,此时其他CPU无法操作总线,拥有总线资源的CPU独占共享内存。

  锁CPU缓存:锁总线开销太大了,等于多核CPU变成了单核CPU。一般来说,在同一时刻,我们只需要保证某个内存地址的操作是原子性的就可以了,锁总线让其他CPU在锁定期间,不能访问内存数据,这绝不允许。因此使用缓存锁,对总线锁进行优化。缓存锁是通过缓存一致性机制,保证对共享变量修改的原子性。因为实现了缓存一致性协议的多核处理器,不允许多个CPU同时修改它们缓存的相同内存区域。举个例子,CPU-A缓存了共享变量x,CPU-B也缓存了共享变量x,当CPU-A在回写x到主内存的时候,这期间CPU-B不能回写x到主内存,并且当CPU-A回写完毕,CPU-B缓存的共享变量x此时是无效的,如果接下来CPU-B仍然要操作共享变量x,会强制从主内存重新读数据到缓存。

  P.S.如果内存数据不能被缓存,或者操作的数据跨多个缓存行,此时就必须锁总线;有的处理器不支持锁缓存,那只好锁总线。

4.Java如何实现原子操作

  1.使用循环CAS操作 2.使用锁机制,锁保证了只有一个线程能进入同步代码块,操作共享内存数据,有意思的是,大部分锁也都用到了CAS操作,即进入同步代码块的时候,循环CAS获取锁,退出同步块的时候,循环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对象。

5.JDK提供的原子操作类

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

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;

public class TestCAS {

    public static Object lock = new Object();
    public long count = 0;

    public void testSync(){
        for(int i = 0 ; i < 3; i++){
            new Thread(() -> {
                long startTime = System.currentTimeMillis();
                while(System.currentTimeMillis() - startTime < 2000) {
                    synchronized (lock){
                        count++;
                    }
                }
                long spendTime = System.currentTimeMillis() - startTime;
                System.out.println("synchronized锁自增花销" + spendTime + "毫秒,自增结果" + count);
            }).start();
        }
    }

    public AtomicLong atomicLong = new AtomicLong(0);

    public void testAtomicLong(){
        for(int i = 0 ; i < 3; i++) {
            new Thread(() -> {
                long startTime = System.currentTimeMillis();
                while (System.currentTimeMillis() - startTime < 2000) {
                    atomicLong.incrementAndGet();
                }
                long spendTime = System.currentTimeMillis() - startTime;
                System.out.println("AtomicLong自增花销" + spendTime + "毫秒,自增结果" + atomicLong.get());
            }).start();
        }
    }

    public LongAdder longAdder = new LongAdder();

    public void testLongAdder(){
        for(int i = 0 ; i < 3; i++) {
            new Thread(() -> {
                long startTime = System.currentTimeMillis();
                while (System.currentTimeMillis() - startTime < 2000) {
                    longAdder.increment();
                }
                long spendTime = System.currentTimeMillis() - startTime;
                System.out.println("LongAdder自增花销" + spendTime + "毫秒,自增结果" + longAdder.sum());
            }).start();
        }
    }

    public static void main(String[] args){
        new TestCAS().testSync();
        new TestCAS().testAtomicLong();
        new TestCAS().testLongAdder();
    }

}

  结果是LongAdder > AtomicLong > synchronized,这是因为在高并发情况下,LongAdder将对单一变量的CAS操作,分散为对多个变量的CAS操作,让不同线程的CAS操作对象不同,降低了CAS失败的概率,在取值时,再进行求和,有点分布式计算的感觉。AtomicLong就是正常的对单一变量的CAS操作,全部线程操作同一个变量,如果修改失败就自旋,这种方式的劣势在于,如果操作比较耗时,且一直失败,就会一直占用CPU资源,而越是高并发,就越可能冲突,越发生冲突,就越占用CPU,这是恶性循环,而LongAdder采用分段的策略,在低并发的情况下,各个线程就执行对单一变量的CAS操作,这种情况和AtomicLong性能差不多,到了高并发的环境下,LongAdder会增加变量的分身,让一些线程对分身做CAS操作,通过增加分身的数量,减少CAS冲突的概率,到了计算总数时,再将所有分身进行求和运算,Doug Lea不愧是设计大师。synchronized关键字就等于一刀切了,管你能不能执行成功,都给我按失败的可能来,那就都乖乖排队,顺序自增。

6.ABA问题

  CAS操作的核心,是检查值有没有变化,那如果一直值原来是A,中间变成了B,再变成了A,那么用CAS检查会认为它没有变化,实际上是变化了。这就是ABA问题。解决这个问题的思路是用版本号,在变量值的前面加版本号,每次变量更新的同时,版本号加1,这样就是1A-2B-3A,这样就能检查到值的变化,避免ABA问题。

你可能感兴趣的:(原子性,java多线程)