随意看看AtomicInteger类和CAS
最近在读jdk源码,怎么说呢?感觉收获还行,比看框架源码舒服多了,一些以前就感觉很模糊的概念和一些类的用法也清楚了好多,举个很简单的例子,我在读Integer类的时候,发现了原来这个类自带缓存,看看如下代码:
package com.wyq.test; public class TestIntegerCache {
public static void main(String[] args) {
String str1 = new String("127");
String str2 = new String("127");
Integer int1 = Integer.valueOf(str1);
Integer int2 = Integer.valueOf(str2); System.out.println(str1 == str2);//false
System.out.println(int1 == int2);//true //-------------------------分割线--------------------------------------- String str3 = new String("128");
String str4 = new String("128");
Integer int3 = Integer.valueOf(str3);
Integer int4 = Integer.valueOf(str4); System.out.println(str3 == str4);//false
System.out.println(int3 == int4);//false }
}
为什么127和128的结果不一样呢?因为Integer的内部维护了一个缓存,就是从jvm启动就会实例化-128~127之间的Integer对象,简单理解的话相当于这个范围内的这些对象是单实例的而且给你准备好了的,至于这个范围之外的就是多实例的了,每次获取都是新new出来的对象;(那几个包装类都类似的有缓存机制)
咳咳,废话说的有点多了,哎慢慢啃jdk源码吧!
这节我们来看看一个AtomicInteger类,这个类单独拿出来说一下,为什么呢?因为我感觉一般对于这个类了解不是很多,但是这个东西在多线程那一块的话经常被问到(用不用得上还要另说。。。。)
1.什么是原子操作
其实我本来也不怎么清楚什么是原子类,原子类有什么用啊?我平常用的类就已经够多了,为什么还要用这个类啊?
怎么说呢?反正很多的问题都是多线程引起来的,一个操作在多线程的情况下和单线程的情况下有的时候是由差异的(简单提一句,什么样的多线程是比较完美的多线程呢?要保证多线程环境下运行的结果和在单线程环境下运行的结果是一样的);
一个很经典的操作:i++,这个在单线程运行下一点问题都没有,但是一到多线程环境下就很坑爹,这个i++在计算机中运行的时候会解析为i=i+1,这里其实是分为三步的:
(1)从主存中读取i的值
(2)将i进行加一操作
(3)再将结果赋值到主存中i
这里每一步都要花费一点时间,但是我们肯定是感觉不到,然而计算机却能很清楚的知道,我们简单画一个多线程的图:
程1在执行这三步过程中,其他线程对线程1不会有任何影响!但是在这里因为i是共享的,线程1对i加一操作,此时线程1中的i等于2,线程2也执行同样的操作那么线程2中的i应该也是2,现在线程1和2都将自己的i更新到主存中,主存中的i最后就等于2。。。。。很明显线程之间相互影响,导致最后的得到的结果出问题了,这就是所谓的线程安全!原子操作的话就是将这三步给包装起来看成一个整体,能够一下子就给主存中i赋值,其他的线程影响不了(即使影响了也会有应对方法),最后就相当于类似直接i=2的操作,这就是原子操作!具体的概念网上很多。。。。
2.volatile关键字
千万别说用volatile关键字保证原子性,首先这个关键字最坑,感觉这个关键字没什么人能说的清楚到底是个什么东西,为什么不能保证原子性,只能保证可见性?对于volatile关键字一般的解释如下:
volatile赋予了变量可见——禁止编译器对成员变量进行优化,它修饰的成员变量在每次被线程访问时,都强迫从内存中重读该成员变量的值;而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存,这样在任何时刻两个不同线程总是看到某一成员变量的同一个值,这就是保证了可见性。
这个概念每个字都认识,连在一起就不认识了。。。。
每个线程内部还会进行这么一大串操作,可见性只能保证在前面两步Read和load,只能确保这两步从主存中获得的值一定是最新的!举个例子,线程1和线程2都从主存中读取到了数字i为0,线程1已经到了第三步use那里,而线程2执行的特别快已经将i加一等于2写入到了主存中;此时即使主存中i被volatile修饰了,但是线程1中的i是不会改变的,因为都已经在使用了还能吐出来呢?最后的结果就是线程1写入结果也是等于2。。。。。最终主存中的i等于2
假如线程1运行的比较慢在还只是在load阶段,线程2就已经将2写入到主存中了,那么线程1就要重新到主存中去读取新的值1=2,然后再拿到自己这里进行运算,最后得出结果为3,写入主存。。。
至于用volatile关键字测试的话,可以自己去试试,我这里就不多说了!因为重点是AtomicInteger类
3.AtomicInteger类
说了这么久才开始说到正题,这个类可以解决线程原子性问题,啥也不管,先用一下看看用法再说;
简单的创建100个线程,每个线程都对AtomicInteger中的数加1,进行1000次,最后的结果应该是100000,下面的结果就是这个;当然你可以把AtomicInteger类型的变为int类型的你就知道区别了,自己试试(运气好一两次就出结果了,运气差的试n次,或者你要适当增大线程的数量和等待时间才能看到效果,好麻烦!);
package com.wyq.test; import java.util.concurrent.atomic.AtomicInteger; public class TestAtomicInteger {
//用的是一个原子类,你可以试试将这个count类型变成int count = 0,最后计算出的结果不是100000
public static AtomicInteger count = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() { @Override
public void run() {
for (int j = 0; j < 1000; j++) {
//这句话就相当于count++,自增
count.incrementAndGet();
}
}
}).start();
}
//主线程等20s,这种效率贼慢,但看着最简单
//为了让创建的100个线程都执行完毕,如果没有这句话的话,那么肯定是main主线程先执行完毕,有的线程还没有创建出来,最后得到的值可能为0
Thread.sleep(20000);
System.out.println("最后计算的结果为:"+count);
} }
那么话说回来了,这个类到底是怎么实现这种原子操作的呢?我们慢慢说,其中上面的代码中出现AtomicInteger的地方就两处,一个是传入0到构造器中实例化对象,另外一个就是incrementAndGet()方法实现自增,我把其中用到的源码拿出来看看:
public class AtomicInteger extends Number implements java.io.Serializable {
//这个Unsafe类是一个工具类,看不到源码,有兴趣的可以通过openjdk去看看源码,大概的作用就是类似C语言中的指针,可以获取内存某处的的值
private static final Unsafe unsafe = Unsafe.getUnsafe();
//value声明为volatile,多线程可见性
private volatile int value;
//直接传进去一个int类型的数据
public AtomicInteger(int initialValue) {
value = initialValue;
}
//自增操作,这里涉及到了一个CAS的问题,等下我们简单说说
//这里就是一个无限循环,首先取到当前的value值,进行加一操作,这个时候会比较value的值和内存中value的值是不是一样,是一样的话说明没有其他线程对value进行修改,那就返回加一之后的值
//否则继续这个循环
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
//这个compareAndSet,每个单词第一个字母提取出来就是CAS,明显这个就是CAS的核心方法,通过Unsafe这个类的CAS方法(其实就是native方法,c/c++实现的)
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
4.CAS原理
CAS是个什么鬼呢?说实话今天之前我也不知道居然还有这个鬼东西,不过今天一直在看很多的博客,终于大概知道了CAS是个什么东西了。。。。反正java肯定是实现不了的,必须要通过JNI调用c/c++代码才能实现这个CAS!
我就用我的理解来说说CAS吧!举个很简单的例子,如果一个用户要对数据库中一条记录进行修改,并且要保证在这个用户还没修改成功的时候,其他用户不能对这条记录进行修改操作,一个很简单的方法:
这应该就是CAS的基本思想,在这里我们通过version来判断数据有没有被别的用户修改过,然后根据是否满足条件来更新数据,ok,现在我们来看看CAS的概念:
CAS(Compare-and-Swap),即比较并替换;CAS需要有3个操作数:CAS(v,e,u);v表示要更新的变量,e表示变量的预期值,u表示变量的新值。当且仅当v的实际值等于e值时,才会将v的值设为u,如果v值和e值不同,则说明已经有其他线程做了更新,则当前线程什么都不做,即更新失败。
我稍微说一下,这三个操作数比较抽象,其实理解起来还是很容易的,对应上图中的操作:v指表中最开始查询到的version的值,e指的是表中version字段不出预料的话应该始终是0(比较乐观,认为没有人会来改变这数据),u指的是我们数据插入成功的话version的新值应该是version+1;只有我们预料version的值和原先表中version的值一样,那么才会将新的version的值更新给version字段;
CAS应该就是这样的原理,至于对应到Atomicinteger中的unsafe.compareAndSwapInt(this, valueOffset, expect, update)这个CAS方法,是不是就比较清晰了啊,它这里的this和valueOffset这两个参数就是为了更快的找到数据value所在的位置,然后就是预料的值expect去和value比较,相等的话就将更新的值update赋值给value,如果失败的话还是会继续那个无限循环啊,去慢慢的试(也就是不断尝试获取最新值,然后进行加一,再用CAS去比较。。。),直到满足条件再更新;
5.CAS知识补充
这些是我看的很多博客中觉得说的很好的东西记录一下:
CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即时没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
CAS其底层是通过CPU的1条指令来完成3个步骤,因此其本身是一个原子性操作,不存在其执行某一个步骤的时候而被中断的可能。
CAS的优点:很高效的解决了原子操作问题,天生免疫死锁(根本就没有锁,当然就不会有线程一直阻塞了),更为重要的是,使用无锁的方式没有所竞争带来的开销,也没有线程间频繁调度带来的开销,他比基于锁的方式有更优越的性能
CAS缺点:1.循环时间长开销很大;2.只能保证一个共享变量的原子操作;3.ABA问题
顺便说说ABA问题:还是举个例子,线程一和线程二要对主存中的数据A进行操作:
第一步:线程一读取A数据,线程二读取A数据;
第二步:线程一从取到数据A进行自增操作然后再到将修改后的数据写入主存,这是需要花时间的,在这段时间内,线程二运行速度可能贼快,直接将主存中的数据A修改为数据B,然后又修改为数据A(或者是线程2将数据A改为B,还有个线程3将数据B改为A)
第三步:线程一进行CAS操作,将期望的值A和内存中的值A比较,是一样的,那么线程一的CAS操作成功;线程一却不知道线程二早就对A说修改两次了,这就有点可怕了。。。
这就是最简单版本的ABA问题(话说前几天面试,面试官问我知不知道ABA问题,怎么处理ABA问题呢?直接把我问懵了,妈耶!我还直到今天才知道CAS和ABA呢。。。。),那么怎么解决比较好呢?其实很容易,就跟我们上面那个数据库表的处理一样,弄个版本号出来,每次进行修改版本号都会进行改变,更新数据的时候会比较版本号!基于这个原理,JAVA中AtomicStampedReference这个类就实现了这种版本标记的功能,后面应该会看看这个类的源码的!
随意看看AtomicInteger类和CAS的更多相关文章
- Atomic类和CAS
说Atomic类之前,先聊一聊volatile. 对volatile的第一印象就是可见性.所谓可见性,就是一个线程对共享变量的修改,别的线程能够感知到. 但是对于原子性,volatile是不能保证的. ...
- AtomicInteger类的理解及使用
AtomicInteger在多线程并发场景的使用 AtomicInteger提供原子操作来进行Integer的使用,因此十分适合高并发情况下的使用. AtomicInteger位于包package j ...
- AtomicInteger类的理解与使用
AtomicInteger类的理解与使用 首先看两段代码,一段是Integer的,一段是AtomicInteger的,为以下: public class Sample1 { private stati ...
- Java AtomicInteger类的使用方法详解_java - JAVA
文章来源:嗨学网 敏而好学论坛www.piaodoo.com 欢迎大家相互学习 首先看两段代码,一段是Integer的,一段是AtomicInteger的,为以下: public class Samp ...
- AtomicInteger类和int原生类型自增鲜明的对比
AtomicInteger这个类的存在是为了满足在高并发的情况下,原生的整形数值自增线程不安全的问题.比如说 int i = 0 ; i++; 上面的写法是线程不安全的. 有的人可能会说了,可以使 ...
- AtomicInteger类保证线程安全的用法
J2SE 5.0提供了一组atomic class来帮助我们简化同步处理.基本工作原理是使用了同步synchronized的方法实现了对一个long, integer, 对象的增.减.赋值(更新)操作 ...
- AtomicInteger类的使用
AtomicInteger介绍 AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减. AtomicInteger使用场景 AtomicInteger提供原子操作 ...
- AtomicInteger类的简单应用
AtomicInteger,一个提供原子操作的Integer的类.在Java语言中,++i和i++操作并不是线程安全的,在使用的时候,不可避免的会用到synchronized关键字.而AtomicIn ...
- AtomicInteger类
今天写代码.尝试使用了AtomicInteger这个类,感觉使用起来非常爽,特别适用于高并发訪问.能保证i++,++id等系列操作的原子性. ++i和i++操作并非线程安全的.非常多人会用到synch ...
随机推荐
- Nginx在开发中常用的基础命令
场景 Ubuntu Server 16.04 LTS上怎样安装下载安装Nginx并启动: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/detai ...
- jTopo HTML5 Canvas 画图组件
jTopo是什么? jTopo(Javascript Topology library)是一款完全基于HTML5 Canvas的关系.拓扑图形化界面开发工具包. jTopo关注于数据的图形展示,它是面 ...
- WPF布局容器
1.StackPanel:堆栈面板,通过Orientation属性设置子元素的布局排列方向为“Vertical”(垂直)和“Horizontal”(水平),不写其默认值为“Vertical”,当设置为 ...
- [Linux] linux路由表
路由表用于决定数据包从哪个网口发出,其主要判断依据是目标IP地址Linux路由表其实有2个主要概念:按顺序走路由策略,在路由策略对应的路由表中匹配规则路由策略(rule)路由表(table) 查看所有 ...
- linux中批量添加文件前缀的操作
需要在文件夹内所有txt文件的文件名前面添加"gt_"; 就是由原来的文件“xxx.txt”变成“gt_xxx.txt”: 网上搜来的脚本如下: for i in `ls`; do ...
- Appium(八):Appium API(二) 元素等待、元素操作
1. 元素等待 我们在使用脚本的时候,可能会由于网络.服务器处理.电脑等原因,我们想要找的元素没有加载出来,这个时候如果直接定位就可能会报错. 这个时候我们就可以设置元素等待了. 什么叫元素等待呢? ...
- 关于使用DB2数据库的项目后台报-420错误码的问题
### Error querying database. Cause: com.ibm.db2.jcc.am.SqlDataException: DB2 SQL Error: SQLCODE=-4 ...
- 老版本nginx存在安全漏洞,不停服务热升级
1.场景描述 安全部通知:nginx存在"整数溢出漏洞",经测试2017年4月21日之后的版本无问题,将openresty升级到最新版本,Nginx升级到1.13.2之后的版本. ...
- Pick of the Week'19 | 图数据库 Nebula 第 47 周看点-- insert 的二三事
每周五 Nebula 为你播报每周看点,每周看点由本周大事件.用户问答.Nebula 产品动态和推荐阅读构成. 今天是 2019 年第 47 个工作周的周五,来和 Nebula 看看本周有什么图数据库 ...
- Go使用变量类型声明和方法的注意事项
当我们通过把一个现有(非interface)的类型定义为一个新的类型时,新的类型不会继承现有类型的方法. 神马意思?来一段简短错误的代码: package main import "sync ...