前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >吃透并发编程之—-Atomic原子类学习思考

吃透并发编程之—-Atomic原子类学习思考

原创
作者头像
Joseph_青椒
发布2023-09-07 19:38:50
2040
发布2023-09-07 19:38:50
举报
文章被收录于专栏:java_joseph

什么是原子类?有何用

不可分割

一个操作是不可中断的,即使并发下

juc.atomic包下,都是原子特性的

原子类和锁都是为了保证并发下的线程安全,不过原子类相比于锁,有一定的优势:

***粒度更细,将竞争的粒度缩小到变量级别。

***效率高:在竞争不是很高的情况下:原子类的效率往往比锁的效率高。

第二个优势怎么说,。为何原子类在高度竞争的时候,效率会降低

因为原子操作利用了自旋锁和CAS算法,当并发高的时候

发生冲突的情况会大大增加 (也就是存在大量更新时去比较预期的值发生了变化,导致此次更新失效的情况),因此效率会大大降低

6类原子类

image-20230905201927512
image-20230905201927512

Atomic*基本原子类

有AtomicInteger AtomicLong AtomicBoolean

以AtomicInteger为例

以CAS技术保障原子性

public final int get()//获取当前的值

public final int getAndSet(int newValue)//获取当前的值,并设置新的值

public final int getAndIncrement()//获取当前的值,并自增

public final int getAndDecrement()//获取当前的值,并自减

public fianl int getAndAdd(int delta)//获取当前的值,并加上预期的值

boolean compareAndSet(int expect,int update)//如果当前的值等于预期值,则以原子的方式设置为输入的的update值,典型的CAS技术

演示

代码语言:javascript
复制
/**
 * @Author:Joseph
 * 演示AtomicInteger的基本用法 对比非原子类的线程安全问题
 * 使用了原子类之后不需要加锁,也能保障线程安全
 */
public class AtomicIntegerDemo1 implements Runnable{
    private static final AtomicInteger atomicInteger = new AtomicInteger();
    public void incrementAtomic(){
//        atomicInteger.getAndDecrement();
//        atomicInteger.getAndIncrement()
        atomicInteger.getAndAdd(2);
    }
    private static volatile int basicCount = 0;
    public void incremeBasic(){
        basicCount++;
    }
​
    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerDemo1 r = new AtomicIntegerDemo1();
        Thread thread1 = new Thread(r);
        Thread thread2 = new Thread(r);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("原子类的结果:"+atomicInteger.getAcquire());
        System.out.println("普通变量的结果:"+basicCount);
​
    }
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            incremeBasic();
            incrementAtomic();
        }
    }
}

Atomic*Array数组类型原子类

场景:比如做账系统,财务管理,并发修改,数字特别多,就可以通过AtomicArray来保障

例子

代码语言:javascript
复制
/**
 * @Author:Joseph
 * 演示原子数组的使用方法
 */
public class AtomicArrayDemo {
​
    public static void main(String[] args) throws InterruptedException {
​
        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(1000);
        Incrementer incrementer = new Incrementer(atomicIntegerArray);
        Decrementer decrementer = new Decrementer(atomicIntegerArray);
        Thread[] threadsIncrementer = new Thread[100];
        Thread[] threadsDecrementer = new Thread[100];
        for (int i = 0; i < 100; i++) {
            threadsDecrementer[i] = new Thread(decrementer);
            threadsIncrementer[i] = new Thread(incrementer);
            threadsDecrementer[i].start();
            threadsIncrementer[i].start();
        }
        for (int i = 0; i < 100; i++) {
            threadsDecrementer[i].join();
            threadsIncrementer[i].join();
        }
​
        for (int i = 0; i < atomicIntegerArray.length(); i++) {
            if(atomicIntegerArray.get(i)!=0){
                System.out.println("发现了错误:"+i);
            }
        }
        System.out.println("运行结束");
    }
}
class Decrementer implements Runnable{
​
    private  AtomicIntegerArray array;
​
    public Decrementer(AtomicIntegerArray array) {
        this.array = array;
    }
​
    @Override
    public void run() {
        for (int i = 0; i < array.length(); i++) {
            array.getAndDecrement(i);
        }
    }
}
class Incrementer implements Runnable{
​
    private  AtomicIntegerArray array;
​
    public Incrementer(AtomicIntegerArray array) {
        this.array = array;
    }
​
    @Override
    public void run() {
        for (int i = 0; i < array.length(); i++) {
            array.getAndIncrement(i);
        }
    }
}

