Java原子操作类,你知道多少?
原子操作类简介
由于synchronized是采用的是悲观锁策略,并不是特别高效的一种解决方案。 实际上,在J.U.C下的atomic包提供了一系列的操作简单,性能高效,并能保证线程安全的类去 更新基本类型变量,数组元素,引用类型以及更新对象中的字段类型。 atomic包下的这些类都是采用的是乐观锁策略去原子更新数据,在Java中则是使用CAS操作具体实现。
CAS
随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略: 先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。 这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。 乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。 硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。 CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。 当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。
//著名的CAS
//var1是比较值所属的对象,var2需要比较的值(但实际是使用地址偏移量来实现的),
//如果var1对象中偏移量为var2处的值等于var4,那么将该处的值设置为var5并返回true,如果不等于var4则返回false。
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
- CAS的问题
1.ABA问题
如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。
J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题, 它可以通过控制变量值的版本来保证 CAS 的正确性。 大部分情况下 ABA 问题不会影响程序并发的正确性, 如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。
2.自旋时间长开销大
自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用, 第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源, 延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。 第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation) 而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
3.只能保证一个共享变量的原子操作 CAS只对单个共享变量有效,当操作涉及跨多个共享变量时CAS无效。 但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性, 可以把多个变量封装成对象里来进行 CAS 操作. 所以我们可以使用锁或者利用AtomicReference类把多个共享变量封装成一个共享变量来操作。
- synchronized VS CAS
元老级的synchronized(未优化前)最主要的问题是: 在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。 而CAS并不是武断的将线程挂起,当CAS操作失败后会进行一定的尝试,而不是进行耗时的挂起唤醒的操作, 因此也叫做非阻塞同步。这是两者主要的区别。
原子更新基本类
atomic包提高原子更新基本类的工具类,如下:
AtomicBoolean //以原子更新的方式更新Boolean
AtomicIntege //以原子更新的方式更新Integer
AtomicLong //以原子更新的方式更新Long
- 以AtomicInteger为例总结常用的方法:
addAndGet(int delta) //以原子方式将输入的数值与实例中原本的值相加,并返回最后的结果
incrementAndGet() //以原子的方式将实例中的原值进行加1操作,并返回最终相加后的结果
getAndSet(int newValue) //将实例中的值更新为新值,并返回旧值
getAndIncrement() //以原子的方式将实例中的原值加1,返回的是自增前的旧值
AtomicInteger的getAndIncrement()方法源码如下:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
实际上是调用了unsafe实例的getAndAddInt方法,unsafe实例的获取时通过UnSafe类的静态方法getUnsafe获取:
private static final Unsafe unsafe = Unsafe.getUnsafe();
public class AtomicIntegerDemo {
// 请求总数
public static int clientTotal = 5000; // 同时并发执行的线程数
public static int threadTotal = 200; //java.util.concurrent.atomic.AtomicInteger;
public static AtomicInteger count = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
//Semaphore和CountDownLatch模拟并发
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (Exception e) {
e.printStackTrace();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println("count:{"+count.get()+"}");
} public static void add() {
count.incrementAndGet();
}
}
输出结果:
count:{5000}
AtomicLong的实现原理和AtomicInteger一致,只不过一个针对的是long变量,一个针对的是int变量。 而boolean变量的更新类AtomicBoolean类是怎样实现更新的呢?核心方法是compareAndSet()方法,其源码如下:
public final boolean compareAndSet(boolean expect, boolean update) {
int e = expect ? 1 : 0;
int u = update ? 1 : 0;
return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}
可以看出,compareAndSet方法的实际上也是先转换成0,1的整型变量, 然后是通过针对int型变量的原子更新方法compareAndSwapInt来实现的。 可以看出atomic包中只提供了对boolean,int ,long这三种基本类型的原子更新的方法, 参考对boolean更新的方式,原子更新char,doule,float也可以采用类似的思路进行实现。
原子更新数组
atomic包下提供能原子更新数组中元素的类有:
AtomicIntegerArray //原子更新整型数组中的元素
AtomicLongArray //原子更新长整型数组中的元素
AtomicReferenceArray //原子更新引用类型数组中的元素
这几个类的用法一致,就以AtomicIntegerArray来总结下常用的方法:
getAndAdd(int i, int delta) //以原子更新的方式将数组中索引为i的元素与输入值相加
getAndIncrement(int i) //以原子更新的方式将数组中索引为i的元素自增加1
compareAndSet(int i, int expect, int update) //将数组中索引为i的位置的元素进行更新
可以看出,AtomicIntegerArray与AtomicInteger的方法基本一致, 只不过在AtomicIntegerArray的方法中会多一个指定数组索引位i。
public class AtomicIntegerArrayDemo {
private static int[] value = new int[]{1, 2, 3};
private static AtomicIntegerArray integerArray = new AtomicIntegerArray(value); public static void main(String[] args) {
//对数组中索引为1的位置的元素加5
int result = integerArray.getAndAdd(1, 5);
System.out.println(integerArray.get(1));
System.out.println(result);
}
}
输出结果:
7
2
原子更新引用类型
如果需要原子更新引用类型变量的话,为了保证线程安全,atomic也提供了相关的类:
AtomicReference //原子更新引用类型
AtomicReferenceFieldUpdater //原子更新引用类型里的字段
AtomicMarkableReference //原子更新带有标记位的引用类型
这几个类的使用方法也是基本一样的,以AtomicReference为例。
public class AtomicReferenceDemo { private static AtomicReference<User> reference = new AtomicReference<>(); public static void main(String[] args) {
User user1 = new User("a", 1);
reference.set(user1);
User user2 = new User("b",2);
User user = reference.getAndSet(user2);
System.out.println(user);
System.out.println(reference.get());
} static class User {
private String userName;
private int age; public User(String userName, int age) {
this.userName = userName;
this.age = age;
} @Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", age=" + age +
'}';
}
}
}
输出结果:
User{userName='a', age=1}
User{userName='b', age=2}
首先将对象User1用AtomicReference进行封装,然后调用getAndSet方法, 从结果可以看出,该方法会原子更新引用的user对象, 变为User{userName='b', age=2},返回的是原来的user对象User{userName='a', age=1}。
原子更新字段类型
如果需要更新对象的某个字段,并在多线程的情况下,能够保证线程安全,atomic同样也提供了相应的原子操作类:
AtomicIntegeFieldUpdater //原子更新整型字段类
AtomicLongFieldUpdater //原子更新长整型字段类
AtomicStampedReference //原子更新引用类型,这种更新方式会带有版本号。
// 而为什么在更新的时候会带有版本号,是为了解决CAS的ABA问题;
要想使用原子更新字段需要两步操作:
- 原子更新字段类都是抽象类,只能通过静态方法newUpdater来创建一个更新器,并且需要设置想要更新的类和属性
- 更新类的属性必须使用public volatile进行修饰
这几个类提供的方法基本一致,以AtomicIntegerFieldUpdater为例。
public class AtomicIntegerFieldUpdaterDemo {
private static AtomicIntegerFieldUpdater updater =
AtomicIntegerFieldUpdater.newUpdater(User.class,"age"); public static void main(String[] args) {
User user = new User("a", 1);
int oldValue = updater.getAndAdd(user, 5);
System.out.println(oldValue);
System.out.println(updater.get(user));
} static class User {
private String userName;
public volatile int age; public User(String userName, int age) {
this.userName = userName;
this.age = age;
} @Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", age=" + age +
'}';
}
}
}
输出结果:
1
6
创建AtomicIntegerFieldUpdater是通过它提供的静态方法进行创建, getAndAdd方法会将指定的字段加上输入的值,并且返回相加之前的值。 user对象中age字段原值为1,加5之后,可以看出user对象中的age字段的值已经变成了6。
免费Java高级资料需要自己领取,涵盖了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高并发分布式等教程,一共30G。
传送门:https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q
Java原子操作类,你知道多少?的更多相关文章
- Java 并发系列之九:java 原子操作类Atomic(13个)
1. 原子更新基本类型类 2. 原子更新数组 3. 原子更新引用 4. 原子更新属性 5. txt java 原子操作类Atomic 概述 java.util.concurrent.atomic里的原 ...
- Java原子操作类AtomicInteger应用场景
Java中有那么一些类,是以Atomic开头的.这一系列的类我们称之为原子操作类.以最简单的类AtomicInteger为例.它相当于一个int变量,我们执行Int的 i++ 的时候并不是一个原子操作 ...
- Java原子操作类汇总
当程序更新一个变量时,如果是多线程同时更新这个变量,可能得到的结果与期望值不同.比如:有一个变量i,A线程执行i+1,B线程也执行i+1,经过两个线程的操作后,变量i的值可能不是期望的3,而是2.这是 ...
- Java原子操作类汇总(2)
当程序更新一个变量时,如果是多线程同时更新这个变量,可能得到的结果与期望值不同.比如:有一个变量i,A线程执行i+1,B线程也执行i+1,经过两个线程的操作后,变量i的值可能不是期望的3,而是2.这是 ...
- JAVA 原子操作类
上文中,guava代码中就用到了,在这里再专门捋一下 部分内容源自: https://www.jianshu.com/p/712681f5aecd https://www.yiibai.com/jav ...
- 【Java多线程】Java 原子操作类API(以AtomicInteger为例)
1.java.util.concurrent.atomic 的包里有AtomicBoolean, AtomicInteger,AtomicLong,AtomicLongArray, AtomicRef ...
- java中的原子操作类AtomicInteger及其实现原理
/** * 一,AtomicInteger 是如何实现原子操作的呢? * * 我们先来看一下getAndIncrement的源代码: * public final int getAndIncremen ...
- java并发编程基础 --- 7章节 java中的13个原子操作类
当程序更新一个变量时,如果多线程同时更新这个变量,可能得到期望之外的值,比如变量 i=1,A线程更新 i+1,B线程也更新 I+1,经过两个线程的操作之后可能 I不等于3,而是等于2.因为A和B线程更 ...
- 并发之java.util.concurrent.atomic原子操作类包
15.JDK1.8的Java.util.concurrent.atomic包小结 14.Java中Atomic包的原理和分析 13.java.util.concurrent.atomic原子操作类包 ...
随机推荐
- 常用Style
有些输入框什么的,字数限制什么的style,ceb为我们写好了.我感觉,每个app的style都是很有用的一个东西. <?xml version="1.0" encoding ...
- STL学习笔记5--map and multimap
Maps是一种关联式容器,包含“关键字/值”对. Multimaps和maps很相似,但是MultiMaps允许重复的元素. 简单介绍: 1.声明,首先包含头文件 “map” map <int, ...
- vim中插入递增数
假设生成0-9的递增数 1.插入数字1,yy复制,9p 2.输入命令 let i= | g//s//\=i/ | let i=i+1 3.结果:
- Leetcode 581.最短无序连续子数组
最短无序连续子数组 给定一个整数数组,你需要寻找一个连续的子数组,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序. 你找到的子数组应是最短的,请输出它的长度. 示例 1: 输入: [2, ...
- 系统中同时安装sql2005 和 sql2008 R2 提示要删除SQL Server 2005 Express
修改注册表:HKLM\Software\Microsoft\Microsoft SQL Server\90\Tools\ShellSEM,把 ShellSEM重命名即可 如果是64位机器 在 HKL ...
- [oldboy-django][2深入django]学生管理(Form)-- 编辑(设置input标签属性,设置input标签默认显示值,设置input的类型)
1 django 后台实现设置input标签属性,设置input标签默认显示值,设置input输入框类型 # Form生成html标签 a. 通过Form生成Input输入框,Form标签,以及sub ...
- HDU3977 Evil teacher 求fib数列模p的最小循环节
In the math class, the evil teacher gave you one unprecedented problem! Here f(n) is the n-th fibona ...
- asp.net允许跨域访问
C# ASP.NET MVC 配置允许跨域访问 在web.config文件中的 system.webServer 节点下 增加如下配置 <httpProtocol> <customH ...
- rsync 使用小记
工作中遇到了有关rsync使用的问题,在这里记录下供有同样需求的人参考一下 先说下环境 服务端配置 pid file = /rsyncdata/rsyncd.pid port = 873 addres ...
- java中json依赖包
Exception in thread "main" java.lang.NoClassDefFoundError: org/jaxen/JaxenException 上面的是do ...