原子性带来的并发安全问题

在并发编程中,产生并发安全问题主要有三个源头,可见性,原子性,有序性
可见性与有序性都可以使用volatile关键字解决,下面我们来分析下原子性带来的并发安全问题

由于IO太慢,在早期的操作系统中就发明了多进程,操作系统允许某个进程执行一小段时间,例如过了50毫秒,过了50毫秒后,操作系统就会重新选择一个新的进程来执行(也就是“任务切换”),这个50毫秒也就是操作系统中的时间片概念
进程切换
在一个时间片内,如果一个进程进行一个IO操作,例如读文件,这个时候该进程可以把自己标记为“休眠状态”并让出CPU的使用权,当文件全部读进内存,OS会把这个休眠的进程唤醒,然后重新获得CPU的使用权

之所以这个进程会释放自己的CPU使用权,就是为了让CPU等待这个IO操作的过程中能够处理别的任务,这样一来CPU的使用率就上来了

在早期的操作系统中是基于进程来调度CPU,不同的进程是共享不同的内存空间的,所以每当一个进程需要做任务切换就要切断内存映射地址,而一个进程中的所有线程都是共享一个内存空间的,所以线程做任务切换的成本会非常低

Java并发编程都是基于多线程的,自然会涉及到线程间的任务切换,而这个任务切换,就是导致并发条件下程序bug的核心原因!

在高级语言中,一条语句往往需要多条CPU指令完成,例如很常见的加1操作,count+=1,至少需要三条CPU指令

  • 指令1:首先,需要把变量count从内存加载到CPU寄存器中
  • 指令2:之后,在寄存器中执行+1操作
  • 指令3:最后,将结果写入到内存(缓存机制可能导致写入的是缓存而不是内存)

操作系统做任务切换,可以发生在任意一条CPU指令执行完,注意是CPU指令而不是高级语言中的一条语句

对于上面的 +1 操作,线程A在指令1执行完后执行了任务切换,然后由线程B来重新执行这三条指令,最后发现得到的结果不是我们想的2,而是1
原子性问题
我们把一个或者多个操作在CPU执行的过程中不被中断的特性称为原子性,CPU能保证的原子操作是CPU级别的,而不是高级语言级别的,很多时候我们需要在高级语言层面保证操作的原子性

原子类

java.util.concurrent.atomic包下给我们提供许多原子操作的实现类,同样是出自于大名鼎鼎的Doug Lea之手!

  • 不可分割
  • 一个操作不可中断,即使在多线程下也保证不可中断性

有什么作用?

原子类的作用和锁类似,目的都是为了保证在多线程并发条件下的安全问题,但是原子类类比锁来说有两个优势

  • 锁的粒度更细,原子类可以将锁的粒度细化到变量级别, 但是通常锁的粒度都是好几行代码
  • 效率更高,但是在线程高度竞争的情况下效率会降低
    原子类

AtomicInteger

下方代码为使用原子的AtomicInteger与普通的Integer进行累加操作演示

public class AtomicIntegerDemo implements Runnable{
    private static final AtomicInteger atomicInteger = new AtomicInteger(0);
    private static volatile Integer integer = 0;

    public void incrementAtomic(){
        atomicInteger.getAndIncrement();
    }

    public void incrementInteger(){
        integer++;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            incrementAtomic();
            incrementInteger();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerDemo t = new AtomicIntegerDemo();
        Thread thread1 = new Thread(t);
        Thread thread2 = new Thread(t);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        System.out.println("Atomic:"+atomicInteger.get());
        System.out.println("Integer:"+integer);
    }
}
Atomic:2000
Integer:1484

结果发现使用AtomicInteger执行的结果正确,使用Integer的结果不正确,看了前面的原子性导致的并发安全问题分析后,很容易就看出来这段代码中Integer累加过程中,由于+1操作不是原子性,两个线程进行任务切换的过程中出现了原子性问题

为什么AtomicInteger能保证原子性?

首先先了解一个概念,什么叫做非阻塞同步:

同步:多线程并发访问共享数据时,保证共享数据在同一时刻只被一个或一些线程使用。

