详解java并发原子类AtomicInteger(基于jdk1.8源码分析)

简介: java并发包里面的类一直是学习和面试的重点,这篇文章主要是对java并发包的其中一个类AtomicInteger的讲解。从为什么要出现AtomicInteger再到其底层原理来一个分析。

一、从a++说起为什么使用AtomicInteger


我们知道java并发机制中主要有三个特性需要我们去考虑,原子性、可见性和有序性。volatile关键字可以保证可见性和有序性却无法保证原子性。而这个AtomicInteger的作用就是为了保证原子性。我们先看一个例子。


public class Test {
    //一个变量a
    private static volatile int a = 0;
    public static void main(String[] args) {
        Test test = new Test();
        Thread[] threads = new Thread[5];
        //定义5个线程,每个线程加10
        for (int i = 0; i < 5; i++) {
            threads[i] = new Thread(() -> {
                try {
                    for (int j = 0; j < 10; j++) {
                        System.out.println(a++);
                        Thread.sleep(500);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
            threads[i].start();
        }
    }
}

在上面的这个例子中,我们定义了一个变量a。并且使用了5个线程分别去增加。为了保证可见性和有序性我们使用了volatile关键字对a进行修饰。在这里我们只测试原子性。如果我们第一次接触的话肯定会觉得5个线程,每个线程加10,最后结果一定是50呀。我们可以运行一边测试一波。

v2-2cdc805ca406ef8d61b8a3b7c6161d6d_1440w.jpg

很明显,可能跟你想象的不一样。为什么会出现这个问题呢?这是因为变量a虽然保证了可见性和有序性,但是缺没有保证原子性。其原因我们可以来分析一下。


对于a++的操作,其实可以分解为3个步骤。


(1)从主存中读取a的值

(2)对a进行加1操作

(3)把a重新刷新到主存


这三个步骤在单线程中一点问题都没有,但是到了多线程就出现了问题了。比如说有的线程已经把a进行了加1操作,但是还没来得及重新刷入到主存,其他的线程就重新读取了旧值。因为才造成了错误。如何去解决呢?方法当然很多,但是为了和我们今天的主题对应上,很自然的联想到使用AtomicInteger。下面我们使用AtomicInteger重新来测试一遍:


public class Test3 {
    //使用AtomicInteger定义a
    static AtomicInteger a = new AtomicInteger();
    public static void main(String[] args) {
        Test3 test = new Test3();
        Thread[] threads = new Thread[5];
        for (int i = 0; i < 5; i++) {
            threads[i] = new Thread(() -> {
                try {
                    for (int j = 0; j < 10; j++) {
                        //使用getAndIncrement函数进行自增操作
                        System.out.println(a.incrementAndGet());        
                        Thread.sleep(500);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
            threads[i].start();
        }
    }
}

在上面的代码中我们使用了AtomicInteger来定义a,而且使用了AtomicInteger的函数incrementAndGet来对a进行自增操作。现在我们再来测试一遍。

v2-07c85ebd9632478169bb1f4ade07eef9_1440w.jpg

现在使用了AtomicInteger,不管你测试多少次,最后结果一定是50。为什么会出现这样的结果呢?AtomicInteger又是如何保证了这样的特性呢?下面我们就正式的开始揭开其面纱。


二、原理分析


上面的例子中我们只是调用了incrementAndGet函数来进行自增操作。其实AtomicInteger类为我们提供了很多函数。可以先使用一下。


1、基本使用


public class Test4 {
    private static AtomicInteger atomicInteger = new AtomicInteger();
    //1、获取当前值
    public static void getCurrentValue(){}
    //2、设置value值
    public static void setValue(){}
    //3、先获取旧值,然后设置新值
    public static void getAndSet(){}
    //4、先取得旧值,然后再进行自增
    public static void getAndIncrement(){}
    //5、先获取旧值,然后再减少
    public static void getAndDecrement(){}
    //6、先获取旧值,然后再加10
    public static void getAndAdd(){}
    //7、先加1.然后获取新值
    public static void incrementAndGet(){}
    //8、先减1,然后获取新值
    public static void decrementAndGet(){}  、
    //9、先增加,然后再获取新值
    public static void addAndGet(){}
}

最常用的方法就是这么几个。当然了还有很多其他的方法。对于上面几个函数,每一个函数的意思都已经列了出来。意思都很简单。下面我们就通过源码的角度分析一下AtomicInteger的真正原理。


2、源码分析


既然AtomicInteger使用了incrementAndGet函数,那我们就直接来看这个方法,对于其他的方法也是同样的道理。我们直接看源码,这里使用的是jdk1.8的版本,不同的版本会有出入。


/**
* Atomically increments by one the current value.
* @return the updated value
*/
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

在这里我们会看到,底层使用的是unsafe的getAndAddInt方法。这里你可能有一个疑问了,这个unsafe是个什么鬼,而且还有一个valueOffset参数又是什么,想要看明白,我们从源码的开头开始看起。


public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    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;
    //这里还有更多的代码没有列出    
}

开头在Unsafe的上面会发现,有一行注释叫做Unsafe.compareAndSwapInt。这又是什么?带着这些疑问我们开始一点一点揭开其面纱。


(1)compareAndSwapInt的含义


compareAndSwapInt又叫做CAS,如果你将来找工作,这个不清楚的话,基本上可以告别java这个方向了。


CAS 即比较并替换,实现并发算法时常用到的一种技术。CAS操作包含三个操作数——内存位置、预期原值及新值。执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。


我看过无数篇文章,对这个概念都是这样解释的,但是一开始看会一脸懵逼。我们使用一个例子来解释相信你会更加的清楚。

比如说给你儿子订婚。你儿子就是内存位置,你原本以为你儿子是和杨贵妃在一起了,结果在订婚的时候发现儿子身边是西施。这时候该怎么办呢?你一气之下不做任何操作。如果儿子身边是你预想的杨贵妃,你一看很开心就给他们订婚了,也叫作执行操作。现在你应该明白了吧。

对于CAS的解释我不准备长篇大论讲解。因为里面涉及到的知识点还是挺多的。在这里你理解了其含义就好。


(2)Unsafe的含义


在上面我们主要是讲解了CAS的含义,CAS修饰在Unsafe上面。那这个Unsafe是什么意思呢?


Unsafe是位于sun.misc包下的一个类,Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重。


这里说一句题外话,在jdk1.9中,对Usafe进行了删除,所以因为这,那些基于Usafe开发的框架慢慢的都死掉了。


在这里也就是说,Usafe再进行getAndAddInt的时候,首先是先加1,然后对底层对象的地址做出了更改。这个地址是什么呢?这就是涉及到我们的第三个疑问参数了。


(3)valueOffset的含义


这个valueOffset是long类型的,代表的含义就是对象的地址的偏移量。下面我们重新解释一下这行代码。


unsafe.getAndAddInt(this, valueOffset, 1) + 1。这行代码的含义是,usafe通过getAndAddInt方法,对原先对象的地址进行了加1操作。现在应该明白了。我们return的时候,也是直接返回的最新的值。这一点我们对比另外一个方法incrementAndGet就能看出。


/**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

在这个方法的源代码中我们可以看到最后的+1操作没有了,也就是说,直接返回的是旧地址的值,然后再进行自增操作。如何去拿的地址的偏移量呢?是通过下面这个代码。

static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

OK,到了这一步相信你已经知道了,usafe对a的值使用getAndAddInt方法进行了加1操作。然后返回最新的值。那么这个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;   
}

这段代码的含义也很清晰。底层还是通过compareAndSwapInt这个CAS机制来完成的增加操作,


第一个参数var1表示的是当前对象,也就是a。

第二个参数var2表示的是地址偏移量

第三个参数var3表示的是我们要增加的值,这里表示为1


对于AtomicInteger的原理就是这,主要是通过Usafe的方式来完成的。Usafe又是通过CAS机制来实现的,因此想要弄清整个原子系列的真正实现,就是要搞清楚CAS机制。不过我会在下一章节进行讲解。


3、其他方法


对于其他方法其实也是同样的道理,我们可以给出几个看看。

public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    public final int decrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
    }
    public final int addAndGet(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
    }

我们可以看到底层基本上还是Usafe来实现的。Usafe又是经过CAS实现。


三、总结


对于jdk1.8的并发包来说,底层基本上就是通过Usafe和CAS机制来实现的。有好处也肯定有一个坏处。从好的方面来讲,就是上面AtomicInteger类可以保持其原子性。但是从坏的方面来看,Usafe因为直接操作的底层地址,肯定不是那么安全,而且CAS机制也伴随着大量的问题,比如说有名的ABA问题等等。关于CAS机制,我也会在后续的文章中专门讲解。大家可以先根据那个给儿子订婚的例子有一个基本的认识。

相关文章
|
2月前
|
Java Linux
java基础(3)安装好JDK后使用javac.exe编译java文件、java.exe运行编译好的类
本文介绍了如何在安装JDK后使用`javac.exe`编译Java文件,以及使用`java.exe`运行编译好的类文件。涵盖了JDK的安装、环境变量配置、编写Java程序、使用命令行编译和运行程序的步骤,并提供了解决中文乱码的方法。
60 2
|
14天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
19天前
|
Java 数据库连接 数据库
如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面
本文介绍了如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面。通过合理配置初始连接数、最大连接数和空闲连接超时时间,确保系统性能和稳定性。文章还探讨了同步阻塞、异步回调和信号量等并发控制策略,并提供了异常处理的最佳实践。最后,给出了一个简单的连接池示例代码,并推荐使用成熟的连接池框架(如HikariCP、C3P0)以简化开发。
39 2
|
1月前
|
缓存 Java Maven
java: 警告: 源发行版 11 需要目标发行版 11 无效的目标发行版: 11 jdk版本不符,项目jdk版本为其他版本
如何解决Java项目中因JDK版本不匹配导致的编译错误,包括修改`pom.xml`文件、调整项目结构、设置Maven和JDK版本,以及清理缓存和重启IDEA。
48 1
java: 警告: 源发行版 11 需要目标发行版 11 无效的目标发行版: 11 jdk版本不符,项目jdk版本为其他版本
|
24天前
|
设计模式 Java API
[Java]静态代理与动态代理(基于JDK1.8)
本文介绍了代理模式及其分类,包括静态代理和动态代理。静态代理分为面向接口和面向继承两种形式,分别通过手动创建代理类实现;动态代理则利用反射技术,在运行时动态创建代理对象,分为JDK动态代理和Cglib动态代理。文中通过具体代码示例详细讲解了各种代理模式的实现方式和应用场景。
20 0
[Java]静态代理与动态代理(基于JDK1.8)
|
1月前
|
Java
让星星⭐月亮告诉你,jdk1.8 Java函数式编程示例:Lambda函数/方法引用/4种内建函数式接口(功能性-/消费型/供给型/断言型)
本示例展示了Java中函数式接口的使用,包括自定义和内置的函数式接口。通过方法引用,实现对字符串操作如转换大写、数值转换等,并演示了Function、Consumer、Supplier及Predicate四种主要内置函数式接口的应用。
25 1
|
1月前
|
Java
【编程进阶知识】揭秘Java多线程:并发与顺序编程的奥秘
本文介绍了Java多线程编程的基础,通过对比顺序执行和并发执行的方式,展示了如何使用`run`方法和`start`方法来控制线程的执行模式。文章通过具体示例详细解析了两者的异同及应用场景,帮助读者更好地理解和运用多线程技术。
27 1
|
1月前
|
Java
Java基础之 JDK8 HashMap 源码分析(中间写出与JDK7的区别)
这篇文章详细分析了Java中HashMap的源码,包括JDK8与JDK7的区别、构造函数、put和get方法的实现,以及位运算法的应用,并讨论了JDK8中的优化,如链表转红黑树的阈值和扩容机制。
24 1
|
2月前
|
Java API 容器
JAVA并发编程系列(10)Condition条件队列-并发协作者
本文通过一线大厂面试真题,模拟消费者-生产者的场景,通过简洁的代码演示,帮助读者快速理解并复用。文章还详细解释了Condition与Object.wait()、notify()的区别,并探讨了Condition的核心原理及其实现机制。
|
2月前
|
Oracle Java 关系型数据库
Linux下JDK环境的配置及 bash: /usr/local/java/bin/java: cannot execute binary file: exec format error问题的解决
如果遇到"exec format error"问题,文章建议先检查Linux操作系统是32位还是64位,并确保安装了与系统匹配的JDK版本。如果系统是64位的,但出现了错误,可能是因为下载了错误的JDK版本。文章提供了一个链接,指向Oracle官网上的JDK 17 Linux版本下载页面,并附有截图说明。
Linux下JDK环境的配置及 bash: /usr/local/java/bin/java: cannot execute binary file: exec format error问题的解决