Atomic*Reference引用类型原子类

比如我在锁那篇文章讲自旋锁的时候,就用到了引用类型原子类AtomicReference

代码语言:javascript
复制
public class SpinLock {
​
    private AtomicReference<Thread> sign = new AtomicReference<>();
​
    public void lock(){
        //获取当前线程的引用
        Thread current = Thread.currentThread();
        //通过compareAndSet命令,只有当线程为null,才设置当前线程,达到加锁目的
        while (!sign.compareAndSet(null,current)){
            System.out.println("自旋获取失败,再次尝试");
        }
    }
​
    public void unlock(){
        Thread current = Thread.currentThread();
        //解锁的时候,会判断有线程,才会设置为null,从而实现解锁 
        sign.compareAndSet(current,null);
    }
​
    public static void main(String[] args) throws InterruptedException {
        SpinLock spinLock = new SpinLock();
        Runnable runnable = ()->{
            System.out.println(Thread.currentThread().getName()+"尝试获取自旋锁");
            spinLock.lock();
            System.out.println(Thread.currentThread().getName()+"获取到了自旋锁");
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                spinLock.unlock();
                System.out.println(Thread.currentThread().getName()+"释放自旋锁");
            }
        };
        Thread thread1 = new Thread(runnable, "线程1");
        Thread thread2 = new Thread(runnable, "线程2");
        thread1.start();
        thread2.start();
​
    }
​
}

看下它内部的逻辑

image-20230905210434970
image-20230905210434970

可以看到没有像AtomicInteger那么多的API,主要就是这个compareAndSet方法

image-20230905210557294
image-20230905210557294

这里的注释,大致意思就是如果预期值和旧的值一样的话,就更新新的值,标准的CAS操作,

既然讲到了CAS操作,我的并发专栏里也专门讲到了这个,本质是通过cpu的原子指令,来完成比较再set的操作,

这里我通过synchronized来完成目的,这个synchronized是模拟原子性的,cpu执行这个的时候,指令是原子性,不可分割

代码语言:javascript
复制
public class EqualCAS {
    private int value;
    
    public synchronized  boolean compareAndSet(int expectedValue,int newValue){
        if(value == expectedValue){
            value = newValue;
            return true;
        }
        return false;
    }
}

普通变量升级具有原子功能

场景是这样的,如果一个类被定义好了,但是普通变量不具备原子性的,且类已经被定义好,不方便再进行改变,

就可通过AtomicIntegerFiledUpdater

比如一个业务。很少需要原子的get-set操作,就可以这样升级,而不用每个类都嵌入一个原子类型。减少资源占用,

用法,代码演示:

代码语言:javascript
复制
public class AtomicIntegerFieldUpdaterDemo implements Runnable {
​
    static Candidate tom;
    static Candidate peter;
​
    public static AtomicIntegerFieldUpdater<Candidate> socreUpdater = AtomicIntegerFieldUpdater.newUpdater(Candidate.class,"score");
​
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            peter.score++;
            socreUpdater.getAndIncrement(tom);
        }
    }
​
    public static class Candidate{
        volatile  int score;
    }
​
    public static void main(String[] args) throws InterruptedException {
        tom = new Candidate();
        peter = new Candidate();
        AtomicIntegerFieldUpdaterDemo r = new AtomicIntegerFieldUpdaterDemo();
        Thread thread = new Thread(r);
        thread.start();
        Thread thread3 = new Thread(r);
        thread3.start();
        thread.join();
        thread3.join();
        System.out.println("普通变量:"+peter.score);
        System.out.println("升级变量:"+tom.score);
    }
}

注意点

修饰的变量必须是可见的,也就是不能用private修饰

也不能是static修饰,因为static在初始化的时候已经定义到方法区了

这个类使用的时候需要使用类名,filed,这个和反射很相似,对,他背后也是通过反射来做的,一定要注意这两点

非常重要的Adder累加器

