【Java并发编程 八】JUC并发包下原子类

简介: 【Java并发编程 八】JUC并发包下原子类

atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰,所以,所谓原子类说简单点就是具有原子操作特征的类,原子操作类提供了一些修改数据的方法,这些方法都是原子操作的,在多线程情况下可以确保被修改数据的正确性,我们在前边的Java并发机制底层实现中了解到,通过CAS操作可以实现原子操作,整体分类如下

基本原子类

基本原子类型包含三种,都比较简单,这里我们以AtomicInteger为例进行介绍:

AtomicInteger:int 类型原子类
AtomicLong:long 类型原子类
AtomicBoolean :boolean类型原子类

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) //获取当前的值,并加上预期的值
//如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
boolean compareAndSet(int expect, int update)
//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
public final void lazySet(int newValue)

我们从其实现的源码中可以看出:

private static final Unsafe unsafe = Unsafe.getUnsafe();
//value属性在AtomicInteger中的偏移量,通过这个偏移量可以快速定位到value字段,这个是实现AtomicInteger的关键
private static final long valueOffset; 
static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}
private volatile int value; //使用volatile修饰,可以确保value在多线程中的可见性。

可以通过一个方法的源码来看其调用方式:

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
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));   //CAS自旋等待,多线程情况下安全
    return var5;
}
public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

getAndAddInt操作相当于线程安全的count++操作

synchronize(lock){
   count++;
}

synchronize的方式会导致占时无法获取锁的线程处于阻塞状态,性能比较低。CAS的性能比synchronize要快很多

数组原子类

使用原子的方式更新数组里的某个元素,可以确保修改数组中数据的线程安全性

AtomicIntegerArray:整形数组原子操作类
AtomicLongArray:长整形数组原子操作类
AtomicReferenceArray :引用类型数组原子操作类

上面三个类提供的方法几乎相同,所以我们这里以 AtomicIntegerArray 为例子来介绍

public final int get(int i) //获取 index=i 位置元素的值
//返回 index=i 位置的当前的值,并将其设置为新值:newValue
public final int getAndSet(int i, int newValue)
public final int getAndIncrement(int i)//获取 index=i 位置元素的值,并让该位置的元素自增
public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减
public final int getAndAdd(int delta) //获取 index=i 位置元素的值,并加上预期的值
//如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update)
boolean compareAndSet(int expect, int update) 
//最终 将index=i 位置的元素设置为newValue,
//使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
public final void lazySet(int i, int newValue)

示例如下:

