JUC学习之原子变量

一、简介

在多线程环境下,如果多个线程需要对共享资源同时进行操作的话,很容易出现数据不一致的情况。通常情况下我们会使用synchronized同步方式来保证线程安全。

从JDK 1.5开始提供了java.util.concurrent.atomic包,这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。

在java.util.concurrent.atomic包下面主要有以下一些类:

下面我们以AtomicInteger为例进行详细讲解,其他的原子类原理都类似,具体也可以参考官网https://docs.oracle.com/javase/8/docs/api/index.html进行学习。

二、AtomicInteger

AtomicInteger:一个可以自动更新的整型值,用于描述原子变量的属性。AtomicInteger用于原子递增的计数器等应用程序,不能用作整数的替代。但是,这个类扩展了Number,允许处理基于数字的类的工具和实用程序进行统一访问。

构造方法:

  • AtomicInteger():创建一个初始值为0的新AtomicInteger;
  • AtomicInteger(int initialValue):使用给定的初始值创建一个新的AtomicInteger;

三、AtomicInteger常见API

返回值

方法名称、描述

int

addAndGet(int delta)

原子地将给定值添加到当前值

boolean

compareAndSet(int expect, int update)

如果当前值==期望值,则自动将该值设置为给定的更新值。

返回true表示成功,false表示实际值不等于期望值。

int

decrementAndGet()

原子性地使当前值递减1

double

doubleValue()

在扩展原语转换之后,将此AtomicInteger的值作为双精度值返回

float

floatValue()

在扩展原语转换之后,将此AtomicInteger的值作为浮点数返回

int

get()

获取当前值

int

getAndAccumulate(int x, IntBinaryOperator accumulatorFunction)

使用将给定函数应用于当前值和给定值的结果自动更新当前值,并返回以前的值

int

getAndAdd(int delta)

原子地将给定值添加到当前值

int

getAndDecrement()

原子性地使当前值递减1

int

getAndIncrement()

原子地增加一个当前值

int

getAndSet(int newValue)

自动设置为给定值并返回旧值

int

getAndUpdate(IntUnaryOperator updateFunction)

使用应用给定函数的结果自动更新当前值,并返回以前的值

int

incrementAndGet()

原子地增加一个当前值

int

intValue()

返回这个AtomicInteger的整型值

void

lazySet(int newValue)

最终设置为给定的值

long

longValue()

将此AtomicInteger的值作为扩展原语转换后的long

void

set(int newValue)

设置为给定的值

String

toString()

返回当前值的字符串表示形式

int

updateAndGet(IntUnaryOperator updateFunction)

使用应用给定函数的结果自动更新当前值,并返回更新后的值

boolean

weakCompareAndSet(int expect, int update)

如果当前值==期望值,则自动将该值设置为给定的更新值

四、AtomicInteger示例

下面通过一个简单的多线程操作count++的示例来说明AtomicInteger的用法。

在这之前,我们先看下用synchronized方式实现同步的代码:

public class T14_TestVolatile {

    int count = 0;

    //synchronized既保证了可见性又保证了原子性,而volatile只保证了可见性
    private synchronized void m() {
        for (int i = 0; i < 10000; i++) {
            count++;
        }
    }

    public static void main(String[] args) {
        T14_TestVolatile t02_testVolatile = new T14_TestVolatile();
        List<Thread> threads = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(t02_testVolatile::m, "thread" + i));
        }
        threads.forEach(t -> t.start());
        threads.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(t02_testVolatile.count);
    }
}

运行结果:

count = 100000

我们都知道count++是一个非原子性的操作,下面我们使用AtomicInteger实现原子操作:

public class T15_TestVolatile {
    //原子性操作的Integer
    //但不能保证多个方法连续调用具有原子性
    //指定初始值为0
    AtomicInteger count = new AtomicInteger(0);

    void m() {
        for (int i = 0; i < 10000; i++) {
            //原子操作
            count.incrementAndGet();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        T15_TestVolatile t15_testVolatile = new T15_TestVolatile();
        List<Thread> threads = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(t15_testVolatile::m, "thread" + i));
        }

        threads.forEach(t -> t.start());
        threads.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println("count = " + t15_testVolatile.count);
    }
}

运行结果:

count = 100000

注意:AtomicInteger只能保证一个操作具有原子性,并不能保证多个方法连续调用具有原子性。

五、AtomicInteger源码分析

以上面示例中使用到的count.incrementAndGet()方法为例大概说明一下底层是怎么保证原子性的。

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

public final int decrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
}
//省略....

查看源码,AtomicInteger底层都是

private static final Unsafe unsafe = Unsafe.getUnsafe()

这个unsafe对应的方法来保证原子性的。

然后我们去看一下Unsafe的源码,下面是部分源码

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;
}

继续追踪,发现Unsafe只提供了3种CAS方法:compareAndSwapObject、compareAndSwapInt、compareAndSwapLong:

public final native boolean compareAndSwapObject(Object o,long offset,Object expected,Object x);
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);

以上三个方法都是native本地方法,说明它并不是使用Java来实现的,其底层是使用CAS(Compare-And-Swap)算法来保证原子性的,CAS 是硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问。

下面我们介绍一下CAS算法。

六、CAS算法

CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS包含三个操作数:

  • 一个内存地址V
  • 一个预估值(期望值)A
  • 一个更新值(新值)B

CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为更新值B,否则就什么都不做。整个比较并替换的操作是一个原子操作.

优点:

  • 可以避免优先级倒置和死锁等问题;
  • 允许更高程度的并行机制;

缺点:

CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题:

  • 循环时间长开销很大:

如果CAS失败,会一直进行尝试,如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

  • 只能保证一个共享变量的原子操作:

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。

  • ABA问题:

如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?

如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。

七、总结

本文介绍了JUC并发包中常用的原子变量,并且介绍了其中用到的CAS原理。实际项目中如果在多线程中存在i++或者++i等需要保证原子性操作的时候可以考虑使用原子变量。

参考资料:https://www.cnblogs.com/qjjazry/p/6581568.html

发布了220 篇原创文章 · 获赞 93 · 访问量 15万+

猜你喜欢

转载自blog.csdn.net/Weixiaohuai/article/details/104618668
今日推荐