Java并发编程系列:Java原子类

一、线程不安全

当多个线程访问统一资源时,如果没有做线程同步,可能造成线程不安全,导致数据出错。举例:

@Slf4j
public class ThreadUnsafe {

    // 用于计数的统计变量
    private static int count = 0;
    // 线程数量
    private static final int Thread_Count = 10;
    // 线程池
    private static ExecutorService executorService = Executors.newCachedThreadPool();
    // 初始化值和线程数一致
    private static CountDownLatch downLatch = new CountDownLatch(Thread_Count);

    public static void main(String[] args) throws Exception{
        for (int i = 0; i < Thread_Count; i++) {
            executorService.execute(() -> {
                for (int j = 0; j < 1000; j++) {  // 每个线程执行1000次++操作
                    count++;
                }
                // 一个线程执行完
                downLatch.countDown();
            });
        }
        // 等待所有线程执行完
        downLatch.await();
        log.info("count is {}", count);
    }
}

当多个线程对count变量计数,每个线程加1000次,10个线程理想状态下是加10000次,但实际情况并非如此。

上面的代码执行5次后,打印出来的count值分别为 7130,8290,9370,8790,8132。从测试的结果看出,count的值并不是我们认为的10000次,而都是小于10000次。

之所以出现上面的结果就是因为count++操作并非原子的。它其实被分成了三步:

tp1 = count;  //1
tp2 = tp1 + 1;  //2
count = tp2;  //3

所以 ,如果有两个线程m和n要执行count++操作。如果是理想状态下,m和n线程依次执行,m先执行完后,n再执行,即m1 -> m2 -> m3 -> n1 -> n2 -> n3,那么结果是没问题的。但是如果线程代码的执行顺序是m1 -> n1 -> m2 -> n2 -> m3 -> n3,那么很明显结果就会出错。

而上面的测试结果也正是由于没有做线程同步,导致的线程在执行count++时,乱序执行后count的数值就不对了。

二、原子操作

1、使用synchronized实现线程同步

对上面的代码做一些改造,对count++操作加入synchronized关键字修饰,实现线程同步,以保证每个线程在执行count++时,必须执行完成后,另一个线程才开始执行的。代码如下:

@Slf4j
public class ThreadUnsafe {

    // 用于计数的统计变量
    private static int count = 0;
    // 线程数量
    private static final int Thread_Count = 10;
    // 线程池
    private static ExecutorService executorService = Executors.newCachedThreadPool();
    // 初始化值和线程数一致
    private static CountDownLatch downLatch = new CountDownLatch(Thread_Count);

    public static void main(String[] args) throws Exception{
        for (int i = 0; i < Thread_Count; i++) {
            executorService.execute(() -> {
                for (int j = 0; j < 1000; j++) {  // 每个线程执行1000次++操作
                    synchronized (ThreadUnsafe.class) {
                        count++;
                    }
                }
                // 一个线程执行完
                downLatch.countDown();
            });
        }
        // 等待所有线程执行完
        downLatch.await();
        log.info("count is {}", count);
    }
}

将线程不安全的测试代码添加synchronized关键字进行线程同步,保证线程在执行count++操作时,是依次执行完后,后面的线程才开始执行的。synchronized关键字可以实现原子性和可见性。

将上面的代码执行5次后,打印出来的count值均为10000,已经是正确结果了。

三、原子类

在 JDK1.5 中新增了 java.util.concurrent(J.U.C) 包,它建立在 CAS 之上。而CAS 采用了乐观锁思路,是非阻塞算法的一种常实现,相对于 synchronized 这种阻塞算法,它的性能更好。

1、乐观锁与CAS

在JDK5之前,Java是靠synchronized关键字保证线程同步的,这会导致有锁,锁机制存在以下问题:

在多线程竞争下,加锁和释放锁会导致比较多的上下文切换和调度延时,引起性能问题;

一个线程持有锁后,会导致其他所有等待该锁的线程挂起;

如果一个优先级高的线程等待一个优先级低的线程释放锁会导致线程优先级倒置,引起风险;

独占锁采用的是悲观锁思路。synchronized就是一种独占锁,它会导致其他所有需要锁的线程挂起。而另一种更加有效的锁就是乐观锁,CAS就是一种乐观锁。乐观锁,严格来说并不是锁。它是通过原子性来保证数据的同步,比如说数据库的乐观锁,通过版本控制来实现,所以CAS不会保证线程同步,只是乐观地认为在数据更新期间没有其他线程参与。

CAS是一种无锁算法。无锁编程,即在不使用锁的情况下实现多线程间的同步,也就是在没有线程被阻塞挂起的情况下实现变量的同步。

CAS算法即是:Compare And Swap,比较并替换。

CAS算法存在着三个参数,内存值V,期望值A,以及需要更新的值B。当且仅当内存值V和期望值A相等的时候,才会将内存值修改为B,否则什么也不做,继续循环检查;

由于CAS是CPU指令,我们只能通过JNI与操作系统交互,关于CAS的方法都在sun.misc包下Unsafe的类里,java.util.concurrent.atomic包下的原子类等通过CAS来实现原子操作。

CAS特点:

CAS是原子操作,保证并发安全,而不能保证并发同步

CAS是CPU的一个指令(需要JNI调用Native方法,才能调用CPU的指令)

CAS是非阻塞的、轻量级的乐观锁

2、AtomicInteger实现

JDK提供了原子操作类,指的是 java.util.concurrent.atomic 包下,一系列以Atomic开头的包装类。例如AtomicBoolean,AtomicInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作。以下是AtomicInteger部分源代码:

    static {
        try {
            //获取value属性值在内存中的地址偏移量
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }

AtomicInteger 的getAndIncrement 调用了unsafe的getAndInt 方法完成+1原子操作。unsafe类的getAndInt方法源码如下:

    //var1是this指针,var2是地址偏移量,var4是自增值,是自增1还是自增N
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            //获取内存值
            var5 = this.getIntVolatile(var1, var2);

            //var5是期望值,var5 + var4是要更新的值
            //这个操作就是调用CAS的JNI,每个线程将自己内存里的内存值与var5期望值E作比较,如果相同,就将内存值更新为var5 + var4,否则做自旋操作
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        
        return var5;
    }

猜你喜欢

转载自www.cnblogs.com/alan6/p/11936251.html