我们知道,阻塞同步和非阻塞同步都是实现线程安全的两个保障手段,非阻塞同步对于阻塞同步而言主要解决了阻塞同步中线程阻塞唤醒带来的性能问题,那什么叫做非阻塞同步呢?在并发环境下,某个线程对共享变量先进行操作,如果没有其他线程争用共享数据那操作就成功;如果存在数据的争用冲突,那就才去补偿措施,比如不断的重试机制,直到成功为止,因为这种乐观的并发策略不需要把线程挂起,也就把这种同步操作称为非阻塞同步(操作和冲突检测具备原子性)。在硬件指令集的发展驱动下,使得 "操作和冲突检测" 这种看起来需要多次操作的行为只需要一条处理器指令便可以完成,这些指令中就包括非常著名的CAS指令(Compare-And-Swap比较并交换)。《深入理解Java虚拟机第二版.周志明》第十三章中这样描述关于CAS机制:

在这里插入图片描述

我们来看看AtomicInteger中的incrementAndGet()方法:

public final int incrementAndGet() {
        return U.getAndAddInt(this, VALUE, 1) + 1;
    }

调用jdk.internal.misc.Unsafe类中getAndAddInt()

@HotSpotIntrinsicCandidate
    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!weakCompareAndSetInt(o, offset, v, v + delta));
        return v;
    }
@HotSpotIntrinsicCandidate
    public final boolean weakCompareAndSetInt(Object o, long offset,
                                              int expected,
                                              int x) {
        return compareAndSetInt(o, offset, expected, x);
    }

最终调用Unsafe中的本地方法compareAndSetInt(),使用JNI(Java Native Interface)访问本地C++实现库,来与操作系统进行交互

@HotSpotIntrinsicCandidate
    public final native boolean compareAndSetInt(Object o, long offset,
                                                 int expected,
                                                 int x);

从上述代码可以看到CAS有四个操作数,分别是:对象内存位置对象中的变量的偏移量变量预期值新值

具体含义:

如果对象 o 中内存偏移量为 offset(这里的偏移量是相对起始内存地址的偏移量) 位置的变量与 expect 相等,则把x赋给偏移量为offset位置的变量,返回 true,如果不相等,则取消赋值,返回 false

LongAdder

在高并发情况下,LongAdder(累加器)比AtomicLong原子操作效率更高,LongAdder累加器是java8新加入的,参考以下案例

public class AtomicIntegerAdderDemo {
    private static AtomicInteger atomicInteger = new AtomicInteger(0);

    public static void main(String[] args) {
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5,12,60, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10000));
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    atomicInteger.incrementAndGet();
                }
            }
        };
        Long before = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            poolExecutor.execute(runnable);
        }
        poolExecutor.shutdown();
        while (!poolExecutor.isTerminated()){
        }
        Long after = System.currentTimeMillis();
        System.out.println(atomicInteger.get());
        System.out.println("AtomicInteger累加耗时:"+(after-before));
    }
}
100000000
AtomicInteger累加耗时:2288
public class LongAdderDemo {
    private static LongAdder longAdder = new LongAdder();

    public static void main(String[] args) {
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5,12,60, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10000));
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    longAdder.increment();
                }
            }
        };
        Long before = System.currentTimeMillis();
        
        for (int i = 0; i < 10000; i++) {
            poolExecutor.execute(runnable);
        }
        poolExecutor.shutdown();
        //保证所有的线程任务执行完毕
        while (!poolExecutor.isTerminated()){
        }

        Long after = System.currentTimeMillis();

        System.out.println(longAdder.sum());
        System.out.println("AtomicInteger累加耗时:"+(after-before));
    }
}
100000000
AtomicInteger累加耗时:352

AtomicInteger

在多线程高度竞争的情况下,AtomicInteger每一次操作都需要将修改的值flush到主内存中,然后再refresh到所有线程的工作内存中

LongAdder

LongAdderAtomicInteger不同之处在于每个线程有自己的计数器,不需要每一次操作都flushrefresh一次

LongAdder中增加了分段累加的概念,内部有一个base变量和一个cell[]数组共同参与计数:

  • base变量:竞争不激烈的情况下,直接累加到该变量上
  • cell[]数组:竞争激烈,各个线程分散(通过一定的hash运算)累加到自己的槽cell[i]中

其本质就是空间换时间,最后将base变量和cell[]数组中的值进行sum

