`
fantasy
  • 浏览: 510128 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

聊聊并发(五)原子操作的实现原理

阅读更多

 

本文属于作者原创,原文发表于InfoQ:http://www.infoq.com/cn/articles/atomic-operation

更多并发编程文章,http://ifeve.com/?p=291

1    引言

原子(atom)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。在多处理器上实现原子操作就变得有点复杂。本文让我们一起来聊一聊在Inter处理器和Java里是如何实现原子操作的。

 

2    术语定义

术语名称 英文 解释
缓存行 Cache line 缓存的最小操作单位
比较并交换 Compare and Swap CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。
CPU流水线 CPU pipeline CPU流水线的工作方式就象工业生产上的装配流水线,在CPU中由5~6个不同功能的电路单元组成一条指令处理流水线,然后将一条X86指令分成5~6步后再由这些电路单元分别执行,这样就能实现在一个CPU时钟周期完成一条指令,因此提高CPU的运算速度。
内存顺序冲突 Memory order violation 内存顺序冲突一般是由假共享引起,假共享是指多个CPU同时修改同一个缓存行的不同部分而引起其中一个CPU的操作无效,当出现这个内存顺序冲突时,CPU必须清空流水线。

3    处理器如何实现原子操作

32位IA-32处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。

3.1   处理器自动保证基本内存操作的原子性

首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存当中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。奔腾6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的,但是复杂的内存操作处理器不能自动保证其原子性,比如跨总线宽度,跨多个缓存行,跨页表的访问。但是处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。

3.2   使用总线锁保证原子性

第一个机制是通过总线锁保证原子性。如果多个处理器同时对共享变量进行读改写(i++就是经典的读改写操作)操作,那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致,举个例子:如果i=1,我们进行两次i++操作,我们期望的结果是3,但是有可能结果是2。如下图
1

(例1)

原因是有可能多个处理器同时从各自的缓存中读取变量i,分别进行加一操作,然后分别写入系统内存当中。那么想要保证读改写共享变量的操作是原子的,就必须保证CPU1读改写共享变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。

处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。

3.3 使用缓存锁保证原子性

第二个机制是通过缓存锁定保证原子性。在同一时刻我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,最近的处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。

频繁使用的内存会缓存在处理器的L1,L2和L3高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁,在奔腾6和最近的处理器中可以使用“缓存锁定”的方式来实现复杂的原子性。所谓“缓存锁定”就是如果缓存在处理器缓存行中内存区域在LOCK操作期间被锁定,当它执行锁操作回写内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时会起缓存行无效,在例1中,当CPU1修改缓存行中的i时使用缓存锁定,那么CPU2就不能同时缓存了i的缓存行。

但是有两种情况下处理器不会使用缓存锁定。第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line),则处理器会调用总线锁定。第二种情况是:有些处理器不支持缓存锁定。对于Inter486和奔腾处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。

以上两个机制我们可以通过Inter处理器提供了很多LOCK前缀的指令来实现。比如位测试和修改指令BTS,BTR,BTC,交换指令XADD,CMPXCHG和其他一些操作数和逻辑指令,比如ADD(加),OR(或)等,被这些指令操作的内存区域就会加锁,导致其他处理器不能同时访问它。

4    JAVA如何实现原子操作

在java中可以通过循环CAS的方式来实现原子操作。

4.1 使用循环CAS实现原子操作

JVM中的CAS操作正是利用了上一节中提到的处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止,以下代码实现了一个基于CAS线程安全的计数器方法safeCount和一个非线程安全的计数器count。

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
       private AtomicInteger atomicI = new AtomicInteger(0);
 
       private int i = 0;
 
       public static void main(String[] args) {
 
              final Counter cas = new Counter();
 
              List<Thread> ts = new ArrayList<Thread>(600);
 
              long start = System.currentTimeMillis();
 
              for (int j = 0; j < 100; j++) {
 
                     Thread t = new Thread(new Runnable() {
 
                            @Override
 
                            public void run() {
 
                                   for (int i = 0; i < 10000; i++) {
 
                                          cas.count();
 
                                          cas.safeCount();
 
                                   }
 
                            }
 
                     });
 
                     ts.add(t);
 
              }
 
              for (Thread t : ts) {
 
                     t.start();
 
              }
 
       // 等待所有线程执行完成
 
              for (Thread t : ts) {
 
                     try {
 
                            t.join();
 
                     } catch (InterruptedException e) {
 
                            e.printStackTrace();
 
                     }
 
              }
 
              System.out.println(cas.i);
 
              System.out.println(cas.atomicI.get());
 
              System.out.println(System.currentTimeMillis() - start);
 
       }
 
       /**
 
        * 使用CAS实现线程安全计数器
 
        */
 
       private void safeCount() {
 
              for (;;) {
 
                     int i = atomicI.get();
 
                     boolean suc = atomicI.compareAndSet(i, ++i);
 
                     if (suc) {
 
                            break;
 
                     }
 
              }
 
       }
 
       /**
 
        * 非线程安全计数器
 
        */
 
       private void count() {
 
              i++;
 
       }
 
}

从Java1.5开始JDK的并发包里提供了一些类来支持原子操作,如AtomicBoolean(用原子方式更新的 boolean 值),AtomicInteger(用原子方式更新的 int 值),AtomicLong(用原子方式更新的 long 值),这些原子包装类还提供了有用的工具方法,比如以原子的方式将当前值自增1和自减1。

在Java并发包中有一些并发框架也使用了自旋CAS的方式来实现原子操作,比如LinkedTransferQueue类的Xfer方法。CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作。

  1. ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

1
2
3
4
5
6
7
8
9
public boolean compareAndSet(
               V      expectedReference,//预期引用
 
               V      newReference,//更新后的引用
 
              int    expectedStamp, //预期标志
 
              int    newStamp //更新后的标志
)
  1. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
  2. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

4.2 使用锁机制实现原子操作

锁机制保证了只有获得锁的线程能够操作锁定的内存区域。JVM内部实现了很多种锁机制,有偏向锁,轻量级锁和互斥锁,有意思的是除了偏向锁,JVM实现锁的方式都用到的循环CAS,当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。详细说明可以参见文章Java SE1.6中的Synchronized

5      参考资料

  1. Java SE1.6中的Synchronized
  2. Intel 64和IA-32架构软件开发人员手册
  3. 深入分析Volatile的实现原理

(全文完)

您可能感兴趣的文章

分享到:
评论

相关推荐

    聊聊并发系列文章

    5. 聊聊并发(五)原子操作的实现原理 6. 聊聊并发(六)ConcurrentLinkedQueue的实现原理 7. 聊聊并发(七)Java中的阻塞队列 8. 聊聊并发(八)Fork/Join框架介绍 9. 聊聊并发(九)Java中的CopyOnWrite容器 10. ...

    聊聊并发(5)原子操作的实现原理Java开发Java经验技

    在并发编程领域,原子操作是实现线程安全和高效代码的关键技术之一。本文将深入探讨Java开发中的原子操作实现原理,以及如何利用这些知识来优化Java应用。 首先,我们需要理解什么是原子操作。原子操作是指不可分割...

    聊聊并发(一)深入分析Volatile的实现原理

    本篇文章将深入分析Volatile的实现原理,结合`LinkedTransferQueue`和`TransferQueue`这两个与并发相关的Java源码,探讨其在多线程环境中的应用。 首先,我们需要理解Java内存模型(JMM,Java Memory Model),它是...

    聊聊并发(6)ConcurrentLinkedQueue的

    【标题】:“聊聊并发(6)ConcurrentLinkedQueue的实现原理分析” 【正文】: 并发编程是现代软件开发中的重要组成部分,特别是在多核处理器和分布式系统中,有效地处理并发能够显著提升系统的性能和响应能力。...

    聊聊并发(4)深入分析ConcurrentHashMapJ

    相比`synchronized HashMap`或`Collections.synchronizedMap`包装的`HashMap`,`ConcurrentHashMap`通过分割锁和操作的粒度细化,实现了更高的并发性能。 1. **分段锁机制**:`ConcurrentHashMap`将整个哈希表分为...

    PHP+Redis链表解决高并发下商品超卖问题(实现原理及步骤)

    上一篇文章聊了一下使用Redis事务来解决高并发商品超卖问题,今天我们来聊一下使用Redis链表来解决高并发商品超卖问题。 实现原理 使用redis链表来做,因为pop操作是原子的,即使有很多用户同时到达,也是依次执行,...

    聊聊数据库中的那些锁.docx

    聊数据库中的那些锁主要涉及数据库事务的ACID属性和锁定机制,这是数据库管理系统中确保数据一致性、并发控制和事务安全的重要概念。 **ACID属性** 1. **原子性(Atomicity)**:原子性保证了事务的不可分割性,一个...

    20_来聊聊redis的线程模型吧?为啥单线程还能有很高的效率?.zip

    此外,由于所有操作都在同一个线程中执行,Redis可以确保命令的顺序执行,这对于一些需要事务或原子性的操作来说非常重要。 Redis处理请求的方式也很高效。它将数据存储在内存中,读写速度远超磁盘,且通过LRU...

    redis分布式锁实现

    1. 原子性:使用`lua`脚本确保`SETNX`和`EXPIRE`操作的原子性。 2. 防止死锁:设置合理的超时时间,并提供自动或手动的锁续租机制。 3. 公平性:优化锁的获取策略,避免客户端长时间无法获取锁。 4. 容错性:利用...

    聊聊Java和CPU的关系

    然而,volatile 无法保证原子性,例如对于`i++`这样的操作,需要借助锁或原子类如`AtomicInteger`来确保并发一致性。 其次,Java中的锁机制,如`synchronized`和`java.util.concurrent`包中的工具类,利用CPU的锁...

    Java多线程与线程安全实践-基于Http协议的断点续传

    3. **原子操作类**:Java并发包(`java.util.concurrent.atomic`)提供了原子操作类,如`AtomicInteger`,它们提供了一种无锁的、线程安全的更新方式,适用于简单的原子操作。 4. **Locks**:Java的`java.util....

    聊聊MySQL事务的特性和隔离级别

    事务是一组SQL查询操作,这些操作被视为一个整体,要么全部执行,要么全部回滚,以保证原子性。事务的四大特性——原子性、一致性、隔离性和持久性,通常被称为ACID特性。 1. 原子性(Atomicity):事务中的每个...

    聊聊MySQL中的存储引擎

    它支持ACID(原子性、一致性、隔离性、持久性)事务,并提供了行级锁定以提高并发性能。 - InnoDB还支持外键约束,保证了数据库的参照完整性,有助于维护数据的一致性。 - 行级锁定和非锁定读是InnoDB的两大特性,...

    zookeeper-3.4.10.tar.gz.zip

    3. **原子操作**:Zookeeper的所有操作都是原子性的,即一次操作要么全部完成,要么全部不完成,不存在部分完成的情况。这确保了在并发环境下数据的一致性。 4. **会话(Session)**:客户端与Zookeeper服务器之间...

    在linux下开发的聊天系统,公聊,私聊,多线程

    数据库操作需要遵循ACID原则(原子性、一致性、隔离性和持久性),确保数据的一致性和完整性。 9. **测试与调试**:开发过程中,单元测试、集成测试和压力测试是必不可少的,以确保系统在各种情况下都能正常工作。...

    Java进阶教程解密JVM视频教程

    掌握条件分支、循环控制、异常处理、构造方法在字节码级别的实现原理,利用HSDB工具理解多态原理。还会涉及从编译期的语法糖处理,到类加载的各个阶段,直至运行期的各项优化的详细讲解。最后不要错过方法反射优化的...

    MyQQ最新版(Java版高仿QQ聊天即时通软件)带mysql数据库

    MySQL的ACID(原子性、一致性、隔离性、持久性)特性确保了数据的一致性和完整性,即使在并发访问的情况下也能保证数据的准确无误。 在Java技术栈中,MyQQ可能采用了Spring Boot框架来构建服务,利用MyBatis或者JPA...

    MicrochatSystem

    微聊系统可能采用了JavaFX或Swing等库来构建图形用户界面,确保操作直观、响应迅速。同时,考虑到不同设备和屏幕尺寸,可能进行了响应式设计,以适应多种设备环境。 ### 7. 开源与社区贡献 "每个人都可以向该项目...

    CSC174-Advanced_Database_Management_Systems

    5. 并发控制:理解并发操作可能导致的问题,如死锁和活锁,并学习如何通过锁定机制、多版本并发控制(MVCC)和两阶段锁定(2PL)等策略解决这些问题。 6. 数据库恢复:研究事务的ACID(原子性、一致性、隔离性和...

    聊一聊MyISAM和InnoDB的区别

    相比之下,MyISAM只能锁定整个表,当有写操作时,所有读操作都会被阻塞,反之亦然,这在高并发环境下可能导致性能瓶颈。 InnoDB还支持外键,这是保持数据库引用完整性的关键,允许在不同表之间建立关联。而MyISAM不...

Global site tag (gtag.js) - Google Analytics