美文网首页Java工程师知识树
Java基础-并发编程-原子类的使用与原理解析

Java基础-并发编程-原子类的使用与原理解析

作者: HughJin | 来源:发表于2021-02-07 09:35 被阅读0次

Java工程师知识树 / Java基础


原子类概述

JDK1.5版本之前,多行代码的原子性主要通过synchronized关键字进行保证。

JDK1.5版本,Java提供了原子类专门确保变量操作的原子性

原子类是java.util.concurrent.atomic开发包下的类:

原子类的原理

原子类的原理:原子类是通过自旋CAS操作volatile变量实现的。

  • CAS是compare and swap的缩写,即比较后(比较内存中的旧值与预期值)交换(将旧值替换成预期值)
  • atomic类会多次尝试CAS操作直至成功或失败,这个过程叫做自旋。
  • CAS是sun.misc包下Unsafe类提供的功能,需要底层硬件指令集的支撑,底层硬件指令集的调用通过JNI调用(native方法)。
  • 使用volatile变量是为了多个线程间变量的值能及时同步

原子类分类

分组 功能描述 原子变量类
基础数据型 提供对boolean、int、long的原子性操作 AtomicBoolean
AtomicInteger
AtomicLong
数组型 提供对数组元素的原子性操作 AtomicLongArray
AtomicIntegerArray
AtomicReferenceArray
字段更新器 提供对指定对象的指定字段进行原子性操作 AtomicLongFieldUpdater
AtomicIntegerFieldUpdater
AtomicReferenceFieldUpdater
引用型 提供对对象的原子性操作 AtomicReference
AtomicStampedReference
AtomicMarkableReference
原子累加器(jdk1.8后) AtomicLong和AtomicDouble的升级类型,专门用于数据统计,性能更高 DoubleAdder
LongAdder

基础数据型使用实例

Atomic类常用方法 ,以AtomicInteger为例

public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)

常见方法代码测试:

package com.study.thread.atomic;

import java.util.concurrent.atomic.AtomicInteger;

public class TestAtomicInteger {

    private static AtomicInteger atomicInteger = new AtomicInteger();

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            new Thread(()-> {
                for (int i1 = 0; i1 < 10000; i1++) {
                    atomicInteger.getAndIncrement();
                    System.out.println(atomicInteger.intValue());//顺序打印到100000
                }
            }).start();

            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            new Thread(()-> {
                for (int i1 = 0; i1 < 10000; i1++) {
                    atomicInteger.getAndDecrement();
                    System.out.println(atomicInteger.intValue());//顺序打印到-100000
                }
            }).start();

            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            new Thread(()-> {
                for (int i1 = 0; i1 < 1000; i1++) {
                    atomicInteger.getAndAdd(2);
                    System.out.println(atomicInteger.intValue());//顺序打印从2到20000的所有偶数
                }
            }).start();
        }
    }
}
使用场景

基础数据型适合并发情况下对int,long,boolean类型变量的使用++、--等多指令操作

常见功能实现:使用AtomicLong实现一个计时器
package com.study.thread.atomic;

import java.util.concurrent.atomic.AtomicLong;

/**
 * 基于AtomicLong的计时器
 */
public class Indicator {

    private static final Indicator INDICATOR = new Indicator();

    private Indicator() {}


    public static Indicator getInstance() {
        return INDICATOR;
    }

    private final AtomicLong requestCount = new AtomicLong();


    public void newRequestReceive() {
        requestCount.getAndIncrement();
    }

    public long getRequestCount() {
        return requestCount.get();
    }

}

测试代码:

package com.study.thread.atomic;

public class TesIndicator {

    public static void main(String[] args) {

        Indicator indicator = Indicator.getInstance();

        new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                indicator.newRequestReceive();
            }
        }).start();

        new Thread(()->{
            for (int i = 0; i < 20000; i++) {
                indicator.newRequestReceive();
            }
        }).start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(indicator.getRequestCount());

    }

}
// 30000
比AtomicLong更高效的DoubleAdder

//1、设置BenchmarkMode为Mode.Throughput,测试吞吐量
//2、设置BenchmarkMode为Mode.AverageTime,测试平均耗时
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@BenchmarkMode(Mode.Throughput)
public class Main {

    private static AtomicLong count = new AtomicLong();
    private static LongAdder longAdder = new LongAdder();
    public static void main(String[] args) throws Exception {
        Options options = new OptionsBuilder().include(Main.class.getName()).forks(1).build();
        new Runner(options).run();
    }