public long sum() {
        Cell[] cs = cells;
        long sum = base;
        if (cs != null) {
            for (Cell c : cs)
                if (c != null)
                    sum += c.value;
        }
        return sum;
    }

AtomicIntegerArray

相关实现有AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray,可以原子地更新数组中的每一个元素,这些类提供的方法和原子化的基本数据类型区别仅仅是每个方法多了一个数组的索引参数,这里就不再赘述了

AtomicReference

AtomicReferenceAtomicInteger非常类似,不同之处就在于AtomicInteger是对整数的封装,而AtomicReference则对应普通的对象引用。也就是它可以保证在多线程并发情况下对象的原子性,其原理也是采用的CAS,下面由AtomicReference实现的自旋锁

public class AtomicReferenceMakeSpinLock {
    private AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void lock(){
        Thread currentThread = Thread.currentThread();
        while (!atomicReference.compareAndSet(null,currentThread)){
        }
    }

    public void unLock(){
        Thread currentThread = Thread.currentThread();
        atomicReference.compareAndSet(currentThread,null);
    }

    public static void main(String[] args) {
        AtomicReferenceMakeSpinLock lock = new AtomicReferenceMakeSpinLock();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"开始准备获取锁");
                lock.lock();
                System.out.println(Thread.currentThread().getName()+"获取锁成功");
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    System.out.println(Thread.currentThread().getName()+"解锁成功");
                    lock.unLock();
                }
            }
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}
Thread-0开始准备获取锁
Thread-1开始准备获取锁
Thread-0获取锁成功
Thread-0解锁成功
Thread-1获取锁成功
Thread-1解锁成功

AtomicIntegerFieldUpdater(原子化对象属性更新器)

相关实现有AtomicIntegerFieldUpdaterAtomicLongFieldUpdaterAtomicReferenceFieldUpdater,可以将我们自定义类生成的对象中的成员变量原子化,其原理都是使用反射实现

public class AtomicIntegerFieldUpdaterDemo {
    private static Student jack = new Student(0);
    private static Student ma = new Student(0);
    private static AtomicIntegerFieldUpdater atomicIntegerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(Student.class,"score");
    private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5,12,60, TimeUnit.SECONDS,new ArrayBlockingQueue<>(100));

    public static void main(String[] args) throws InterruptedException {

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                jack.score++;
                atomicIntegerFieldUpdater.getAndIncrement(ma);
            }
        };

        for (int i = 0; i < 1000; i++) {
            threadPoolExecutor.execute(runnable);
        }
        threadPoolExecutor.shutdown();
        while (!threadPoolExecutor.isTerminated()){
        }
        System.out.println(jack.score);
        System.out.println(ma.score);

    }
}

class Student{
    volatile int score;

    public Student(int score) {
        this.score = score;
    }

    public int getScore() {
        return score;
    }

    public void setScore(int score) {
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "score=" + score +
                '}';
    }
}
jack.score:996
ma.score:1000

注意:对象的属性必须由volatile修饰,为了保证多线程下的可见性,如果对象不是volatile修饰,将类原子化后,则会抛出IllegalArgumentException运行时异常

Exception in thread "main" java.lang.ExceptionInInitializerError
Caused by: java.lang.IllegalArgumentException: Must be volatile type
    at java.base/java.util.concurrent.atomic.AtomicIntegerFieldUpdater$AtomicIntegerFieldUpdaterImpl.<init>(AtomicIntegerFieldUpdater.java:420)
    at java.base/java.util.concurrent.atomic.AtomicIntegerFieldUpdater.newUpdater(AtomicIntegerFieldUpdater.java:94)
    at site.kexing.atomic.AtomicIntegerFieldUpdaterDemo.<clinit>(AtomicIntegerFieldUpdaterDemo.java:14)

无锁方案对于互斥锁方案,优点非常多,首先性能好,其次是基本不会出现死锁问题(但可能出现饥饿和活锁问题,因为自旋会反复尝试),Java提供的原子类能解决一些简单的原子性问题,但是我们会发现,原子类的方法都是针对一个共享变量的,如果需要解决多个变量的原子性问题,建议还是使用互斥锁方案

最后修改:2021 年 11 月 15 日 05 : 11 PM
如果觉得我的文章对你有用,请随意赞赏