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
上一篇: 你的屁不是这味
下一篇: 传统零售失守 最后一个巨头也要卖身了