当前位置:天才代写 > tutorial > JAVA 教程 > Java原子操纵的实现道理

Java原子操纵的实现道理

2017-11-02 08:00 星期四 所属: JAVA 教程 浏览:39

副标题#e#

1. 引言

原子(atom)本意是“不能被进一步支解的最小粒子”,而原子操纵(atomic operation)意为"不行被间断的一个或一系列操纵" 。在多处理惩罚器上实现原子操纵就变得有点 巨大。本文让我们一起来聊一聊在Intel处理惩罚器和Java里是如何实现原子操纵的。

2. 术语界说

Java原子哄骗的实现原理

3. 处理惩罚器如何实现原子操纵

32位IA-32处理惩罚器利用基于对缓存加锁或总线加锁的方法来 实现多处理惩罚器之间的原子操纵。

3.1 处理惩罚器自动担保根基内存操纵的原子性

首先处理惩罚器 会自动担保根基的内存操纵的原子性。处理惩罚器担保从系统内存傍边读取可能写入一个字节是原子的,意思 是当一个处理惩罚器读取一个字节时,其他处理惩罚器不能会见这个字节的内存地点。奔驰6和最新的处理惩罚器能自 动担保单处理惩罚器对同一个缓存行里举办16/32/64位的操纵是原子的,可是巨大的内存操纵处理惩罚器不能自动 担保其原子性,好比跨总线宽度,跨多个缓存行,跨页表的会见。可是处理惩罚器提供总线锁定缓和存锁定两 个机制来担保巨大内存操纵的原子性。

3.2 利用总线锁担保原子性

第一个机制是通过总 线锁担保原子性。假如多个处理惩罚器同时对共享变量举办读改写(i++就是经典的读改写操纵)操纵,那么 共享变量就会被多个处理惩罚器同时举办操纵,这样读改写操纵就不是原子的,操纵完之后共享变量的值会和 期望的纷歧致,举个例子:假如i=1,我们举办两次i++操纵,我们期望的功效是3,可是有大概功效是2。 如下图

Java原子哄骗的实现原理

(例1)

原因是有大概多个处理惩罚器同时从各自的缓存中读取变量i,别离 举办加一操纵,然后别离写入系统内存傍边。那么想要担保读改写共享变量的操纵是原子的,就必需担保 CPU1读改写共享变量的时候,CPU2不能操纵缓存了该共享变量内存地点的缓存。

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


#p#副标题#e#

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。

   
public class Counter {
    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++;
    }
}

查察本栏目

#p#副标题#e#

#p#分页标题#e#

在java并发包中有一些并发框架也利用了自旋CAS的方法来实现原子操纵,好比 LinkedTransferQueue类的Xfer要领。CAS固然很高效的办理原子操纵,可是CAS仍然存在三大问题。ABA问 题,轮回时间长开销大和只能担保一个共享变量的原子操纵。

ABA问题。因为CAS需要在操纵值的时候查抄下值有没有产生变革,假如没有产生变革则更新,可是如 果一个值本来是A,酿成了B,又酿成了A,那么利用CAS举办查抄时会发明它的值没有产生变革,可是实际 上却变革了。ABA问题的办理思路就是利用版本号。在变量前面追加上版本号,每次变量更新的时候把版 本号加一,那么A-B-A 就会酿成1A-2B-3A。 从Java1.5开始JDK的atomic包里提供了一个类 AtomicStampedReference来办理ABA问题。这个类的compareAndSet要领浸染是首先查抄当前引用是否便是 预期引用,而且当前符号是否便是预期符号,假如全部相等,则以原子方法将该引用和该符号的值配置为 给定的更新值。

public boolean compareAndSet
        (V      expectedReference,//预期引用
         V      newReference,//更新后的引用
        int    expectedStamp, //预期符号
        int    newStamp) //更新后的符号
  

轮回时间长开销大。自旋CAS假如长时间不乐成,会给CPU带来很是大的执行开销。假如JVM能支持处 理器提供的pause指令那么效率会有必然的晋升,pause指令有两个浸染,第一它可以延迟流水线执行指令 (de-pipeline),使CPU不会耗损过多的执行资源,延迟的时间取决于详细实现的版本,在一些处理惩罚器上 延迟时间是零。第二它可以制止在退出轮回的时候因内存顺序斗嘴(memory order violation)而引起 CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

只能担保一个共享变量的原子操纵。当对一个共享变量执行操纵时,我们可以利用轮回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。

 

    关键字:


天才代写-代写联系方式