    @Benchmark
    @Threads(10)
    public void run0(){
        count.getAndIncrement();
    }

    @Benchmark
    @Threads(10)
    public void run1(){
        longAdder.increment();
    }
}

下面通过JMH比较一下AtomicLong 和 LongAdder的性能。

线程数为1

1、吞吐量

Benchmark   Mode  Cnt    Score   Error   Units
Main.run0  thrpt    5  154.525 ± 9.767  ops/us
Main.run1  thrpt    5   89.599 ± 7.951  ops/us

2、平均耗时

Benchmark  Mode  Cnt  Score    Error  Units
Main.run0  avgt    5  0.007 ±  0.001  us/op
Main.run1  avgt    5  0.011 ±  0.001  us/op

单线程情况:AtomicLong的吞吐量和平均耗时都占优势

线程数为10

1、吞吐量

Benchmark   Mode  Cnt    Score     Error   Units
Main.run0  thrpt    5   37.780 ±   1.891  ops/us
Main.run1  thrpt    5  464.927 ± 143.207  ops/us

2、平均耗时

Benchmark  Mode  Cnt  Score   Error  Units
Main.run0  avgt    5  0.290 ± 0.038  us/op
Main.run1  avgt    5  0.021 ± 0.001  us/op

并发线程为10个时:

  • LongAdder的吞吐量比较大,是AtomicLong的10倍多。
  • LongAdder的平均耗时是AtomicLong的十分之一。

一些高并发的场景,比如限流计数器,建议使用LongAdder替换AtomicLong。

数组型使用实例

核心方法:

  • get(int i):获取数组第i个下标的元素
  • getAndIncrement(int i):将第i个下标元素加一
  • getAdnAdd(int i,int delta):将第i个下标的元素增加delta(可为负数)
  • compareAndSet(int i,int expect,int update):进行CAS操作,如果第i个下标的元素等于expect,则设置为update,设置成功返回true

测试方法代码:

package com.study.thread.atomic;

import java.util.Arrays;
import java.util.concurrent.atomic.AtomicIntegerArray;

public class IntegerArrayTest {

    private static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10);
    private static int[] integerArray = new int[10];

     public static void main(String[] args) {

         System.out.println(atomicIntegerArray);
         AddThread[] arr =  new AddThread[500];//线程数越越明显
         for (int i = 0; i < 500; i++) {
             arr[i] = new AddThread();
         }

         for (int i = 0; i < 500; i++) {
             arr[i].start();
         }

         //将线程加入主线程中,执行完再执行主线程
         for (int i = 0; i < 500; i++) {
             try {
                 arr[i].join();
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }

         System.out.println(atomicIntegerArray);
         System.out.println(Arrays.toString(integerArray));

     }

    static class AddThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                for (int i1 = 0; i1 < 1000; i1++) {
                    atomicIntegerArray.getAndIncrement(i);//将第i个下标元素加一
                    integerArray[i]++;//使用多指令操作元素加一
                }
            }
        }
    }
}
//打印结果
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[500000, 500000, 500000, 500000, 500000, 500000, 500000, 500000, 500000, 500000]
[499729, 499561, 499352, 499122, 499149, 499156, 499176, 499004, 498873, 499131]

字段更新器使用实例

字段更新器注意事项:

  • 初始化字段更新器使用AtomicIntegerFieldUpdater.newUpdater
  • 不支持static字段
  • 必须使用volatile
package com.study.thread.atomic;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class AtomicIntegerFieldUpdaterDemo {

    private static AtomicInteger atomicInteger = new AtomicInteger();

    private static AtomicIntegerFieldUpdater<FieldDTO> atomicIntegerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(FieldDTO.class, "score");

    public static void main(String[] args) throws InterruptedException {
        FieldDTO fieldDTO = new FieldDTO();
        Thread[] arrThread = new Thread[1000];

        for (int i = 0; i < 1000; i++) {
            arrThread[i] = new Thread(()->{
                atomicInteger.getAndIncrement();
                atomicIntegerFieldUpdater.incrementAndGet(fieldDTO);
            });
        }
        for (int i = 0; i < 1000; i++) {
            arrThread[i].start();
        }
        for (int i = 0; i < 1000; i++) {
            arrThread[i].join();
        }

        System.out.println(fieldDTO.score);
        System.out.println(atomicInteger.get());
    }

    static class FieldDTO{
        int id ;
        String name;
        volatile int score;
    }
}
//打印结果
1000
1000