public class ThreadTest {
    static AtomicIntegerArray pageRequest = new AtomicIntegerArray(new int[10]);
    public static void request(int page) throws InterruptedException {
        //模拟耗时5毫秒
        TimeUnit.MILLISECONDS.sleep(5);
        //pageCountIndex为pageCount数组的下标,表示页面对应数组中的位置
        int pageCountIndex = page - 1;
        pageRequest.incrementAndGet(pageCountIndex);
    }
    public static void main(String[] args) throws InterruptedException {
        long starTime = System.currentTimeMillis();
        int threadSize = 100;
        CountDownLatch countDownLatch = new CountDownLatch(threadSize);
        for (int i = 0; i < threadSize; i++) {
            Thread thread = new Thread(() -> {
                try {
                    for (int page = 1; page <= 10; page++) {
                        for (int j = 0; j < 10; j++) {
                            request(page);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            });
            thread.start();
        }
        countDownLatch.await();
        long endTime = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName() + ",耗时:" + (endTime - starTime));
        for (int pageIndex = 0; pageIndex < 10; pageIndex++) {
            System.out.println("索引值为" + (pageIndex + 1) + " 被累加次数为" + pageRequest.get(pageIndex));
        }
    }
}

执行结果为:

main,耗时:650
索引值为1 被累加次数为1000
索引值为2 被累加次数为1000
索引值为3 被累加次数为1000
索引值为4 被累加次数为1000
索引值为5 被累加次数为1000
索引值为6 被累加次数为1000
索引值为7 被累加次数为1000
索引值为8 被累加次数为1000
索引值为9 被累加次数为1000
索引值为10 被累加次数为1000

其实其底层也是调用了普通原子类实现

引用原子类

基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用 引用类型原子类

AtomicReference:引用类型原子类
AtomicStampedRerence:原子更新引用类型里的字段原子类  //可以用时间戳解决ABA问题
AtomicMarkableReference :原子更新带有标记位的引用类型

ABA问题

普通情况下ABA问题没有危害,不过可以看一种特殊场景,场景是用链表来实现一个栈,初始化向栈中压入B、A两个元素,栈顶head指向A元素。head(A)->B

Thread thread1 = new Thread(
    ->{
          oldValue = head;
          sleep(3秒);
          //thread2切换执行
          compareAndSet(oldValue, B);
    }
);
Thread thread2 = new Thread(
    ->{
          // 弹出A
          newHead = head.next;
          head.next = null; //即A.next = null;
          head = newHead;
          // 弹出B
          newHead = head.next;
          head.next = null; // 即B.next = null;
          head = newHead; // 此时head为null
          // 压入C
          head = C;
          // 压入D
          D.next = head;
          head = D;
          // 压入A
          A.next = D;
          head = A;         
    }
);
thread1.start();
thread2.start();
  1. 线程1试图将栈顶换成B,但它获取栈顶的oldValue(head,也就是A)后,被线程2中断了。
  2. 线程2依次将A、B弹出,然后压入C、D、A。head(A)->D->C
  3. 然后换线程1继续运行,线程1执行compareAndSet发现head指向的元素确实与oldValue一致,都是A,所以就将head指向B了。head(B)

但是,线程2在弹出B的时候,将B的next置为null了,因此在线程1将head指向B后,栈中只剩元素B。但按预期来说,栈中应该放的是B → A → D → C

AtomicStampedRerence可以解决ABA问题,他内部不仅维护了对象的值,还维护了一个时间戳(我们这里把他称为时间戳,实际上它可以使用任何一个整形来表示状态值),当AtomicStampedRerence对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳。当AtomicStampedRerence设置对象值时,对象值及时间戳都必须满足期望值,写入才会成功。因此,即使对象值被反复读写,写回原值,只要时间戳发生变量,就能防止不恰当的写入

更新原子类

如果需要原子更新某个类里的某个字段时,需要用到对象的属性修改原子类

AtomicIntegerFieldUpdater:原子更新整形字段的值
AtomicLongFieldUpdater:原子更新长整形字段的值
AtomicReferenceFieldUpdater :原子更新引用类型字段的值

要想原子地更新对象的属性需要两步:

  1. 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
  2. 更新的对象属性必须使用 public volatile 修饰符。

上面三个类提供的方法几乎相同,所以我们这里以AtomicReferenceFieldUpdater为例子来介绍。

调用AtomicReferenceFieldUpdater静态方法newUpdater创建AtomicReferenceFieldUpdater对象

//tclass:需要操作的字段所在的类,vclass:操作字段的类型,fieldName:字段名称
public static <U, W> AtomicReferenceFieldUpdater<U, W> 
         newUpdater(Class<U> tclass, Class<W> vclass, String fieldName)

多线程并发调用一个类的初始化方法,如果未被初始化过,将执行初始化工作,要求只能初始化一次

public class Demo5 {
    static Demo5 demo5 = new Demo5();
    //isInit用来标注是否被初始化过
    volatile Boolean isInit = Boolean.FALSE;
    AtomicReferenceFieldUpdater<Demo5, Boolean> updater = AtomicReferenceFieldUpdater.newUpdater(Demo5.class, Boolean.class, "isInit");
    public void init() throws InterruptedException {
        //isInit为false的时候,才进行初始化,并将isInit采用原子操作置为true
        if (updater.compareAndSet(demo5, Boolean.FALSE, Boolean.TRUE)) {
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",开始初始化!");
            //模拟休眠3秒
            TimeUnit.SECONDS.sleep(3);
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",初始化完毕!");
        } else {
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",有其他线程已经执行了初始化!");
        }
    }
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    demo5.init();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

