Java多线程之原子操作类

在并发编程中很容易出现并发安全问题,最简单的例子就是多线程更新变量i=1,多个线程执行i++操作,就有可能获取不到正确的值,而这个问题,最常用的方法是通过Synchronized进行控制来达到线程安全的目的。但是由于synchronized是采用的是悲观锁策略,并不是特别高效的一种解决方案。实际上,在J.U.C下的Atomic包提供了一系列的操作简单,性能高效,并能保证线程安全的类去更新多种类型。Atomic包下的这些类都是采用乐观锁策略CAS来更新数据。

CAS原理与问题

CAS操作(又称为无锁操作)是一种乐观锁策略。它假设所有线程访问共享资源的时候不会出现冲突,因此不会阻塞其他线程的操作。那么,如果出现冲突了怎么办?无锁操作是使用CAS(compare and swap)来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。

CAS的操作过程

举例说明:
Atomic包中的AtomicInteger类,是通过Unsafe类下的native函数compareAndSwapInt自旋来保证原子性,
其中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;
    }

CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
可见只有自旋实现更新数据操作之后,while循环才能够结束。

CAS的问题

  1. 自旋时间过长。由compareAndSwapInt函数可知,自旋时间过长会对性能是很大的消耗。
  2. ABA问题。因为CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。解决方案可以添加一个版本号可以解决。原来的变化路径A->B->A就变成了1A->2B->3C,或使用AtomicStampedReference工具类。

Atomic包的使用

原子更新基本类型

Atomic包中原子更新基本类型的工具类:
AtomicBoolean:以原子更新的方式更新boolean;
AtomicInteger:以原子更新的方式更新Integer;
AtomicLong:以原子更新的方式更新Long;

这几个类的用法基本一致,这里以AtomicInteger为例总结常用的方法

  1. addAndGet(int delta):以原子方式将输入的数值与实例中原本的值相加,并返回最后的结果;
  2. incrementAndGet() :以原子的方式将实例中的原值进行加1操作,并返回最终相加后的结果;
  3. getAndSet(int newValue):将实例中的值更新为新值,并返回旧值;
  4. getAndIncrement():以原子的方式将实例中的原值加1,返回的是自增前的旧值;

原理不再赘述,参考上文compareAndSwapInt函数。

AtomicInteger使用示例:

public class AtomicExample {

    private static AtomicInteger atomicInteger = new AtomicInteger(2);

    public static void main(String[] args) {
        System.out.println(atomicInteger.getAndIncrement());
        System.out.println(atomicInteger.incrementAndGet());
        System.out.println(atomicInteger.get());
    }
}
// 2 4 4

LongAdder

为了解决自旋导致的性能问题,JDK8在Atomic包中推出了LongAdder类。LongAdder采用的方法是,共享热点数据分离的计数:将一个数字的值拆分为一个数组。不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多;要得到这个数字的话,就要把这个值加起来。相比AtomicLong,并发量大大提高。

优点:有很高性能的并发写的能力
缺点:读取的性能不是很高效,而且如果读取的时候出现并发写的话,结果可能不是正确的

原子更新数组类型

Atomic包中提供能原子更新数组中元素的工具类:
AtomicIntegerArray:原子更新整型数组中的元素;
AtomicLongArray:原子更新长整型数组中的元素;
AtomicReferenceArray:原子更新引用类型数组中的元素

这几个类的用法一致,就以AtomicIntegerArray来总结下常用的方法:

  1. addAndGet(int i, int delta):以原子更新的方式将数组中索引为i的元素与输入值相加;
  2. getAndIncrement(int i):以原子更新的方式将数组中索引为i的元素自增加1;
  3. compareAndSet(int i, int expect, int update):将数组中索引为i的位置的元素进行更新

AtomicIntegerArray与AtomicInteger的方法基本一致,只不过在前者的方法中会多一个指定数组索引位i。

AtomicIntegerArray使用示例:

public class AtomicExample {

    private static int[] value = new int[]{1, 2, 3};
    private static AtomicIntegerArray integerArray = new AtomicIntegerArray(value);

    public static void main(String[] args) {
        //对数组中索引为2的位置的元素加3
        int result = integerArray.getAndAdd(2, 3);
        System.out.println(integerArray.get(2));
        System.out.println(result);
    }
}
// 6 3

原子更新引用类型

