JAVA并发机制底层实现原理

voliate详解

1)voliate的内存可见性
voliate对于JAVA并发编程而言是一个用于保证共享变量在各线程间可见的一个重要同步原语。而其实现该功能的底层原理如下:
对于voliate申明的的变量在编译器生成汇编代码时,会在其前加上“Lock”的汇编指令,该“Lock”前缀的汇编指令用于告诉编译器,当线程要修改共享变量时,会把其修改的值从其线程自身的工作内存中刷新到主存中。同时依据缓存一致性协议(MESI)用于告知其他线程对该共享变量缓存的无效,迫使要读取该变量时必须从主存中读取。从而保证了某一线程对共享变量的修改能及时被其他线程可见。
其实为了达到以上的效果,在细节上还要依靠修改voliate变量时的一系列操作的原子性,而实现这一原子性的底层实现就是“总线锁定”或者“内存锁定”
总线锁定:就是当线程要修改voliate变量时,总线会被锁定,迫使其他线程无法依靠总线访问自身的工作内存和主存。只有带修改共享变量的线程把工作内存中修改的值刷新到主存后,总线才解锁。同时在刷新后,也会依据缓存一致性协议(MESI)告知其他线程自身对该voliate变量的缓存无效,使用时只能重新从主存里读取该变量的新值。
内存锁定:内存锁定较“总线锁定”而言其开销小,原理是但线程修改共享变量时,只会锁定缓存了该共享变量的缓存行,而不是锁定总线,因此其他线程还是可以依靠总线操作其他缓存行里的数据和主存。之后内存锁定的原理和步骤同总线锁定一致,也会刷新的主存和告知其他线程缓存无效。
总结:所以,voliate修饰的变量的内存可见性原理,底层实现原理就是依靠“缓存一致性协议”和“总线锁定”或者“内存锁定”。
2)voliate的有序性(禁止指令排序)
对于voliate的有序其实指的是对voliate变量的操作会禁止一些编译器和处理器对其的排序,而什么又是指令重排呢?其实要涉及JAVA内存模型的涉及(JMM),这里先简单说下,其实就是一个JMM设计时的原则,编译器和处理器在保证程序语义一致的前提下会尽量的提高指令执行的并发度,及提高执行效率。因此代码在源程序上的执行顺序和实际上的各语句的执行顺序可能不一致。因此在多线程的条件下,因为指令重排可能会造成多线程的内存可见性问题。因此对于voliate变量的操作,JMM就规定了一些规则来限制对其的重排,而这些规则的底层实现其实是在对voliate变量操作的汇编指令的前后加上一些内存屏障,比如对voliate变量的读取时,编译器会在该操作的后面加上LoadLoad和LoadStore的内存屏障,意思是指该voliate变量的加载会优先于其voliate变量操作后面的所有加载指令,和存储指令(就是把数据从工作 内存刷新大主存里)。

总结:总之,如果要实现内存可见性就要求操作具有原子性、指令不被排序(程序顺序执行,没有随机性)满足了这两点就可以实现内存可见性,其实这两点是顺序内存一致性模型的特点(之后说JMM的时候会提及)。而voliate底层正是通过"内存锁定"达到操作的原子性、和加内存屏障来实现(程序的关键处顺序执行,禁止指令重排)。

synchronized实现原理

1)synchronized实现原理
对于synchronized同步原语,都知道它是用于多线程执行时同步用的,而他底层是如何实现的呢?在了解它底层实现原理前,需要知道一些概念:
管程:monitor,一种数据结构,用于监视一段代码,确保该代码只能有一个线程执行,它会对等待获取锁的线程放入其同步队列中,在锁释放后会从同步队列中唤醒一个线程去获取锁、
在知道了管程的概念后,synchronized的原理其实也比较容易理解,就是对于synchronized修饰的代码块,在汇编时编译器会在代码块的起始处生成“moniterenter”的汇编指令和在代码块末尾处生成“moniterexit”的汇编指令。
moniterenter:该汇编指令会让线程执行同步代码块前去获取锁的宿主,即对象关联的监视器(monitor)的使用权,获取成功时会调用ObjectMonitor的enter方法改变monitor的_owner属性和count属性,_owner属性指向获取锁的线程,获取锁成功次数就是count记录。当但获取锁失败时,即锁已经别其他线程获取了,monitor就会时失败的线程直接阻塞进入它的同步队列中,等待释放锁的线程的唤醒。注意这一阻塞动作会造成线程的上下文的切换开销和由于JAVA多线程实现模型原理,会切换到内核态去阻塞线程,这也会造成开销。因此才有synchronized是重量级锁的称号。
monitereixt:该汇编指令就是释放锁,底层动作就是重置monitor的_owner和count的属性,和唤醒同步队列里的阻塞线程。
2)synchronized锁JDK1.6的优化
正是由于,获取锁失败后就会被阻塞,造成的开销大,所以JDK1.6对synchronized进行了优化,引入了“偏向锁”、“轻量级锁”,其实优化的原理也很好理解就是,在原始synchronized的动作前加入了“偏向锁”、“轻量级锁”的动作,依据锁竞争情况来逐步升级锁的开销:无锁<偏向锁<轻量级锁<重量级锁
轻量级锁:它在获取锁时,会把锁宿主的对象头中的Mark World拷贝到线程中,并使用CAS操作来让锁宿主对象头中的Mark World指向线程里的锁记录,成功就获取锁;失败那么就通过自旋一段时间的CAS获取锁的操作来重新尝试,还是失败后才会阻塞线程。释放锁的时候,会通过CAS来将之前线程里Mark World的拷贝重新替换到对象头中的Mark World,成功就释放锁成功;失败就说明当前有线程在竞争锁,因此此时轻量级锁会膨胀成为重量级锁,因此才会阻塞获取锁失败的线程,和在释放锁成功后回去同步队列中唤醒阻塞的线程。
总结:轻量级锁的实现正是避免立即的阻塞,通过CAS尝试减小了开销。但它在有竞争时会膨胀为重量级锁,即原始的synchronzied的操作。
偏向锁:偏向锁是对轻量级锁的优化,同时它是基于一个结论:在大多的时候线程获取锁时是没有竞争的并且锁往往都是由同一个线程重复获取。因此当线程获取锁时会通过CAS操作来将对象头记录获取成功的线程的ID,之后该线程在获取该锁时就不用再采用CAS操作了,而是直接比对对象头中是否记录了该线程的ID。记录就直接获取锁,没有才CAS获取。释放锁只有当有线程来竞争锁才会让获取偏向锁的线程去释放锁,它会先暂停拥有偏向锁的线程,将对象头中的线程ID设为空,之后再唤醒线程。
总结:偏向锁就是通过减少不必要CAS操作来减小开销。
3)synchronized的可见性
对于voliate变量具有内存可见性那么对于synchronized修饰后的代码块呢?要知道要用内存可见性,依据顺序内存模型必须具备:一、程序顺序执行;二、操作的原子性且操作的立即可见(立即刷新到主存)。synchronized通过加锁,实现了代码块每个时刻只有一个线程执行,具备了原子性(多个线程不能同时操作该同步代码块),因此也就是单线程执行,所以尽管同步代码中会有指令重排,但由于是单线程执行,所以执行语义不变。也就是达到了 内存可见

你可能感兴趣的:(JAVA并发编程)