返回结果为:

1565159962098,Thread-0,开始初始化!
1565159962098,Thread-3,有其他线程已经执行了初始化!
1565159962098,Thread-4,有其他线程已经执行了初始化!
1565159962098,Thread-2,有其他线程已经执行了初始化!
1565159962098,Thread-1,有其他线程已经执行了初始化!
1565159965100,Thread-0,初始化完毕!

可以看出多线程同时执行init()方法,只有一个线程执行了初始化的操作,其他线程跳过了。多个线程同时到达updater.compareAndSet,只有一个会成功

增强原子类

原子类的基本实现机制,它们都是在一个死循环内,不断尝试修改目标值,知道修改成功。如果竞争不激烈,那么修改成功的概率就很高,否则,修改失败的概率就很高。在大量修改失败时,这些原子操作就会进行多次循环尝试,因此性能会受到影响

当竞争激烈的时候,为了进一步提高系统的性能,一种基本方案就是可以使用热点分离,将竞争的数据进行分解,基于这个思路,可以想到一种对传统AtomicInteger等原子类的改进方法。虽然在CAS操作中没有锁,但是像减小锁粒度这种分离热点的思想依然可以使用。一种可行的方案就是仿造ConcurrentHashMap,将热点数据分离。比如,可以将AtomicInteger的内部核心数据value分离成一个数组,每个线程访问时,通过哈希等算法映射到其中一个数字进行计算,而最终的计算结果,则为这个数组的求和累加。热点value被分离成多个单元cell,每个cell独自维护内部的值,当前对象的实际值由所有的cell累计合成,这样,热点就进行了有效的分离,提高了并行度。增强原子类正是利用了这种原理实现,这里不详细介绍了。

相关文章
|
19小时前
|
安全 Java 数据库
Java并发编程:最佳实践与性能优化
Java并发编程:最佳实践与性能优化
|
1天前
|
Java 数据处理 调度
Java多线程编程入门指南
Java多线程编程入门指南
|
19小时前
|
前端开发 Java 开发工具
Java GUI编程:跨平台应用的设计与开发
Java GUI编程:跨平台应用的设计与开发
|
19小时前
|
设计模式 Java 容器
Java多线程编程中的设计模式与挑战
Java多线程编程中的设计模式与挑战
|
1天前
|
Java API 数据库
深研Java异步编程:CompletableFuture与反应式编程范式的融合实践
【6月更文挑战第30天】Java 8的CompletableFuture革新了异步编程,提供如thenApply等流畅接口,而Java 9后的反应式编程(如Reactor)强调数据流和变化传播,以事件驱动应对高并发。两者并非竞争关系,而是互补,通过Flow API和第三方库结合,如将CompletableFuture转换为Mono进行反应式处理,实现更高效、响应式的系统设计。开发者可根据需求灵活选用,提升现代Java应用的并发性能。
13 1
|
1天前
|
安全 Java 程序员
深入理解Java内存模型(JMM)及其对并发编程的影响
【6月更文挑战第29天】在Java并发编程的世界中,内存模型是基石之一。本文将深入探讨Java内存模型(JMM)的核心概念,包括可见性、原子性、有序性和同步,并解释它们如何影响并发编程实践。通过分析JMM的工作原理和它与Java并发库的关系,我们将揭示正确使用JMM原则可以如何避免并发编程中的常见陷阱。
|
19小时前
|
安全 Java
Java多线程编程实践中的常见问题与解决方案
Java多线程编程实践中的常见问题与解决方案
|
2月前
|
数据可视化 Java 测试技术
Java 编程问题:十一、并发-深入探索1
Java 编程问题:十一、并发-深入探索
54 0
|
2月前
|
存储 设计模式 安全
Java 编程问题:十、并发-线程池、可调用对象和同步器2
Java 编程问题:十、并发-线程池、可调用对象和同步器
45 0
|
3天前
|
Java 调度
Java多线程编程与并发控制策略
Java多线程编程与并发控制策略