如果需要原子更新引用类型变量的话,为了保证线程安全,Atomic也提供了相关的类:

  1. AtomicReference:原子更新引用类型;
  2. AtomicReferenceFieldUpdater:原子更新引用类型里的字段;
  3. AtomicMarkableReference:原子更新带有标记位的引用类型;

AtomicReference使用示例:

public class AtomicExample {

    private static AtomicReference<User> reference = new AtomicReference<>();

    public static void main(String[] args) {
        User user1 = new User("a", 1);
        reference.set(user1);
        User user2 = new User("b",2);
        User user = reference.getAndSet(user2);
        System.out.println(user);
        System.out.println(reference.get());
    }

    static class User {
        private String userName;
        private int age;

        public User(String userName, int age) {
            this.userName = userName;
            this.age = age;
        }

        @Override
        public String toString() {
            return "User{" +
                    "userName='" + userName + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
}
// User{userName='a', age=1}
// User{userName='b', age=2}

AtomicReferenceFieldUpdater使用示例:

public class AtomicExample {

    public static void main(String[] args) {
        AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Dog.class, String.class, "name");
        Dog dog1 = new Dog();
        updater.compareAndSet(dog1, dog1.name, "cat");
        System.out.println(dog1.name);
    }
}

class Dog {
    volatile String name = "dog1";
}

原子更新字段类型

如果需要更新对象的某个字段,Atomic同样也提供了相应的原子操作类:

  1. AtomicIntegeFieldUpdater:原子更新整型字段类;
  2. AtomicLongFieldUpdater:原子更新长整型字段类;

要想使用原子更新字段需要两步操作:
原子更新字段类型类都是抽象类,只能通过静态方法newUpdater来创建一个更新器,并且需要设置想要更新的类和属性;
更新类的属性必须使用public volatile进行修饰;

AtomicIntegerFieldUpdater使用示例:

public class AtomicExample {

    private static AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");

    public static void main(String[] args) {
        User user = new User("a", 1);
        System.out.println(updater.getAndAdd(user, 5));
        System.out.println(updater.addAndGet(user, 1));
        System.out.println(updater.get(user));
    }

    static class User {
        private String userName;
        public volatile int age;

        public User(String userName, int age) {
            this.userName = userName;
            this.age = age;
        }

        @Override
        public String toString() {
            return "User{" +
                    "userName='" + userName + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
}

解决CAS的ABA问题

AtomicStampedReference:原子更新引用类型,这种更新方式会带有版本号,从而解决CAS的ABA问题

AtomicStampedReference使用示例:

public class AtomicExample {