引用型使用类型使用实例

AtomicReference使用示例

常用方法:compareAndSet

package com.study.thread.atomic;

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceDemo {

    static AtomicReference<String> atomicReference = new AtomicReference<>("123");

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

        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                if (atomicReference.compareAndSet("123", "456")) {
                    System.out.println(Thread.currentThread().getName().concat("字符串由123改成了456."));
                }
            },"修改属性"+i).start();
        }

        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                if (atomicReference.compareAndSet("456", "123")) {
                    System.out.println(Thread.currentThread().getName().concat("字符串由456改成了123!"));
                }
            },"还原属性"+i).start();
        }

        Thread.sleep(2000);

        System.out.println(atomicReference.get());
    }
}
//打印结果    每次可能不一样    四个线程执行了操作
修改属性0字符串由123改成了456.
还原属性0字符串由456改成了123!
修改属性9字符串由123改成了456.
还原属性72字符串由456改成了123!
123

AtomicReference中的ABA问题

package com.study.thread.atomic;

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceDemo {

    static AtomicReference<String> atomicReference = new AtomicReference<>("123");

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

        Thread t2 = new Thread(()->{
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (atomicReference.compareAndSet("123", "456")) {
                System.out.println(Thread.currentThread().getName().concat("字符串由123改成了456!"));
            }
        },"修改属性2");

        Thread t1 = new Thread(()->{
            if (atomicReference.compareAndSet("123", "456")) {
                System.out.println(Thread.currentThread().getName().concat("字符串由123改成了456."));
            }
            System.out.println(Thread.currentThread().getName().concat("---").concat(atomicReference.get()));
            if (atomicReference.compareAndSet("456", "123")) {
                System.out.println(Thread.currentThread().getName().concat("字符串由456改成了123."));
            }
        },"修改属性1");


        t2.start();
        t1.start();
        t2.join();
        t1.join();
        System.out.println(atomicReference.get());//456  执行完之后修改了
    }
}
//执行结果  执行完之后修改了    
//事实上在修改属性1线程操作字符串后,修改属性2是不想再修改了 发生了ABA
修改属性1字符串由123改成了456.
修改属性1---456
修改属性1字符串由456改成了123.
修改属性2字符串由123改成了456!
456

使用AtomicStampedReference解决CAS中的ABA问题

package com.study.thread.atomic;

import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicReferenceDemo {

    static AtomicStampedReference<String> atomicStampedReference = new AtomicStampedReference<String>("123", 0);

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

        Thread t2 = new Thread(() -> {
            int start = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName().
                    concat("版本号".concat(String.valueOf(atomicStampedReference.getStamp()))));//版本号
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int end = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName().
                    concat("版本号".concat(String.valueOf(atomicStampedReference.getStamp()))));//版本号
            if (atomicStampedReference.compareAndSet("123", "456", start, end)) {
                System.out.println(Thread.currentThread().getName().concat("字符串由123改成了456!"));
            }
        }, "修改属性2");

        Thread t1 = new Thread(() -> {
            if (atomicStampedReference.compareAndSet("123",
                    "456", atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1)) {
                System.out.println(Thread.currentThread().getName().concat("字符串由123改成了456."));
            }
            System.out.println(Thread.currentThread().getName().concat("---")
                    .concat(atomicStampedReference.getReference()));
            if (atomicStampedReference.compareAndSet("456",
                    "123", atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1)) {
                System.out.println(Thread.currentThread().getName()
                        .concat("字符串由456改成了123."));
            }
        }, "修改属性1");


        t2.start();
        t1.start();
        t2.join();
        t1.join();
        System.out.println(atomicStampedReference.getReference());//123  通过版本号控制,出线ABA问题时可以不更新
    }
}
//打印结果    修改属性2线程执行逻辑前后版本好不一致了,那修改操作就没有操作了,解决了ABA问题
修改属性2版本号0
修改属性1字符串由123改成了456.
修改属性1---456
修改属性1字符串由456改成了123.
修改属性2版本号2
123

总结:AtomicStampedReference通过版本号控制,出线ABA问题时不执行更新操作了。

相关文章

网友评论

    本文标题:Java基础-并发编程-原子类的使用与原理解析

    本文链接:https://www.haomeiwen.com/subject/csxqtltx.html