浅谈Java中的atomic包实现原理及应用

1.同步问题的提出

假设我们使用一个双核处理器执行A和B两个线程,核1执行A线程,而核2执行B线程,这两个线程现在都要对名为obj的对象的成员变量i进行加1操作,假设i的初始值为0,理论上两个线程运行后i的值应该变成2,但实际上很有可能结果为1。

我们现在来分析原因,这里为了分析的简单,我们不考虑缓存的情况,实际上有缓存会使结果为1的可能性增大。A线程将内存中的变量i读取到核1算数运算单元中,然后进行加1操作,再将这个计算结果写回到内存中,因为上述操作不是原子操作,只要B线程在A线程将i增加1的值写回到内存之前,读取了内存中i的值(此时i值为0),那么一定就会出现i的结果为1。因为A和B线程读取的i的值都为0,两个线程对它加1后的值都为1,两个线程先后将1写入到变量i中,也就是说i被两次写入的值都为1。

最通常的解决方法是两个线程中对i加1的代码用synchronize关键字对obj对象加锁。今天我们介绍一种新的解决方案,即使用Atomic包中的相关类来解决。

2.Atomic在硬件上的支持

在单处理器系统(UniProcessor)中,能够在单条指令中完成的操作都可以认为是"原子操作",因为中断只能发生于指令之间(因为线程的调度需要通过中断完成)。这也是某些CPU指令系统中引入了test_and_set、test_and_clear等指令用于临界资源互斥的原因。在对称多处理器(SymmetricMulti-Processor)结构中就不同了,由于系统中有多个处理器在独立地运行,即使能在单条指令中完成的操作也有可能受到干扰。

在x86平台上,CPU提供了在指令执行期间对总线加锁的手段。CPU芯片上有一条引线#HLOCKpin,如果汇编语言的程序中在一条指令前面加上前缀"LOCK",经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCKpin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。当然,并不是所有的指令前面都可以加lock前缀的,只有ADD,ADC,AND,BTC,BTR,BTS,CMPXCHG,DEC,INC,NEG,NOT,OR,SBB,SUB,XOR,XADD,和XCHG指令前面可以加"LOCK"指令,实现原子操作。

Atomic的核心操作就是CAS(compareandset,利用CMPXCHG指令实现,它是一个原子指令),该指令有三个操作数,变量的内存值V(value的缩写),变量的当前预期值E(exception的缩写),变量想要更新的值U(update的缩写),当内存值和当前预期值相同时,将变量的更新值覆盖内存值,执行伪代码如下。

if(V == E){
  V = U
  return true
}else{
  return false
}

现在我们就用CAS操作来解决上述问题。B线程将内存中的变量i读取一个临时变量中(假设此时读取的值为0),然后再将i的值读取到core1的算数运算单元中,接下来进行加1操作,比较临时变量中的值和i当前的值是否相同,如果相同用运算单元中的结果(即i+1)的值覆盖内存中i的值(注意这一部分就是CAS操作,它是个原子操作,不能被中断且其它线程中的CAS操作不能同时执行),否则指令执行失败。如果指令失败,说明A线程已经将i的值加1。由此可知如果两个线程一开始读取的i的值为都为0,那么必然只有一个线程的CAS操作能够成功,因为CAS操作不能并发执行。对于CAS操作执行失败的线程,只要循环执行CAS操作,那么一定能够成功。可以看到并没有线程阻塞,这和synchronize的原理有着本质的不同。

3.Atomic包简介及源码分析

Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。

Atomic系列的类中的核心方法都会调用unsafe类中的几个本地方法。我们需要先知道一个东西就是Unsafe类,全名为:sun.misc.Unsafe,这个类包含了大量的对C代码的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的,是告诉你这个里面大量的方法调用都会存在安全隐患,需要小心使用,否则会导致严重的后果,例如在通过unsafe分配内存的时候,如果自己指定某些区域可能会导致一些类似C++一样的指针越界到其他进程的问题。

Atomic包中的类按照操作的数据类型可以分成4组

AtomicBoolean,AtomicInteger,AtomicLong

线程安全的基本类型的原子性操作

AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

线程安全的数组类型的原子性操作,它操作的不是整个数组,而是数组中的单个元素

AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

基于反射原理对象中的基本类型(长整型、整型和引用类型)进行线程安全的操作

AtomicReference,AtomicMarkableReference,AtomicStampedReference

线程安全的引用类型及防止ABA问题的引用类型的原子操作

我们一般常用的AtomicInteger、AtomicReference和AtomicStampedReference。现在我们来分析一下Atomic包中AtomicInteger的源代码,其它类的源代码在原理上都比较类似。

1.有参构造函数

public AtomicInteger(int initialValue) {
  value = initialValue;
}

从构造函数函数可以看出,数值存放在成员变量value中

private volatile int value;

成员变量value声明为volatile类型,说明了多线程下的可见性,即任何一个线程的修改,在其它线程中都会被立刻看到

2.compareAndSet方法(value的值通过内部this和valueOffset传递)

public final boolean compareAndSet(int expect, int update) {
 return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

这个方法就是最核心的CAS操作

3.getAndSet方法,在该方法中调用了compareAndSet方法

public final int getAndSet(int newValue) {
    for (;;) {
      int current = get();
      if (compareAndSet(current, newValue))
        return current;
    }
}

如果在执行if(compareAndSet(current,newValue)之前其它线程更改了value的值,那么导致value的值必定和current的值不同,compareAndSet执行失败,只能重新获取value的值,然后继续比较,直到成功。

4.i++的实现

public final int getAndIncrement() {
    for (;;) {
      int current = get();
      int next = current + 1;
      if (compareAndSet(current, next))
        return current;
    }
}

5. ++i的实现

public final int incrementAndGet() {
    for (;;) {
      int current = get();
      int next = current + 1;
      if (compareAndSet(current, next))
        return next;
    }
}

4.使用AtomicInteger例子

下面的程序,利用AtomicInteger模拟卖票程序,运行结果中不会出现两个程序卖了同一张票,也不会卖到票为负数

package javaleanning;
import java.util.concurrent.atomic.AtomicInteger;
public class SellTickets {
	AtomicInteger tickets = new AtomicInteger(100);
	class Seller implements Runnable{
		@Override
		    public void run() {
			while(tickets.get() > 0){
				int tmp = tickets.get();
				if(tickets.compareAndSet(tmp, tmp-1)){
					System.out.println(Thread.currentThread().getName()+" "+tmp);
				}
			}
		}
	}
	public static void main(String[] args) {
		SellTickets st = new SellTickets();
		new Thread(st.new Seller(), "SellerA").start();
		new Thread(st.new Seller(), "SellerB").start();
	}
}

5.ABA问题

上述的例子运行结果完全正确,这是基于两个(或多个)线程都是向同一个方向对数据进行操作,上面的例子中两个线程都是是对tickets进行递减操作。再比如,多个线程对一个共享队列都进行对象的入列操作,那么通过AtomicReference类也可以得到正确的结果(AQS中维护的队列其实就是这个情况),但是多个线程即可以入列也可以出列,也就是数据的操作方向不一致,那么可能出现ABA的情况。

我们现在拿一个比较好理解的例子来解释ABA问题,假设有两个线程T1和T2,这两个线程对同一个栈进行出栈和入栈的操作。

我们使用AtomicReference定义的tail来保存栈顶位置

AtomicReference<T> tail;

假设T1线程准备出栈,对于出栈操作我们只需要将栈顶位置由sp通过CAS操作更新为newSP即可,如图1所示。但是在T1线程执行tail.compareAndSet(sp,newSP)之前系统进行了线程调度,T2线程开始执行。T2执行了三个操作,A出栈,B出栈,然后又将A入栈。此时系统又开始调度,T1线程继续执行出栈操作,但是在T1线程看来,栈顶元素仍然为A,(即T1仍然认为B还是栈顶A的下一个元素),而实际上的情况如图2所示。T1会认为栈没有发生变化,所以tail.compareAndSet(sp,newSP)执行成功,栈顶指针被指向了B节点。而实际上B已经不存在于堆栈中,T1将A出栈后的结果如图3所示,这显然不是正确的结果。

6.ABA问题的解决方法

使用AtomicMarkableReference,AtomicStampedReference。使用上述两个Atomic类进行操作。他们在实现compareAndSet指令的时候除了要比较当对象的前值和预期值以外,还要比较当前(操作的)戳值和预期(操作的)戳值,当全部相同时,compareAndSet方法才能成功。每次更新成功,戳值都会发生变化,戳值的设置是由编程人员自己控制的。

public Boolean compareAndSet(V expectedReference, V newReference,
              int expectedStamp,int newStamp) {
	Pair<V> current = pair;
	return expectedReference == current.reference &&
	      expectedStamp == current.stamp &&
	      ((newReference == current.reference && newStamp == current.stamp) ||
	      casPair(current, Pair.of(newReference, newStamp)));
}

这时的compareAndSet方法需要四个参数expectedReference,newReference,expectedStamp,newStamp,我们在使用这个方法时要保证期望的戳值和要更新戳值不能一样,通常newStamp=expectedStamp+1

还拿上述的例子

假设线程T1在弹栈之前:sp指向A,戳值为100。

线程T2执行:将A出栈后,sp指向B,戳值变为101,

B出栈后,sp指向C,戳值变为102,

A入栈后,sp指向A,戳值变为103,

线程T1继续执行compareAndSet语句,发现sp虽然还是指向A,但是戳值的预期值100和当前值103不同,所以compareAndSet失败,需要从新获取newSP的值(此时newSP就会指向C),以及戳的预期值103,然后再次进行compareAndSet操作,这样A成功出栈,sp会指向C。

注意,由于compareAndSet只能一次改变一个值,无法同时改变newReference和newStamp,所以在实现的时候,在内部定义了一个类Pair类将newReference和newStamp变成一个对象,进行CAS操作的时候,实际上是对Pair对象的操作

private static class Pair<T> {
  final T reference;
  final int stamp;
  private Pair(T reference, int stamp) {
    this.reference = reference;
    this.stamp = stamp;
  }
  static <T> Pair<T> of(T reference, int stamp) {
    return new Pair<T>(reference, stamp);
  }
}

对于AtomicMarkableReference而言,戳值是一个布尔类型的变量,而AtomicStampedReference中戳值是一个整型变量。

总结

以上就是本文关于浅谈Java中的atomic包实现原理及应用的全部内容,希望对大家有所帮助。感兴趣的朋友可以继续参阅本站其他相关专题,如有不足之处,欢迎留言指出。

您可能感兴趣的文章:

  • Java RSA加密解密实现方法分析【附BASE64 jar包下载】
  • java实现一个扫描包的工具类实例代码
  • 把Java程序打包成jar文件包并执行的方法
  • Java程序打包成带参数的jar文件实例代码
  • java调用7zip解压压缩包的实例
  • java动态添加外部jar包到classpath的实例详解
  • java判断字符串中是否包含中文并过滤中文
  • Java 判断字符串中是否包含中文的实例详解
(0)

相关推荐

  • java动态添加外部jar包到classpath的实例详解

    java动态添加外部jar包到classpath的实例详解 前言: 在项目开发过程中我们有时候需要动态的添加外部jar包,但是具体的业务需求还没有遇到过,因为如果动态添加外部jar包后,我们就需要修改业务代码,而修改代码就需要重新启动服务,那样好像就没有必要动态添加外部jar包了,怎么样才能不重新启动服务器就可以使用最新的代码我没有找到方法,如果各位知道的话给我点建议,回归主题,实现动态添加外部jar包到classpath的方法如下: String beanClassName = "com.dy

  • java实现一个扫描包的工具类实例代码

    前言 在很多的实际场景中,我们需要得到某个包名下面所有的类,比如我们在使用SpringMVC的时候,知道SpringMVC可以扫描指定包下的所有类,在平时的开发中,我们也有这样的场景,所以今天写一个扫描包的工具类,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 代码如下: package com.gujin.utils; import java.io.File; import java.io.FileFilter; import java.io.IOException; imp

  • Java RSA加密解密实现方法分析【附BASE64 jar包下载】

    本文实例讲述了Java RSA加密解密实现方法.分享给大家供大家参考,具体如下: 该工具类中用到了BASE64,需要借助第三方类库:javabase64-1.3.1.jar javabase64-1.3.1.jar 本站下载地址. 注意: RSA加密明文最大长度117字节,解密要求密文最大长度为128字节,所以在加密和解密的过程中需要分块进行. RSA加密对明文的长度是有限制的,如果加密数据过大会抛出如下异常: Exception in thread "main" javax.cryp

  • Java 判断字符串中是否包含中文的实例详解

    Java 判断字符串中是否包含中文的实例详解 Java判断一个字符串是否有中文是利用Unicode编码来判断,因为中文的编码区间为:0x4e00--0x9fbb, 不过通用区间来判断中文也不非常精确,因为有些中文的标点符号利用区间判断会得到错误的结果.而且利用区间判断中文效率也并不高,例如:str.substring(i, i + 1).matches("[\\一-\\?]+"),就需要遍历整个字符串,如果字符串太长效率非常低,而且判断标点还会错误.这里提高 一个高效准确的判断方法,使

  • 把Java程序打包成jar文件包并执行的方法

    本文介绍了把Java程序打包成jar文件包并执行的方法,分享给大家,具体如下: 1.首先要确认自己写的程序有没有报错. 2.第一次我写的是Web Project到现在,我一直没有执行成功,所以最好创建的是java Project 打包步骤: 1.在项目上,右键,选择Export. 2.进入到下图界面,选择Java 下面的JAR file 3.选择项目,确认必要的文件是否选中,选择保存jar文件包的路径,如下图 4.完成步骤3之后,点击Next,进入如下图界面: 5.直接点Next,进入下面的界面

  • java调用7zip解压压缩包的实例

    前言 最近的项目中需要用到解压缩包的功能,客户给出的压缩包的格式主要是rar和zip,因此就打算使用java调用7zip的命令行进行解压文件,本文主要记录一下实现的过程以及其中遇到的问题. 7zip命令行 7z <command> [<switches>...] <archive_name> [<file_names>...][<@listfiles...>] 7z的commands中包含添加文件到压缩包.从压缩包中删除文件以及提取文件等等多个命

  • Java程序打包成带参数的jar文件实例代码

    这里我们通过Apache Commons CLI来完成目标功能,废话不多说直接上代码 所需的maven依赖 <dependency> <groupId>commons-cli</groupId> <artifactId>commons-cli</artifactId> <version>1.4</version> </dependency> 这里我们贴出主类代码 Options opts = new Optio

  • java判断字符串中是否包含中文并过滤中文

    java判断字符串中是否包含中文并过滤掉中文,具体内容如下 1.判断字符串中是否包含中文方法封装 /** * 判断字符串中是否包含中文 * @param str * 待校验字符串 * @return 是否为中文 * @warn 不能校验是否为中文标点符号 */ public static boolean isContainChinese(String str) { Pattern p = Pattern.compile("[\u4e00-\u9fa5]"); Matcher m = p

  • 浅谈Java中的atomic包实现原理及应用

    1.同步问题的提出 假设我们使用一个双核处理器执行A和B两个线程,核1执行A线程,而核2执行B线程,这两个线程现在都要对名为obj的对象的成员变量i进行加1操作,假设i的初始值为0,理论上两个线程运行后i的值应该变成2,但实际上很有可能结果为1. 我们现在来分析原因,这里为了分析的简单,我们不考虑缓存的情况,实际上有缓存会使结果为1的可能性增大.A线程将内存中的变量i读取到核1算数运算单元中,然后进行加1操作,再将这个计算结果写回到内存中,因为上述操作不是原子操作,只要B线程在A线程将i增加1的

  • 浅谈Java中常用数据结构的实现类 Collection和Map

    线性表,链表,哈希表是常用的数据结构,在进行Java开发时,JDK已经为我们提供了一系列相应的类来实现基本的数据结构.这些类均在java.util包中.本文试图通过简单的描述,向读者阐述各个类的作用以及如何正确使用这些类. Collection ├List │├LinkedList │├ArrayList │└Vector │ └Stack └Set Map ├Hashtable ├HashMap └WeakHashMap Collection接口 Collection是最基本的集合接口,一个C

  • 浅谈Java中各种修饰符与访问修饰符的说明

    JAVA中的类只能是public 或者package的.这是符合逻辑的:人们定义类的初衷就是为了让别人用的.倘若是private,别人怎么调用?但是有一个内部类可以被定义为private.严格上说,内部类,算不得上是一种光明正大的类,内部类在某种意义上是类这个王国里的特务和地下工作者.特务和地下工作者为王国起了不少作用,但是几乎从来不敢在公众场合抛投露面.就算要露面,也要在主人(class)的同意下,向导(Interface)的引导下,才敢战战兢兢的走出来.下面是常规的一些类的修饰符和访问修饰符

  • 浅谈java中类名.class, class.forName(), getClass()的区别

    Class对象的生成方式如下: 1.类名.class    说明: JVM将使用类装载器, 将类装入内存(前提是:类还没有装入内存),不做类的初始化工作.返回Class的对象 2.Class.forName("类名字符串")  (注:类名字符串是包名+类名) 说明:装入类,并做类的静态初始化,返回Class的对象 3.实例对象.getClass()  说明:对类进行静态初始化.非静态初始化:返回引用o运行时真正所指的对象(因为:子对象的引用可能会赋给父对象的引用变量中)所属的类的Cla

  • 浅谈Java中的this作为返回值时返回的是什么

    有时会遇到this作为返回值的情况,那么此时返回的到底是什么呢? 返回的是调用this所处方法的那个对象的引用,读起来有点绕口哈,有没有想起小学语文分析句子成份的试题,哈哈. 一点点分析的话,主干是"返回的是引用": 什么引用呢?"那个对象的引用": 哪个对象呢?"调用方法的那个对象": 调用的哪个方法呢?"调用的是this所位于的方法":这样就清楚了. 再总结一下就是,this作为返回值时,返回的是调用某方法的对象的引用,这

  • 浅谈Java中的class类

    Class 类是在Java语言中定义一个特定类的实现.一个类的定义包含成员变量,成员方法,还有这个类实现的接口,以及这个类的父类.Class类的对象用于表示当前运行的 Java 应用程序中的类和接口. 比如:每个数组均属于一个 Class 类对象,所有具有相同元素类型和维数的数组共享一个Class 对象.基本的 Java 类型(boolean, byte, char, short,int, long, float 和 double) 和 void 类型也可表示为 Class 对象. 以下示例使用

  • 浅谈java中字节与字符的区别

    最近在看Java中的IO相关知识,发现对字节和字符的理解还不够.写篇总结记录一下. 一.字节 所谓字节(Byte),是计算机数据存储的一种计量单位.一个二进制位称为比特(bit),8个比特组成一个字节,也就是说一个字节可以用于区分256个整数(0~255).由此我们可以知道,字节本是面向计算机数据存储及传输的基本单位,后续的字符也就是以字节为单位存储的,不同编码的字符占用的字节数不同. 那么在Java中,除了存储的意义外,Java还将字节Byte作为一种基本数据类型,该数据类型在内存中占用一个字

  • 浅谈java中BigDecimal类的简单用法

    一.BigDecimal概述 ​ Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算.双精度浮点型变量double可以处理16位有效数,但在实际应用中,可能需要对更大或者更小的数进行运算和处理.一般情况下,对于那些不需要准确计算精度的数字,我们可以直接使用Float和Double处理,但是Double.valueOf(String) 和Float.valueOf(String)会丢失精度.所以开发中,如果我们需要精确计算的结果,则必须使用

  • 浅谈Java中FastJson的使用

    FastJson的使用 使用maven导入依赖包 <!--下边依赖跟aop没关系,只是项目中用到了 JSONObject,所以引入fastjson--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.70</version> </dependency> 常用方法:

  • 浅谈Java中ArrayList线程不安全怎么办

    ArrayList线程不安全怎么办? 有三种解决方法: 使用对应的 Vector 类,这个类中的所有方法都加上了 synchronized 关键字 就和 HashMap 和 HashTable 的关系一样 使用 Collections 提供的 synchronizedList 方法,将一个原本线程不安全的集合类转换为线程安全的,使用方法如下: List<Integer> list = Collections.synchronizedList(new ArrayList<>());

随机推荐