AtomicInteger的原理

java的并发原子包里面提供了很多可以进行原子操作的类,比如:

  • AtomicInteger
  • AtomicBoolean
  • AtomicLong
  • AtomicReference

等等,一共分为四类:原子更新基本类型(3个)、原子更新数组、原子更新引用和原子更新属性(字段)。、提供这些原子类的目的就是为了解决基本类型操作的非原子性导致在多线程并发情况下引发的问题。那么非原子性的操作会引发什么问题呢?下面我们通过一个示例来看一下。

1. i++引发的问题

我们知道基本类型的赋值操作是原子操作,但是类似这种i++的操作并不是原子操作,通过反编译代码我们可以大致了解此操作分为三个阶段:

tp1 = i;  //1
tp2 = tp1 + 1; //2
i = tp2; //3

如果有两个线程m和n要执行i++操作,因为重排序的影响,代码执行顺序可能会发生改变。如果代码的执行顺序是m1 - m2 - m3 - n1 - n2 - n3,那么结果是没问题的,如果代码的执行顺序是m1 - n1 - m2 - n2 - m3 - n3那么很明显结果就会出错。

测试代码

package com.wangjun.thread;

public class AtomicIntegerTest {

	private static int n = 0;

	public static void main(String[] args) throws InterruptedException {
//i++引发的线程问题
Thread t1 = new Thread() {
public void run() {
for(int i = 0; i < 1000; i++) {
n++;
}
};
};
Thread t2 = new Thread() {
public void run() {
for(int i = 0; i < 1000; i++) {
n++;
}
};
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最终n的值为:" + n);
}
}

如果i++是原子操作,那么结果应该就是2000,反复运行几次发现结果大部分情况下都不是2000,这也证明了i++的非原子性在多线程下产生的问题。当然我们可以通过加锁的方式保证操作的原子性,但本文的重点是使用原子类的解决这个问题。

最终n的值为:1367
---
最终n的值为:1243
---
最终n的值为:1380

2. AtomicInteger的原子操作

上面的问题可以使用AtomicInteger来解决,我们更改一下代码如下:

package com.wangjun.thread;

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerTest {

	private static AtomicInteger n2 = new AtomicInteger(0);

	public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
public void run() {
for(int i = 0; i < 1000; i++) {
n2.incrementAndGet();
}
};
};
Thread t2 = new Thread() {
public void run() {
for(int i = 0; i< 1000; i++) {
n2.incrementAndGet();
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最终n2的值为:" + n2.toString());
}
}

多次运行,发现结果永远是2000,由此可以证明AtomicInteger的操作是原子性的。

最终n2的值为:2000

那么AtomicInteger是通过什么机制来保证原子性的呢?接下来,我们对源码进行一下分析。

3. AtomicInteger源码分析

构造函数

private volatile int value;
/*
* AtomicInteger内部声明了一个volatile修饰的变量value用来保存实际值
* 使用带参的构造函数会将入参赋值给value,无参构造器value默认值为0
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}

自增函数

import sun.misc.Unsafe;
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); }
}
/*
* 可以看到自增函数中调用了Unsafe函数的getAndAddInt方法
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

那么这个getAndAddInt方法是干嘛的呢,首先来了解一下Unsafe这个类。

Unsafe类是在sun.misc包下,不属于Java标准。但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe类在提升Java运行效率,增强Java语言底层操作能力方面起了很大的作用。
Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。
通常我们最好也不要使用Unsafe类,除非有明确的目的,并且也要对它有深入的了解才行。

再来说Unsafe的getAndAddInt,通过反编译可以看到实现代码:

/*
* 其中getIntVolatile和compareAndSwapInt都是native方法
* getIntVolatile是获取当前的期望值
* compareAndSwapInt就是我们平时说的CAS(compare and swap),通过比较如果内存区的值没有改变,那么就用新值直接给该内存区赋值
*/
public final int getAndAddInt(Object paramObject, long paramLong, int paramInt)
{
int i;
do
{
i = getIntVolatile(paramObject, paramLong);
} while (!compareAndSwapInt(paramObject, paramLong, i, i + paramInt));
return i;
}

incrementAndGet是将自增后的值返回,还有一个方法getAndIncrement是将自增前的值返回,分别对应++ii++操作。同样的decrementAndGet和getAndDecrement则对--ii--操作。

4. CAS中ABA问题的解决

CAS也并非完美的,它会导致ABA问题,就是说,当前内存的值一开始是A,被另外一个线程先改为B然后再改为A,那么当前线程访问的时候发现是A,则认为它没有被其他线程访问过。在某些场景下这样是存在错误风险的。比如在链表中。

那么如何解决这个ABA问题呢,大多数情况下乐观锁的实现都会通过引入一个版本号标记这个对象,每次修改版本号都会变话,比如使用时间戳作为版本号,这样就可以很好的解决ABA问题。

在JDK中提供了AtomicStampedReference类来解决这个问题,思路是一样的。这个类也维护了一个int类型的标记stamp,每次更新数据的时候顺带更新一下stamp。

下面我们通过代码演示来看一下AtomicStampedReference的使用:

package com.wangjun.thread;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference; public class ABA { // 普通的原子类,存在ABA问题
AtomicInteger a1 = new AtomicInteger(10);
// 带有时间戳的原子类,不存在ABA问题,第二个参数就是默认时间戳,这里指定为0
AtomicStampedReference<Integer> a2 = new AtomicStampedReference<Integer>(10, 0); public static void main(String[] args) {
ABA a = new ABA();
a.test();
} public void test() {
new Thread1().start();
new Thread2().start();
new Thread3().start();
new Thread4().start();
} class Thread1 extends Thread {
@Override
public void run() {
a1.compareAndSet(10, 11);
a1.compareAndSet(11, 10);
}
}
class Thread2 extends Thread {
@Override
public void run() {
try {
Thread.sleep(200); // 睡0.2秒,给线程1时间做ABA操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("AtomicInteger原子操作:" + a1.compareAndSet(10, 11));
}
}
class Thread3 extends Thread {
@Override
public void run() {
try {
Thread.sleep(500); // 睡0.5秒,保证线程4先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
int stamp = a2.getStamp();
a2.compareAndSet(10, 11, stamp, stamp + 1);
stamp = a2.getStamp();
a2.compareAndSet(11, 10, stamp, stamp + 1);
}
}
class Thread4 extends Thread {
@Override
public void run() {
int stamp = a2.getStamp();
try {
Thread.sleep(1000); // 睡一秒,给线程3时间做ABA操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("AtomicStampedReference原子操作:" + a2.compareAndSet(10, 11, stamp, stamp + 1));
}
}
}

可以看到使用AtomicStampedReference进行compareAndSet的时候,除了要验证数据,还要验证时间戳。

如果数据一样,但是时间戳不一样,那么这个数据其实也被修改过了。

参考:

java的Unsafe类:https://www.cnblogs.com/pkufork/p/java_unsafe.html

Java CAS 和ABA问题https://www.cnblogs.com/549294286/p/3766717.html

AtomicInteger原理的更多相关文章

  1. AtomicInteger原理&源码分析

    转自https://www.cnblogs.com/rever/p/8215743.html 深入解析Java AtomicInteger原子类型 在进行并发编程的时候我们需要确保程序在被多个线程并发 ...

  2. Unsafe与CAS

    Unsafe 简单讲一下这个类.Java无法直接访问底层操作系统,而是通过本地(native)方法来访问.不过尽管如此,JVM还是开了一个后门,JDK中有一个类Unsafe,它提供了硬件级别的原子操作 ...

  3. 什么是CAS机制?(转)

    围绕下面四个点展开叙述: 一:什么是CAS机制? 二:Java当中CAS的底层实现 三:CAS的ABA问题和解决方法 四:java8对CAS的优化 一:什么是CAS机制? 我们先看一段代码: 启动两个 ...

  4. 【重学Java】多线程进阶(线程池、原子性、并发工具类)

    线程池 线程状态介绍 当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态.线程对象在不同的时期有不同的状态.那么Java中的线程存在哪几种状态呢?Java中的线程 状态被定 ...

  5. 对Java原子类AtomicInteger实现原理的一点总结

    java原子类不多,包路径位于:java.util.concurrent.atomic,大致有如下的类: java.util.concurrent.atomic.AtomicBoolean java. ...

  6. Java原子类AtomicInteger实现原理的一点总结

    java原子类不多,包路径位于:java.util.concurrent.atomic,大致有如下的类: java.util.concurrent.atomic.AtomicBoolean java. ...

  7. java的原子类 AtomicInteger 实现原理是什么?

    采用硬件提供原子操作指令实现的,即CAS.每次调用都会先判断预期的值是否符合,才进行写操作,保证数据安全. CAS机制 CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换. ...

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

    /** * 一,AtomicInteger 是如何实现原子操作的呢? * * 我们先来看一下getAndIncrement的源代码: * public final int getAndIncremen ...

  9. Java并发集合的实现原理

    本文简要介绍Java并发编程方面常用的类和集合,并介绍下其实现原理. AtomicInteger 可以用原子方式更新int值.类 AtomicBoolean.AtomicInteger.AtomicL ...

随机推荐

  1. java封装数据类型——Integer

    今天来学习整型 int 的封装数据类型,Integer. 1. 定义 首先来看看定义.可以看到,Integer 继承 Number 抽象类,实现了 Comparable 接口.Number 类是常用数 ...

  2. Go part 5 结构体,方法与接收器

    结构体 结构体定义 结构体的定义只是一种内存布局的描述(相当于是一个模板),只有当结构体实例化时,才会真正分配内存空间 结构体是一种复合的基本类型,通过关键字 type 定义为 自定义 类型后,使结构 ...

  3. bat实现监控进程守护程序-保证平台服务的稳定执行

    背景是平台所在的服务器经常因异常原因导致当前机器所在的服务关闭....直接贴代码吧. ********************************************************* ...

  4. navicat远程连接mysql错误

    确保端口与服务正常. 如果都正常,那么服务有可能没有添加远程访问. 本人链接报错10060,那么问题出在阿里云的主机默认不开放3306端口,那么在安全组策略中加入对应的端口   登录后输入 use m ...

  5. C和指针--预处理器

    编译一个C程序的第1个步骤是预处理(preprocessing)阶段.C预处理器在源代码编译之前对其进行一些文本性质的操作.它的主要任务包括删除注释.插入被#include指令包含的文件的内容.定义和 ...

  6. fwrite、write、fread、read

    1. write和read 1.1 write: 头文件:#include<unistd.h> 原型: ssize_t write(int fd,const void*buf,size_t ...

  7. 9.jsonp的实现原理

    1. 由于浏览器的安全性限制,不允许AJAX访问 协议不同.域名不同.端口号不同的数据接口,浏览器认为这种访问不安全:2. 以通过动态创建script标签的形式,把script标签的src属性,指向数 ...

  8. mongoDB的基本操作之数据更新多条数据

    在默认情况下,update会更新第一条找到的数据,我们做个实验,插入3条c为1的数据 db.test_collection.insert({c:1}) 然后我们find的一下 db.test_coll ...

  9. WCF 使用Stream模式进行文件上传 --节选自Packt.Net.Framework.4.5.Expert.Programming.Cookbook

    使用Stream上传文件 文件上传功能是web程序/服务上常用和必须的功能,WCF也不例外.在4.0版本之前,WCF仅仅提供了buffered模式上传文件.从4.0版本之后,WCF开始提供了Strea ...

  10. [转载]Java 应用性能调优实践

    Java 应用性能调优实践 Java 应用性能优化是一个老生常谈的话题,笔者根据个人经验,将 Java 性能优化分为 4 个层级:应用层.数据库层.框架层.JVM 层.通过介绍 Java 性能诊断工具 ...