java8引入的,相对较新的一个类,

高并发下,LongAdder比AtmoicLong效率高,不过本质是空间换时间

竞争激烈的时候,Long 改,降低了冲突的概率,是多段锁的理念,提高了并发性

这里演示一个例子,目的为了展示Atomic在多线程下,性能会有瓶颈,每一次加法,都要flush和refresh

代码语言:javascript
复制
**
 * @Author:Joseph
 *  演示高并发场景下,LongAdder比AtomicLong性能好
 */
public class AtomicLongDemo {
    public static void main(String[] args) throws InterruptedException {
        AtomicLong counter = new AtomicLong(0);
        long start = System.currentTimeMillis();
        ExecutorService service = Executors.newFixedThreadPool(20);
        for (int i = 0; i < 10000; i++) {
            service.submit(new Task(counter));
        }
        service.shutdown();
        while (!service.isTerminated()){
        }
        long end = System.currentTimeMillis();
        System.out.println(counter.get());
        System.out.println("AtomicLong耗时"+(end - start));
    }
​
    private static class Task implements Runnable{
        private AtomicLong counter;
​
        public Task(AtomicLong counter) {
            this.counter = counter;
        }
​
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                counter.incrementAndGet();
            }
        }
    }
}
/**
 * @Author:Joseph
 *  演示高并发场景下,LongAdder比AtomicLong性能好
 */
public class LongAdderDemo {
    public static void main(String[] args) throws InterruptedException {
        LongAdder counter = new LongAdder();
        long start = System.currentTimeMillis();
        ExecutorService service = Executors.newFixedThreadPool(20);
        for (int i = 0; i < 10000; i++) {
            service.submit(new Task(counter));
        }
        service.shutdown();
        while (!service.isTerminated()){
        }
        long end = System.currentTimeMillis();
        System.out.println(counter.sum());
        System.out.println("LongAdder耗时"+(end - start));
    }
​
    private static class Task implements Runnable{
        private LongAdder counter;
​
        public Task(LongAdder counter) {
            this.counter = counter;
        }
​
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                counter.increment();
            }
        }
    }
}
​

自己运行一下

看到了吧,差这么多,就是因为AtomicLong每次加法都要flush和refresh

什么是flush和refresh???

这个我在JMM那个文章讲过

Atomic在每次加的时候要通过flush到主存,然后其他线程refresh到工作内存,这就浪费了性能

而LongAdder采用了分段锁的思想,并发量高的时候,每个线程在自己的cell中加数据,最后再汇总

它是这样搞的:

LongAdder有两个变量,base变量作为正常的累加值,并发量不高的时候直接在这里加

还有一个cell【】数组,并发搞的时候会把线程分散累加到自己的槽cell【i】中,通过hash值,为每个线程分配一个cell,用空间换时间的思想

sum源码分析

image-20230907192933896
image-20230907192933896

as为null,就直接return sum。不为null。就遍历数组加到sum再返回

这个过程是不加锁的,所有就会有安全问题,累加的时候,可能被更改,造成不准确的问题,

这样是LongAdder的 一个不好的地方。

场景

image-20230907193128497
image-20230907193128497

Accumlator累加器

Accumlator与Adder类似,就是一个更通用版本的Adder

代码语言:javascript
复制
public class LongAccumlatorDemo {
    public static void main(String[] args) {
        //x+y可以换成x*y,max(x,y)等等,就很方便
        LongAccumulator accumulator = new LongAccumulator((x, y) -> x + y, 100);
        ExecutorService service = Executors.newFixedThreadPool(8);
        IntStream.range(1,10).forEach(i->accumulator.accumulate(i));
        service.shutdown();
        while (!service.isTerminated()){}
​
        System.out.println(accumulator.getThenReset());
    }
}

适合场景:

适合大量的计算,需要通过并行方式提高效率的计算

但是要注意线程的先后不影响原先逻辑的情况下

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 6类原子类
  • Atomic*基本原子类
  • Atomic*Array数组类型原子类
  • Atomic*Reference引用类型原子类
    • 看下它内部的逻辑
    • 普通变量升级具有原子功能
      • 注意点
      • 非常重要的Adder累加器
        • 场景
        • Accumlator累加器
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档