Java并发编程之原子变量
原子变量最主要的一个特点就是所有的操作都是原子的,synchronized关键字也可以做到对变量的原子操作。只是synchronized的成本相对较高,需要获取锁对象,释放锁对象,如果不能获取到锁,还需要阻塞在阻塞队列上进行等待。而如果单单只是为了解决对变量的原子操作,建议使用原子变量。关于原子变量的介绍,主要涉及以下内容:
- 原子变量的基本概念
- 通过AtomicInteger了解原子变量的基本使用
- 通过AtomicInteger了解原子变量的基本原理
- AtomicReference的基本使用
- 使用FieldUpdater操作非原子变量的字段属性
- 经典的ABA问题的解决
一、原子变量的基本概念
原子变量保证了该变量的所有操作都是原子的,不会因为多线程的同时访问而导致脏数据的读取问题。我们先看一段synchronized关键字保证变量原子性的代码:
public class Counter {
private int count;
public synchronized void addCount(){
this.count++;
}
}
简单的count++操作,线程对象首先需要获取到Counter 类实例的对象锁,然后完成自增操作,最后释放对象锁。整个过程中,无论是获取锁还是释放锁都是相当消耗成本的,一旦不能获取到锁,还需要阻塞当前线程等等。
对于这种情况,我们可以将count变量声明成原子变量,那么对于count的自增操作都可以以原子的方式进行,就不存在脏数据的读取了。Java给我们提供了以下几种原子类型:
- AtomicInteger和AtomicIntegerArray:基于Integer类型
- AtomicBoolean:基于Boolean类型
- AtomicLong和AtomicLongArray:基于Long类型
- AtomicReference和AtomicReferenceArray:基于引用类型
在本文的余下内容中,我们将主要介绍AtomicInteger和AtomicReference两种类型,AtomicBoolean和AtomicLong的使用和内部实现原理几乎和AtomicInteger一样。
二、AtomicInteger的基本使用
首先看它的两个构造函数:
private volatile int value;
public AtomicInteger(int initialValue) {
value = initialValue;
}
public AtomicInteger() {
}
可以看到,我们在通过构造函数构造AtomicInteger原子变量的时候,如果指定一个int的参数,那么该原子变量的值就会被赋值,否则就是默认的数值0。
也有获取和设置这个value值的方法:
public final int get()
public final void set(int newValue)
当然,这两个方法并不是原子的,所以一般也很少使用,而以下的这些基于原子操作的方法则相对使用的频繁,至于它们的具体实现是怎样的,我们将在本文的后续小节中进行简单的学习。
//基于原子操作,获取当前原子变量中的值并为其设置新值
public final int getAndSet(int newValue)
//基于原子操作,比较当前的value是否等于expect,如果是设置为update并返回true,否则返回false
public final boolean compareAndSet(int expect, int update)
//基于原子操作,获取当前的value值并自增一
public final int getAndIncrement()
//基于原子操作,获取当前的value值并自减一
public final int getAndDecrement()
//基于原子操作,获取当前的value值并为value加上delta
public final int getAndAdd(int delta)
//还有一些反向的方法,比如:先自增在获取值的等等
下面我们实现一个计数器的例子,之前我们使用synchronized实现过,现在我们使用原子变量再次实现该问题。
//自定义一个线程类
public class MyThread extends Thread {
public static AtomicInteger value = new AtomicInteger();
@Override
public void run(){
try {
Thread.sleep((long) ((Math.random())*100));
//原子自增
value.incrementAndGet();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//main函数中启动100条线程并让他们启动
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[100];
for (int i=0;i<100;i++){
threads[i] = new MyThread();
threads[i].start();
}
for (int j=0;j<100;j++){
threads[j].join();
}
System.out.println("value:"+MyThread.value);
}
多次运行会得到相同的结果:
很显然,使用原子变量要比使用synchronized要简洁的多并且效率也相对较高。
三、AtomicInteger的内部基本原理
AtomicInteger的实现原理有点像我们的包装类,内部主要操作的是value字段,这个字段保存就是原子变量的数值。value字段定义如下:
private volatile int value;
首先value字段被volatile修饰,即不存在内存可见性问题。由于其内部实现原子操作的代码几乎类似,我们主要学习下incrementAndGet方法的实现。
在揭露该方法的实现原理之前,我们先看另一个方法:
public final boolean compareAndSet(int expect, int update{
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
compareAndSet方法又被称为CAS,该方法调用unsave的一个compareAndSwapInt方法,这个方法是native,我们看不到源码,但是我们需要知道该方法完成的一个目标:比较当前原子变量的值是否等于expect,如果是则将其修改为update并返回true,否则直接返回false。当然,这个操作本身就是原子的,较为底层的实现。
在jdk1.7之前,我们的incrementAndGet方法是这样实现的:
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
方法体是一个死循环,current获取到当前原子变量中的值,由于value被修饰volatile,所以不存在内存可见性问题,数据一定是最新的。然后current加一后赋值给next,调用我们的CAS原子操作判断value是否被别的线程修改过,如果还是原来的值,那么将next的值赋值给value并返回next,否则重新获取当前value的值,再次进行判断,直到操作完成。
incrementAndGet方法的一个很核心的思想是,在加一之前先去看看value的值是多少,真正加的时候再去看一下,如果发现变了,不操作数据,否则为value加一。
但是在jdk1.8以后,做了一些优化,但是最后还是调用的compareAndSwapInt方法。但基本思想还是没变。
四、AtomicReference的基本使用
对于一些自定义类或者字符串等这些引用类型,Java并发包也提供了原子变量的接口支持。AtomicReference内部使用泛型来实现的。
private volatile V value;
public AtomicReference(V initialValue) {
value = initialValue;
}
public AtomicReference() {
}
有关其他的一些原子方法如下:
//获取并设置value的值为newvalue
public final V getAndSet(V newValue) {
return (V)unsafe.getAndSetObject(this, valueOffset, newValue);
}
AtomicReference中少了一些自增自减的操作,但是对于value的修改依然是原子的。
五、使用FieldUpdater操作非原子变量的字段属性
FieldUpdater允许我们不必将字段设置为原子变量,利用反射直接以原子方式操作字段。例如:
//定义一个计数器
public class Counter {
private volatile int count;
public int getCount() {
return count;
}
public void addCount(){
AtomicIntegerFieldUpdater<Counter> updater = AtomicIntegerFieldUpdater.newUpdater(Counter.class,"count");
updater.getAndIncrement(this);
}
}
然后我们创建一百个线程随机调用同一个Counter对象的addCount方法,无论运行多少次,结果都是一百。这种方式实现的原子操作,对于被操作的变量不需要被包装成原子变量,但是却可以直接以原子方式操作它的数值。
六、经典的ABA问题
我们的原子变量都依赖一个核心的方法,那就是CAS。这个方法最核心的思想就是,更改变量值之前先获取该变量当前最新的值,然后在实际更改的时候再次获取该变量的值,如果没有被修改,那么进行更改,否则循环上述操作直至更改操作完成。假如一个线程想要对变量count进行修改,实际操作之前获取count的值为A,此时来了一个线程将count值修改为B,又来一个线程获取count的值为B并将count修改为A,此时第一个线程全然不知道count的值已经被修改两次了,虽然值还是A,但是实际上数据已经是脏的。
这就是典型的ABA问题,一个解决办法是,对count的每次操作都记录下当前的一个时间戳,这样当我们原子操作count之前,不仅查看count的最新数值,还记录下该count的时间戳,在实际操作的时候,只有在count的数值和时间戳都没有被更改的情况之下才完成修改操作。
public static void main(String[] args){
int count=0;
int stamp = 1;
AtomicStampedReference reference = new AtomicStampedReference(count,stamp);
int next = count++;
reference.compareAndSet(count, next, stamp, stamp);
}
AtomicStampedReference 的CAS方法要求传入四个参数,该方法的内部会同时比较count和stamp,只有这两个值都没有发生改变的前提下,CAS才会修改count的值。
上述我们介绍了有关原子变量的最基本内容,最后我们比较下原子变量和synchronized关键字的区别。
从思维模式上看,原子变量代表一种乐观的非阻塞式思维,它假定没有别人会和我同时操作某个变量,于是在实际修改变量的值的之前不会锁定该变量,但是修改变量的时候是使用CAS进行的,一旦发现冲突,继续尝试直到成功修改该变量。
而synchronized关键字则是一种悲观的阻塞式思维,它认为所有人都会和我同时来操作某个变量,于是在将要操作该变量之前会加锁来锁定该变量,进而继续操作该变量。
Java并发编程之原子变量的更多相关文章
- java并发编程学习: 原子变量(CAS)
先上一段代码: package test; public class Program { public static int i = 0; private static class Next exte ...
- Java多线程并发编程之原子变量与非阻塞同步机制
1.非阻塞算法 非阻塞算法属于并发算法,它们可以安全地派生它们的线程,不通过锁定派生,而是通过低级的原子性的硬件原生形式 -- 例如比较和交换.非阻塞算法的设计与实现极为困难,但是它们能够提供更好的吞 ...
- Java并发编程实战 第15章 原子变量和非阻塞同步机制
非阻塞的同步机制 简单的说,那就是又要实现同步,又不使用锁. 与基于锁的方案相比,非阻塞算法的实现要麻烦的多,但是它的可伸缩性和活跃性上拥有巨大的优势. 实现非阻塞算法的常见方法就是使用volatil ...
- 转: 【Java并发编程】之二十:并发新特性—Lock锁和条件变量(含代码)
简单使用Lock锁 Java5中引入了新的锁机制--Java.util.concurrent.locks中的显式的互斥锁:Lock接口,它提供了比synchronized更加广泛的锁定操作.Lock接 ...
- JAVA并发编程J.U.C学习总结
前言 学习了一段时间J.U.C,打算做个小结,个人感觉总结还是非常重要,要不然总感觉知识点零零散散的. 有错误也欢迎指正,大家共同进步: 另外,转载请注明链接,写篇文章不容易啊,http://www. ...
- 【java并发编程实战】-----线程基本概念
学习Java并发已经有一个多月了,感觉有些东西学习一会儿了就会忘记,做了一些笔记但是不系统,对于Java并发这么大的"系统",需要自己好好总结.整理才能征服它.希望同仁们一起来学习 ...
- java并发编程实战学习(3)--基础构建模块
转自:java并发编程实战 5.3阻塞队列和生产者-消费者模式 BlockingQueue阻塞队列提供可阻塞的put和take方法,以及支持定时的offer和poll方法.如果队列已经满了,那么put ...
- 《Java并发编程实战》学习笔记 线程安全、共享对象和组合对象
Java Concurrency in Practice,一本完美的Java并发参考手册. 查看豆瓣读书 推荐:InfoQ迷你书<Java并发编程的艺术> 第一章 介绍 线程的优势:充分利 ...
- Java并发编程-总纲
Java 原生支持并发,基本的底层同步包括:synchronized,用来标示一个方法(普通,静态)或者一个块需要同步执行(某一时刻,只允许一个线程在执行代码块).volatile,用来标识一个变量是 ...
随机推荐
- 201521123066 《java程序设计》第一周学习总结
本周学习总结 (1)学习了Java的跨平台运行是因为有虚拟机,其特点是具有简单性,结构中立. (2)老师使用了新的作业模式,要学会发现其中的优势并好好学习使用. 书面作业 (1)为什么java程序可以 ...
- 201521123065《java程序设计》第12周学习总结
1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多流与文件相关内容. 1.流的划分:输入流:字节流(InputStream).字符流(reader): 输出流:字节流(Output ...
- 201521123054 《Java程序设计》第14周学习总结
1. 本周学习总结 2. 书面作业 1. MySQL数据库基本操作 1.1建立数据库,将自己的姓名.学号作为一条记录插入.(截图,需出现自己的学号.姓名) 1.2在自己建立的数据库上执行常见SQL语句 ...
- 201521123102 《Java程序设计》第11周学习总结
1. 本周学习总结 2.书面作业 1.互斥访问与同步访问 完成题集4-4(互斥访问)与4-5(同步访问)## 1.1 除了使用synchronized修饰方法实现互斥同步访问,还有什么办法实现互斥同步 ...
- Dodobox一个基于所有平台的嵌入式操作系统(OS)
DodoBox是为广大应用开始者提供的一个跨平台应用发布平台.它提供了客户端和服务器端的SDK及详细的开发者帮助文件,帮助开发者创建.移植软件应用或游戏应用. DodoBox基于OpenGL技术,提供 ...
- 关于sping quartz定时执行理解与思考
转载请注明原创出处,谢谢! 一直以为自己理解spring quartz,忽然最近几天发现自己理解的不对,在4月18号的时候,我执行了一个spring quartz的计划如下: 1 0 0 */3 * ...
- day09<面向对象+>
面向对象(多态的概述及其代码体现) 面向对象(多态中的成员访问特点之成员变量) 面向对象(多态中的成员访问特点之成员方法) 面向对象(多态中的成员访问特点之静态成员方法) 面向对象(超人的故事) 面向 ...
- day20<IO流>
IO流(IO流概述及其分类) IO流(FileInputStream) IO流(read()方法返回值为什么是int) IO流(FileOutputStream) IO流(FileOutputStre ...
- 前端系列——jquery前端国际化解决方案“填坑日记”
前言:最近,新的平台还没有开发完成,原来的老项目又提出了新的需求:系统国际化.如果是前后端完全分离的开发模式,要做国际化,真的太简单了,有现成的解决方案,基于Node构建的时下热门的任何一种技术选型都 ...
- 《MATLAB从入门到放弃》打通 “矩阵” 障碍
目录: » 矩阵的生成与大小 > 简单矩阵的生成 > 随机矩阵的生成 > 矩阵的大小 » 矩阵的索引与访问 » 矩阵的拼接与裁剪 > 矩阵的拼接 &g ...