    public static void main(String[] args) {
        Integer init1 = 1110;
//        Integer init2 = 126;
        AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(init1, 1);
        int curent1 = reference.getReference();
//        Integer current2 = reference.getReference();
        reference.compareAndSet(reference.getReference(), reference.getReference() + 1, reference.getStamp(), reference.getStamp() + 1);//正确写法
//        reference.compareAndSet(current2, current2+1, reference.getStamp(), reference.getStamp() + 1);//正确写法
//        reference.compareAndSet(1110, 1111, reference.getStamp(), reference.getStamp() + 1);//错误写法
//        reference.compareAndSet(curent1, curent1+1, reference.getStamp(), reference.getStamp() + 1);//错误写法
//        reference.compareAndSet(current2, current2 + 1, reference.getStamp(), reference.getStamp() + 1);
        System.out.println("reference.getReference() = " + reference.getReference());
    }
}

AtomicStampedReference踩过的坑

参考上面的代码,分享一个笔者遇到的一次坑。AtomicStampedReference的compareAndSet函数中,前两个参数是使用包装类的。所以当参数超过128时,而且传入参数并不是reference.getReference()获取的话,会导致expectedReference == current.reference为false,则无法进行更新。

public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

最后,限于笔者经验水平有限,欢迎读者就文中的观点提出宝贵的建议和意见。如果想获得更多的学习资源或者想和更多的是技术爱好者一起交流,可以关注我的公众号『全菜工程师小辉』后台回复关键词领取学习资料、进入前后端技术交流群和程序员副业群。同时也可以加入程序员副业群Q群:735764906?一起交流。

原文地址:https://www.cnblogs.com/mseddl/p/11541248.html

时间: 2024-10-08 12:04:59

Java多线程之原子操作类的相关文章

java中的原子操作类AtomicInteger及其实现原理

/** * 一,AtomicInteger 是如何实现原子操作的呢? * * 我们先来看一下getAndIncrement的源代码: * public final int getAndIncrement() { * for (;;) { * int current = get(); // 取得AtomicInteger里存储的数值 * int next = current + 1; // 加1 * if (compareAndSet(current, next)) // 调用compareAnd

Java中的原子操作类

转载: <ava并发编程的艺术>第7章 当程序更新一个变量时,如果多线程同时更新这个变量,可能得到期望之外的值,比如变量i=1,A线程更新i+1,B线程也更新i+1,经过两个线程操作之后可能i不等于3,而是等于2.因为A和B线程在更新变量i的时候拿到的i都是1,这就是线程不安全的更新操作,通常我们会使用synchronized来解决这个问题,synchronized会保证多线程不会同时更新变量i. 而Java从JDK 1.5开始提供了java.util.concurrent.atomic包(以

Java多线程01(Thread类、线程创建、线程池)

Java多线程(Thread类.线程创建.线程池) 第一章 多线程 1.1 多线程介绍 1.1.1 基本概念 进程:进程指正在运行的程序.确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能. 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程.一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序. 简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程 1.1.2 单线程程序 - 从入口m

Java 多线程之 Thread 类 和 Runnable 接口初步使用

Thread 类 Thread 类是在 JDK1.0 时就存在的, 在 Java 中接触的多线程开发基本上都会从这个类开始. Thread之定义线程类 使用 Thread 创建线程的方法很简单, Thread 是一个类, 在需要创建线程时, 我们只需要继承这个类, 并将 run() 方法进行重写即可. class PrimeThread extends Thread { long minPrime; PrimeThread(long minPrime) { this.minPrime = min

Java多线程之~~~Phaser类实现任务的同步

在多线程开发中,经常会碰到将多个任务分配给多个线程,每个线程执行他的任务,但是,每个任务又分为好几个 阶段,每个阶段期望各个线程同时达到,意思是,每一步每个线程都要同步,当有一个线程走完第一步的时候,他得等 待其他的线程都完成第一步了才能继续下一步,步调一致能解决很多问题.下面我们使用一个例子,这个例子是模拟遍 历机器上的一些文件,找出以log结尾的文件,并且他的最后修改时间为24小时以内,我们开启3个线程去完成这个任 务.并且使用Phaser来同步各个任务. package com.bird.

Java多线程之~~~CyclicBarrier 类的使用

上一节说了CountDown的使用方法,对于用来同步多个线程之间的协作关系,Java更提供了更加高级的方法来实 现,这个类就是CyclicBarrier. 它可以实现当多个分支线程完成他们的工作后,调用await方法来等待,然后等所有的分 支线程工作完毕后,会自动的调用主线程的run方法,这个主线程是一个实现runnable接口的类,在CyclicBarrier实例化 的时候就调用了. 下面我们就用代码来说明这个问题.代码实现的效果是从一个二维数组中查询到我们需要的数字,然后分支成五个 线程来分

Java 多线程 (Thread 类)

1.多线程 1.多线程实现 两种方式可以实现多线程: 继承 Thread 类,重写 run 方法:定义对象,调用 start 方法 创建类实现 Runnable 接口,作为实参传递给 thread 的构造方法.定义对象,调用 start 方法. 1.1.继承 Thread 继承类,重写方法 class TDemo1 extends Thread { public String name; // 取个名字,便于识别 public TDemo1 (String name) { // 构造方法 thi

Java多线程同步工具类之CyclicBarrier

一.CyclicBarrier使用 CyclicBarrier从字面上可以直接理解为线程运行的屏障,它可以让一组线程执行到一个共同的屏障点时被阻塞,直到最后一个线程执行到指定位置,你设置的执行线程就会触发运行:同时CyclicBarrier相比与CountDownLatch,它是可以被重置的:下面我们通过一个简单例子看下CyclicBarrier的使用: 实例化一个CyclicBarrier对象并传入你要控制的线程内部: public static void main(String[] args

【多线程与并发】Java中的12个原子操作类

从JDK1.5开始,Java提供了java.util.concurrent.atomic包,该包中的原子操作类提供了一种使用简单.性能高效(使用CAS操作,无需加锁).线程安全地更新一个变量的方式. `java.util.concurrent.atomic`包中的类.png 根据变量类型的不同,Atomic包中的这12个原子操作类可以分为4种类型: ①原子更新基本类型:AtomicBoolean.AtomicInteger.AtomicLong ②原子更新数组:AtomicIntegerArra