【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累计合成,这样,热点就进行了有效的分离,提高了并行度。增强原子类正是利用了这种原理实现,这里不详细介绍了。

相关文章
|
9天前
|
JSON Java Apache
非常实用的Http应用框架,杜绝Java Http 接口对接繁琐编程
UniHttp 是一个声明式的 HTTP 接口对接框架,帮助开发者快速对接第三方 HTTP 接口。通过 @HttpApi 注解定义接口,使用 @GetHttpInterface 和 @PostHttpInterface 等注解配置请求方法和参数。支持自定义代理逻辑、全局请求参数、错误处理和连接池配置,提高代码的内聚性和可读性。
|
10天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
7天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
9天前
|
存储 缓存 安全
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见。本文介绍了使用 `File.createTempFile` 方法和自定义创建临时文件的两种方式,详细探讨了它们的使用场景和注意事项,包括数据缓存、文件上传下载和日志记录等。强调了清理临时文件、确保文件名唯一性和合理设置文件权限的重要性。
23 2
|
10天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
运维 Java 大数据
Java并发JUC(java.util.concurrent)ForkJoin/异步回调
Java并发JUC(java.util.concurrent)ForkJoin/异步回调
Java并发JUC(java.util.concurrent)ForkJoin/异步回调
|
11天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
20天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
7天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
27 9
|
10天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
24 3