线程安全之原子性Atomic(AtomicInteger|LongAdder|AtomicLong)

1,049 阅读4分钟

线程安全性

当多线程访问某个类时,不管运行环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何的同步或者协同,这个类都能表现出正确的行为,那么这个类就是线程安全的.

原子性

提供互斥访问,同一时刻只有一个线程对它进行访问.

Atomic包

位于java.util.concurrent.atomic,AtomicXXX : CAS、Unsafe.compareAndSwapXXX

CAS(Compare and swap)比较和替换是设计并发算法用的的一项技术,比较和替换是用一个期望值和一个变量的当前值进行比较,如果变量的值和期望值相等,那么就用一个新值替换变量的值.

案例

线程安全

package com.keytech.task;

import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;

/**
 * @className: AtomicTest
 * @description: TODO 类描述
 * @author: mac
 * @date: 2020/12/27
 **/
public class AtomicTest {

    private static Integer clientTotal=5000;
    private static Integer threadTotal=200;

    private static AtomicInteger count=new AtomicInteger(0);

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        Semaphore semaphore= new Semaphore(threadTotal);
        for (int i = 0; i <clientTotal ; i++) {
            executorService.execute(()->{
                try{
                    semaphore.acquire();
                    update();
                    semaphore.release();
                }catch (Exception e){
                    e.printStackTrace();
                }
            });
        }
        executorService.shutdown();
        System.out.println("count:"+count);
    }

    private static void update(){
        count.incrementAndGet();
    }
}

getAndAddInt源码

public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
}

compareAndSwapInt(this, stateOffset, expect, update)这个方法的作用就是通过cas技术来预测stateOffset变量的初始值是否是expect,如果是,那么就把stateOffset变量的值变成update,如果不是,那么就一直自旋转,一直到stateOffset变量的初始值是expect,然后在在修改stateOffset变量的值变成update

LongAddr

线程安全

package com.keytech.task;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.LongAdder;

/**
 * @className: LongAddrTest
 * @description: TODO 类描述
 * @author: mac
 * @date: 2020/12/28
 **/
public class LongAddrTest {

    private static Integer clientTotal=5000;
    private static Integer threadTotal=200;
    
    private static LongAdder count=new LongAdder();
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        Semaphore semaphore=new Semaphore(threadTotal);
        for (int i = 0; i < clientTotal; i++) {
            try{
                semaphore.acquire();
                update();
                semaphore.release();

            }catch (Exception e){
                e.printStackTrace();
            }
        }

        executorService.shutdown();
        System.out.println("count"+count);

    }
    private static void update(){
        count.increment();
    }
}

AtomicLong

线程安全

package com.keytech.task;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicLong;

/**
 * @className: AtomicLongTest
 * @description: TODO 类描述
 * @author: mac
 * @date: 2020/12/28
 **/
public class AtomicLongTest {

    private static Integer clientTotal=5000;
    private static Integer threadTotal=200;
    private static AtomicLong count=new AtomicLong();

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        Semaphore semaphore=new Semaphore(threadTotal);
        for (int i = 0; i < clientTotal; i++) {
            try{
                semaphore.tryAcquire();
                update();
                semaphore.release();

            }catch (Exception e){
                e.printStackTrace();
            }

        }
        executorService.shutdown();
        System.out.println("count"+count);
    }

    private static void update(){
        count.incrementAndGet();
    }
}

LongAddr与AtomicLong的区别

AtomicLong的原理是依靠底层的cas来保障原子性的更新数据,在要添加或者减少的时候,会使用死循环不断地cas到特定的值,从而达到更新数据的目的。如果竞争不激烈,修改成功几率很高,否则失败概率很高,在失败几率很高的情况下,这些原子操作就会进行多次的循环操作尝试,因此性能会受到影响。

LongAdder

对于普通类型的Long和Doubble变量,JVM允许将64位的读操作或写操作拆成两个三十二位的操作。

LongAdder则是内部维护一个Cells数组,每个Cell里面有一个初始值为0的long型变量,在同等并发量的情况下,争夺单个变量的线程会减少,这是变相的减少了争夺共享资源的并发量,另外多个线程在争夺同一个原子变量时候,如果失败并不是自旋CAS重试,而是尝试获取其他原子变量的锁,最后当获取当前值时候是把所有变量的值累加后再加上base的值返回的。

LongAdder的核心是将热点数据分离,比如说它可以将AtomicLong内部核心数据value分离成一个数组,每个线程访问时,通过hash等算法,映射到其中一个数字进行计数,最终的计数结果则会这个数据的求和累加,其中热点数据value会被分离成多个cell,每个cell独自维护内部的值,当前对象实际值为所有cell累计合成,这样的话,热点就进行了有效的分离,并提高了并行度。

LongAdder在AtomicLong的基础上将单点的更新压力分散到各个节点,在低并发的时候通过对base的直接更新可以很好的保障和AtomicLong的性能基本保持一致,而在高并发的时候通过分散提高了性能。缺点是LongAdder在统计的时候如果有并发更新,可能导致统计的数据有误差。

LongAddr与AtomicLong的使用场景

实际使用中,在处理高并发时,可以优先使用LongAdder,而不是继续使用AtomicLong,当然,在线程竞争很低的情况下,使用AtomicLong更简单更实际一些,并且效率会高些。其他情况下,比如序列号生成,这种情况下需要准确的数值,全局唯一的AtomicLong才是正确的选择,而不是LongAdder