java并发机制的底层实现原理

  java代码在编译后变成java字节码,字节码被类加载器加载到jvm里,jvm执行字节码,最终须要转化为汇编指令在cpu上执行,java中所使用的并发机制依赖于jvm的实现和cpu指令。java

二、1 volatile的应用编程

  在多线程并发编程中,synchronized和volatile都扮演着重要的角色,volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的"可见性",可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读取到这个修改的值。若是volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,觉得它不会引发线程上下文的切换和调度。本文将深刻分析在硬件层面上的intel处理器是如何实现volatile的,经过深刻分析帮助咱们正确的使用volatile变量。
缓存

一、volatile实现原理安全

 咱们在x86处理器下经过工具获取JIT编译器生成的汇编指令来查看对volatile进行写操做时,cpu会作什么事情。java代码以下:
多线程

    instance = new Singleton();    //instance是volatile变量
架构

转变为汇编代码以下:并发

    0x01a3de1d: movb $0x0,0x1104800(%esi);0x01a3de24:lock $0x0,(%esp)
框架

有volatile修饰的共享变量进行写操做的时候会多出第二行汇编代码,经过查IA-32架构软件开发者手册可知,lock前缀的执行在多核处理器下会引起两件事情。jvm

1)、将当前处理器缓存行的数据写会系统内存ide

2)、这个写回内存的操做会使在其余cpu里缓存了该内存地址的数据无效

为了提升处理速度,处理器不直接和内存进行通讯,而是先将系统内存的数据读取到内部缓存(L一、L2或其余)后再进行操做,但操做完不知道什么时候会写到内存。若是对声明了volatile的变量进行写操做,jvm就会向处理器发送一条lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。可是就算写回到内存,若是其余处理器缓存的值仍是旧的,在执行计算操做就会有问题,因此,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每一个处理器经过嗅探在总线上传播的数据来检查本身缓存的值是否是过时了,当处理器发现本身缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操做的时候,会从新从系统内存中把数据读处处理器缓存中。

  下面具体讲解volatile的两条实现原则。

1)、lock前缀指令会引发处理器缓存写回内存。

  lock前缀指令致使在执行指令期间,声言处理器的LOCK#信号。在多处理器环境中,LOCK#信号确保在声言该信号期间,处理器能够独占任何共享内存。(由于它会锁住总线,致使其余cpu不能访问总线,不能访问总线就觉得着不能访问系统内存)。可是在最近的处理器里,LOCK#信号通常不锁总线,而是锁缓存,毕竟锁总线开销比较大,目前的处理器中,若是访问的内存区域已经缓存在处理器内部,则不会声言LOCK#信号。相反,它会锁定这块内存区域的缓存并写回到内存,并使用缓存一致性机制来确保修改的原子性,此操做被称为"缓存锁定",缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。

2)、一个处理器的缓存写回到内存会致使其余处理器的缓存无效。

  IA-32处理器和Intel64处理器使用MESI(修改、独占、共享、无效)控制协议去维护内部缓存和其余处理器缓存的一致性。在多核处理器系统中进行操做的时候,IA-32和Intel64处理器能嗅探其余处理器访问系统内存和它们的内部缓存。处理器使用嗅探基数保证它的内部缓存、系统内存和其余处理器的缓存的数据在总线上保持一致。如在Pentium和P6 family处理器中,若是经过嗅探一个处理器来监测其余处理器打算写回内存地址,而这个地址当前处于共享状态,那么正在嗅探的处理器将使它的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充。

二、2 synchronized的实现原理和应用

  在多线程并发编程中synchronized一直是元老级角色。

先来看看利用synchronized实现同步的基础:java中的每个对象均可以做为锁,具体表现为如下3种形式。

1)、对于普通同步方法,锁是当前实例对象

2)、对于静态同步方法,锁是当前类的Class对象

3)、对于同步方法块,锁是Synchonized括号里配置的 对象

  当一个线程试图访问同步代码块时,它首先必须获得锁,退出或者抛出异常时必须释放锁,那么锁到底存在哪里呢?所里面会存储哪些信息呢?

 从jvm规范中能够看到Synchonized在jvm里的实现原理,jvm基于进入和退出Monitor对象来实现方法同步和代码块同步,但二者的实现细节不同。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另一种方式实现的,细节在jvm规范里没有详细说明。可是,方法的同步一样可使用者两个指令来实现。

 monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit指令是插入到方法结束和异常处,jvm保证每一个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,而且一个monitor被持有后,它处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的全部权,即尝试得到对象的锁。


 synchronized用的锁是存在java对象头里的。


二、3 原子操做的实现原理

  原子(atomic)本意是"不能被进一步分割的最小粒子",而原子操做意为"不可能被中断的一个或一系列操做",在处理器上实现原子操做就变得有些复杂。

一、处理器如何实现原子操做

  a)、使用总线锁保证原子性

  b)、使用缓锁保证原子性

二、java如何实现原子操做

  在java中能够经过锁和循环CAS的方式来实现原子操做

  a)、使用循环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++;
    }

}

java1.5开始,jdk的并发包中提供了一些支持原子操做的类。 

 b)、CAS实现原子操做的三大问题

在java并发包中有一些并发框架也使用了循环CAS的方式来实现原子操做,CAS虽然很高效的解决了原子操做,可是CAS仍然存在三大问题。ABA问题,循环时间长开销大,以及只能保证一个共享变量的原子操做。

ABA问题:使用AtomicStampedReference来解决。

循环时间长开销大问题

只能保证一个共享变量的原子操做:能够把多个共享变量合并成一个共享变量来操做。

同时JDK提供AtomicReference类来保证引用对象之间的原子性,就能够把多个变量放在一个对象里来进行CAS操做。

 c)、使用锁机制来实现原子操做

   锁机制保证了只有得到锁的线程才可以操做锁定的内存区域。jvm内部实现了不少种锁机制,有偏向锁、轻量级锁和互斥锁。除了偏向锁,jvm实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式获取锁,当它退出同步块的时候使用循环CAS释放锁。

相关文章
相关标签/搜索