【多线程与并发】Java中的12个原子操作类
从JDK1.5开始,Java提供了java.util.concurrent.atomic包,该包中的原子操作类提供了一种使用简单、性能高效(使用CAS操作,无需加锁)、线程安全地更新一个变量的方式。

根据变量类型的不同,Atomic包中的这12个原子操作类可以分为4种类型:
①原子更新基本类型:AtomicBoolean、AtomicInteger、AtomicLong
②原子更新数组:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
③原子更新引用:AtomicReference、AtomicReferenceFiledUpdater、AtomicMarkableReference
④原子更新字段(属性):AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicStampedReference
它们都是使用Unsafe实现的包装类。
原子更新基本类型类
使用原子的方式更新基本类型,Atomic包提供了以下3个类:
①AtomicBoolean
②AtomicInteger
③AtomicLong
这三个类提供的方式几乎是一模一样,下面以AtomicInteger为例进行讲解,AtomicInteger的常用的方法如下:
int addAndGet(int delta)
以原子方式将AtomicInteger的value设置为:delta + 原value,返回更新后的值(即delta + 原value)
boolean compareAndSet(int expect, int update)
以原子的方式,如果AtomicInteger的当前值是expect,则将AtomicInteger的值设置为update
int getAndIncrement()
以原子的方式将AtomicInteger的当前值加1,注意:返回的是加1前的值
void lazySet(int newValue)
最终将AtomicInteger设置为newValue(使用lazySet设置值后,其他线程可能在之后的一段时间内还是可以读到旧的值)
int getAndSet(int newValue)
以原子的方式将AtomicInteger设置为newValue
基本使用方式示例:
public void test(){
Executor executor = Executors.newFixedThreadPool(3);
AtomicInteger atomicInteger = new AtomicInteger(0);
for(int i = 0; i < 10; i++){
executor.execute(()->{
System.out.println("atomicInteger的当前值:" + atomicInteger.addAndGet(1));
});
}
}
//输出如下(输出顺序可能不同,但结果一定是正确的)
atomicInteger的当前值:1
atomicInteger的当前值:2
atomicInteger的当前值:4
atomicInteger的当前值:5
atomicInteger的当前值:3
atomicInteger的当前值:7
atomicInteger的当前值:6
atomicInteger的当前值:9
atomicInteger的当前值:8
atomicInteger的当前值:10
其内部实现原子操作的原理是通过UnSafe类的CAS操作。//TODO 具体实现
其他Java的基本类型均可以使用类似的思路实现。
原子更新数组
通过原子的方式更新数组里的某个元素,Atomic包提供了以下3个类:
①AtomicIntegerArray:原子更新整型数组里的元素
②AtomicLongArray:原子更新长整型数组里的元素
③AtomicReferenceArray:原子更新引用类型数组里的元素
以AtomicIntegerArray为例,主要是提供以原子的方式更新数组里的整型元素,其主要方法如下:
int addAndGet(int i, int delta)
以原子的方式将数组中i位置处的元素值加上delta,返回:i位置处的元素的旧值+ delta
boolean compareAndSet(int i, int expect, int update)
如果当前值等于预期值(数组i位置处的元素),则以原子的方式将数组i位置处的元素值设置为update
使用示例:
public void testAtomicIntegerArray() {
int[] originArray = new int[]{1, 2, 3};
AtomicIntegerArray array = new AtomicIntegerArray(originArray);
array.getAndSet(0, 8);
System.out.println(array.get(0));
System.out.println(originArray[0]);
}
//输出结果:
8
1 ----注意这里,构造方法中是将原数组复制了一份,所以对AtomicIntegerArray的操作,不会影响原数组
原子更新引用类型
如果要原子更新多个变量,就需要使用原子更新引用类型,Atomic提供了3个类:
①AtomicReference:原子更新引用类型
②AtomicReferenceFiledUpdater:原子更新引用类型里的字段
③AtomicMarkableReference:原子更新带有标记位的引用类型
以AtomicReference为例,演示代码如下:
class AtomicReferenceExample{
private AtomicReference<User> userAtomicReference = new AtomicReference<>();
@Test
public void test(){
User originUser = new User(18, "小岳");
userAtomicReference.set(originUser);
User updateUser = new User(28, "老岳");
userAtomicReference.compareAndSet(originUser, updateUser);
System.out.println(userAtomicReference.get().getName() + ":" + userAtomicReference.get().getAge());
}
class User{
private String name;
private int age;
public User(int age, String name){
this.name = name;
this.age = age;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
}
}
//输出结果如下
老岳:28
原子更新字段类
如果需要原子地更新某个类里的某个字段,就需要使用原子更新字段类,Atomic包提供了一下3个类进行原子字段更新。
①AtomicIntegerFieldUpdater:原子更新整型的字段
②AtomicLongFieldUpdater:原子更新长整型的字段
③AtomicStampedReference:原子更新带有版本号的引用类型(可以解决CAS操作的ABA问题)
使用更新字段类必须使用静态方法newUpdater(Class<U> tclass, String fieldName)创建一个更新器(同时指定要更新的类和该类中的要更新的字段名),并且该字段必须用public volatile修饰。
以AtomicIntegerFieldUpdater为例,演示代码如下:
class AtomicIntegerFieldUpdaterExample{
private AtomicIntegerFieldUpdater<User> fieldUpdater = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
@Test
public void test(){
User user = new User(18, "小岳");
fieldUpdater.addAndGet(user, 10);
System.out.println("user现在的年龄:" + fieldUpdater.get(user));
}
class User{
private String name;
public volatile int age;
public User(int age, String name){
this.name = name;
this.age = age;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
}
}
//输出结果如下
user现在的年龄:28
理解循环CAS
Java中可以通过两种方式来实现原子操作:加锁和循环CAS。
Java中使用循环CAS实现原子操作的实例:
//原子操作类AtomicInteger的incrementAndGet实现(最新源码已非如此,但思想是一致的)
public final int incrementAndGet() {
for (;;) {//一直循环
int current = get();//取出AtomicInteger当前的内存值
int next = current + 1;//要设置的新值
if (compareAndSet(current, next)){//用CAS操作更新AtomicInteger
return next;
}
}
}
boolean compareAndSet(int current, int newValue)方法是一个原子方法,该方法首先会先检查AtomicInteger的当前数值(从内存中读取,即CAS操作中内存值)是否等于current(即CAS操作中预期值),如果等于,则说明AtomicInteger的值没有被其他线程修改过,则会将AtomicInteger的值设置为newValue(即CAS操作中要修改的新值),并返回true;如果不等于,说明AtomicInteger的值被其他线程修改过,则返回fasle。
CAS实现原子操作的三个问题
①ABA问题。
因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。②循环时间长开销大。
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。③只能保证一个共享变量的原子操作。
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
参考
内容全部来自《Java并发编程的艺术》以及上述书籍作者的聊聊并发(五)——原子操作的实现原理。
作者:maxwellyue
链接:https://www.jianshu.com/p/712681f5aecd
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
【多线程与并发】Java中的12个原子操作类的更多相关文章
- java并发编程基础 --- 7章节 java中的13个原子操作类
当程序更新一个变量时,如果多线程同时更新这个变量,可能得到期望之外的值,比如变量 i=1,A线程更新 i+1,B线程也更新 I+1,经过两个线程的操作之后可能 I不等于3,而是等于2.因为A和B线程更 ...
- 第七章 Java中的13个原子操作类
当程序更新一个变量时,如果多线程同时更新这个变量,可能得到期望之外的值,比如变量i = 1:A线程更新i + 1,B线程也更新i + 1,经过两个线程操作之后可能i不等于3,而是等于2,.因为A和B线 ...
- Java中的13个原子操作类
java.util.concurrent.atomic包一共提供了13个类.属于4种类型的原子更新方式,分别是原子更新基本类型,原子更新数组,原子更新引用和原子更新属性.Atomic包里的类基本都是使 ...
- 聊聊并发-Java中的Copy-On-Write容器
详见: http://blog.yemou.net/article/query/info/tytfjhfascvhzxcytp78 聊聊并发-Java中的Copy-On-Write容器 Cop ...
- Java中必须了解的常用类
1.Java的包装类 基本数据类型我们都很熟悉,例如:int.float.double.boolean.char等,基本数据类型不具备对象的特征,不能调用方法,一般能实现的功能比较简单,为了让基本数据 ...
- Java中使用 Long 表示枚举类
Java中使用 Long 表示枚举类 在日常的开发过程中,很多时候我们需要枚举类(enum)来表示对象的各种状态,并且每个状态往往会关联到指定的数字,如: private enum Color { R ...
- Java中各种集合(字符串类)的线程安全性!!!
Java中各种集合(字符串类)的线程安全性!!! 一.概念: 线程安全:就是当多线程访问时,采用了加锁的机制:即当一个线程访问该类的某个数据时,会对这个数据进行保护,其他线程不能对其访问,直到该线程读 ...
- JAVA基础——Java 中必须了解的常用类
Java中必须了解的常用类 一.包装类 相信各位小伙伴们对基本数据类型都非常熟悉,例如 int.float.double.boolean.char 等.基本数据类型是不具备对象的特性的,比如基本类型不 ...
- Java中是否可以调用一个类中的main方法?
前几天面试的时候,被问到在Java中是否可以调用一个类中的main方法?回来测试了下,答案是可以!代码如下: main1中调用main2的主方法 package org.fiu.test; impor ...
随机推荐
- 用Visual Studio 2015 编写 MASM 汇编程序(二)从头开发一个Win32汇编程序
一,建立一个VC的控制台类型的空工程: 1,从VS菜单中选择“文件”->“新建”->“项目”. 2,在新建项目中选择:“Visual c++”->"Win32"- ...
- 如何在SAP UI5应用里添加使用摄像头拍照的功能
昨天Jerry的文章 纯JavaScript实现的调用设备摄像头并拍照的功能 介绍了纯JavaScript借助WebRTC API来开发支持调用设备的摄像头拍照的web应用.而我同事遇到的实际情况是, ...
- containerd简述
containerd是容器虚拟化技术,从docker中剥离出来,形成开放容器接口(OCI)标准的一部分. docker对容器的管理和操作基本都是通过containerd完成的.Containerd 是 ...
- 【转】高性能网络编程3----TCP消息的接收
这篇文章将试图说明应用程序如何接收网络上发送过来的TCP消息流,由于篇幅所限,暂时忽略ACK报文的回复和接收窗口的滑动. 为了快速掌握本文所要表达的思想,我们可以带着以下问题阅读: 1.应用程序调用r ...
- mysql 5.7 my.cnf配置
此为配置上生产环境的参数,后续补充参数说明 [client] port=3306 socket = /data/mysql/tmp/mysql.sock # default-character-set ...
- Vim编译器的相关知识
Vim编译器相关知识 1.关于Vim编译器 在热门Linux操作系统中都会默认安装一款超好用的文本编辑器——名字叫“vim”,vim是vi编辑器的升级版. vim 具有程序编辑的能力,可以主动的以字体 ...
- 数据库类型对应Java语言类型表
下表列出了基本 SQL Server.JDBC 和 Java 编程语言数据类型之间的默认映射: SQL Server 类型 JDBC 类型 (java.sql.Types) Java 语言类型 big ...
- zabbix监控MySQL状态值获取不到值原因分析
在server端测试键值 [root@zbx-server etc]# zabbix_get -s MySQL-glibc -k "buffer_pool_wait_free" 如 ...
- netty实现websocket发送文本和二进制数据
原文:https://huan1993.iteye.com/blog/2433552 最近在学习netty相关的知识,看到netty可以实现 websoket,因此记录一下在netty中实现webso ...
- SpringCloud学习心得—1.3—Eureka与REST API
SpringCloud学习心得—1.3—Eureka与REST API Eureka的REST API接口 API的基本访问 Eureka REST APIEureka 作为注册中心,其本质是存储 ...