并发与高并发系列之线程安全性之原子性

开发 架构
当多个线程访问某个类时,不管运行环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的协同或者同步,这个类都能表现出正确的行为,那么这个类是线程安全的。

[[409053]]

本文转载自微信公众号「安琪拉的博客」,作者安琪拉。转载本文请联系安琪拉的博客公众号。

大家好,我是安琪拉,这是并发编程的第五集,完整大纲如下:

面试官:你好,你先自我介绍一下吧。

安琪拉:面试官你好,我是草丛三婊,最强中单,火球拥有者、不焚者,安琪拉,这是我的简历,请过目。

面试官:听前一个面试官说你Java并发这块掌握的不错,我们深入的交流一下;

安琪拉:好好好,可以交流的深入一点

面试官:什么是线程安全性?

安琪拉:这个问题第一次被问,但是个好问题。

当多个线程访问某个类时,不管运行环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的协同或者同步,这个类都能表现出正确的行为,那么这个类是线程安全的。

面试官:线程安全性有哪三大特点? 或者说线程不安全是由于什么引起的?

安琪拉:【太老套了吧,能不能来点新的】

线程不安全的原因:

当前的一个操作可能不是原子的,执行过程中会被打断,其他线程有能力修改共享变量的值,同时存在线程修改的值不是立即对其他线程可见的,因为线程有自己的执行空间,另外一点就是存在程序可能存在乱序执行的情况,单线程没问题,但是多个线程同时执行,线程共享的数据会出现错乱,以上说的自己问题归纳出线程安全需要保证的三个特性:

  • 原子性

提供互斥访问、同一时刻只能有一个线程在操作

  • 可见性

一个线程对主内存的修改可以及时地被其他线程看到

  • 有序性

有序性是指程序在执行的时候,程序的代码执行顺序和语句的顺序是一致的。(你可能会想难道还有不一致的,是的,因为存在指令重排序,为什么会有指令重排,因为性能优化的需要,比如把多次访问主存合并到一起执行比计算和访问主存交替访问更高效),重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

面试官:那你用过java.util.concurrent.atomic包下原子性相关的类吗?

安琪拉:用过的,Java提供了很多AtomicXXX相关的原子类,如下图所示:

面试官:能举个例子说明下用法吗?

安琪拉:比如存在并发,计数的场景,以netty为例,它的线程池工厂类如下:

nextId就是 AtomicInteger类型的。每次创建线程给线程命名的时候, 代码如下:

  1. public Thread newThread(Runnable r) { 
  2.   Thread t = this.newThread(new DefaultThreadFactory.DefaultRunnableDecorator(r), this.prefix + this.nextId.incrementAndGet()); 
  3.  
  4.   try { 
  5.     if (t.isDaemon()) { 
  6.       if (!this.daemon) { 
  7.         t.setDaemon(false); 
  8.       } 
  9.     } else if (this.daemon) { 
  10.       t.setDaemon(true); 
  11.     } 
  12.  
  13.     if (t.getPriority() != this.priority) { 
  14.       t.setPriority(this.priority); 
  15.     } 
  16.   } catch (Exception var4) { 
  17.   } 
  18.  
  19.   return t; 

通过 incrementAndGet 实现原子性的 +1。

面试官:如果不用AtomicInteger,就用普通的int 会有什么后果?

安琪拉:首先我们知道 +1 操作不是原子性的,可以分成这么几条指令:取数指令,将数据压入操作数栈,执行+1操作,赋值。

关于指令这块,扔个蓝。我们编译一段Java code 看一下。

代码和字节码指令分别为:

  1. public static int add(int a,int b){ 
  2.   int c = 0; 
  3.   c = a + b; 
  4.   return c; 

指令,对应的操作解释也有,如下:

  1. public static int add(intint); 
  2.     Code: 
  3.        0: iconst_0  //初始化常量0压入操作数栈顶 
  4.        1: istore_2  //弹出操作数栈栈顶元素,保存到局部变量表第2个位置 
  5.        2: iload_0   //复制a变量的值入栈 
  6.        3: iload_1   //复制b变量的值入栈 
  7.        4: iadd      //执行加操作,相加结果放在栈顶 
  8.        5: istore_2  //弹出操作数栈栈顶元素,保存到局部变量表第2个位置 
  9.        6: iload_2   //复制局部变量表第2个位置的值入栈 
  10.        7: ireturn   //弹栈,返回结果 

写这么多就是为了让大家明白 a += 1 这种操作它不是原子的,是有多条指令组成,真的不容易,快给我点个赞,好心人的蓝buff

面试官:那能跟我讲下Atomic 的实现原理吗?

安琪拉:【要开始卷了,到安琪拉最爱的源码环节】

  1. /** 
  2.      * Atomically increments by one the current value. 
  3.      * 
  4.      * @return the updated value 
  5.      */ 
  6. public final int incrementAndGet() { 
  7.   return unsafe.getAndAddInt(this, valueOffset, 1) + 1; 

代码很短,注释就一句话,原子性的增加当前值。

继续下探:

  1. public final int getAndAddInt(Object var1, long var2, int var4) { 
  2.   int var5; 
  3.   do { 
  4.     var5 = this.getIntVolatile(var1, var2); 
  5.   } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); 
  6.  
  7.   return var5; 

入参是三个值:var1、var2、var4 ,我们先看下这三个值分别是什么?

Val1:this ,也就是AtomicInteger 对象nextId

Val2:valueOffset 看下代码,我另外画了个图,我们知道一个对象存储空间由对象头和成员变量组成的,那valueOffset 就是成员变量value 在AtomicInteger 对象中的偏移量。

初学者可能会问,函数放在哪呢?函数都放在方法区,因为是属于类的,不是对象私有的。

  1. private static final Unsafe unsafe = Unsafe.getUnsafe(); 
  2. private static final long valueOffset; 
  3.  
  4. static { 
  5.   try { 
  6.     valueOffset = unsafe.objectFieldOffset 
  7.       (AtomicInteger.class.getDeclaredField("value")); 
  8.   } catch (Exception ex) { throw new Error(ex); } 
  9.  
  10. private volatile int value; 

Val4:1

那开始详细解释下,下面这段代码:

compareAndSwapInt 方法:比较val1(AtomicInteger对象)的var2(valueOffset偏移量)的值与var5(原始值)是否相等,如果相等,让值更新成var5(原始值) + val4(1)

  1. //val1: nextId  val2: valueOffset val4: 1 
  2. public final int getAndAddInt(Object var1, long var2, int var4) { 
  3.   int var5; //临时变量 
  4.   do { 
  5.     var5 = this.getIntVolatile(var1, var2); //这是个native方法,获取value的值 
  6.     //比较val1(AtomicInteger对象)的var2(valueOffset偏移量)的值与var5(原始值)是否相等,如果相等,让值更新成var5(原始值) + val4(1) 
  7.   } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));  
  8.    
  9.   return var5; 
  10.  
  11. public native int getIntVolatile(Object var1, long var2); 

compareAndSwapInt 就是Java中非常重要,也是非常出名的CAS操作,比较并交换,并发底层框架用到的地方很多。

compareAndSwapInt 会返回CAS支持状态,如果执行失败,会循环执行,直到成功。

失败的原因一般是同时有别的线程修改了这个变量的值,所以比较的时候不相等,下次执行会获取最新值执行CAS。

。。。。嘤嘤嘤,打字好累啊,先写到这,要去吃自助餐了,明天再写可见性和有序性。 

 

责任编辑:武晓燕 来源: 安琪拉的博客
相关推荐

2021-05-16 17:14:30

线程安全性

2021-01-12 07:39:48

线程线程安全

2023-01-05 12:30:32

Redis

2024-02-26 08:33:51

并发编程活跃性安全性

2009-02-12 09:55:28

2022-10-13 06:46:05

Dapr访问控制策略

2011-01-04 16:20:26

linux安全性

2020-09-04 10:29:47

Java线程池并发

2023-04-04 07:06:21

2023-04-02 09:40:29

2024-09-27 14:45:30

2013-08-22 09:16:01

移动终端安全移动安全移动策略

2010-06-03 15:23:48

2009-11-30 09:41:38

2011-07-22 13:52:46

2011-08-22 14:19:23

linuxUNIXwrite

2010-01-11 10:43:16

应用程序安全性

2010-01-23 20:34:02

企业网络应用程序安全

2021-06-07 17:51:29

并发高并发编程

2016-11-25 00:45:37

队列数据
点赞
收藏

51CTO技术栈公众号