10、java中的原子类AtomicInteger详解(基于jdk1

  • Post author:
  • Post category:java


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


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

我们知道java并发机制中主要有三个特性需要我们去考虑,原子性、可见性和有序性。synchronized关键字可以保证可见性和有序性却无法保证原子性。而这个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个线程分别去增加。为了保证可见性和有序性我们使用了synchronized关键字对a进行修饰。在这里我们只测试原子性。如果我们第一次接触的话肯定会觉得5个线程,每个线程加10,最后结果一定是50呀。我们可以运行一边测试一波。

在这里插入图片描述

很明显,可能跟你想象的不一样。为什么会出现这个问题呢?这是因为变量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进行自增操作。现在我们再来测试一遍。

在这里插入图片描述

现在使用了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机制,我也会在后续的文章中专门讲解。大家可以先根据那个给儿子订婚的例子有一个基本的认识。

今天的文章先写到这,感谢各位老铁的支持。


三、总结

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

今天的文章先写到这,感谢各位老铁的支持。

在这里插入图片描述



版权声明:本文为SDDDLLL原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。