现在的计算机的频率已经很难提升,但现在/将来的趋势是多核,如Nvidia的强大的GPU [8],Intel,ARM也在努力提升核心的数量。所以在现代计算机中,并发编程将成为趋势。 Java内存模型(JMM, Java Memory Model)Java内存模型如图: [22] 根据Java Language Specification中的说明, jvm系统中存在一个主内存(Main Memory或Java Heap Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。 [18][22] public class Singleton { private volatile static Singleton instance; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } public static void main(String[] args) { Singleton.getInstance(); } } 编译后,这段代码对instance变量赋值部分代码如下所示: 0x01a3de0f: mov $0x3375cdb0,%esi ;...beb0cd75 33 ; {oop('Singleton')} 0x01a3de14: mov %eax,0x150(%esi) ;...89865001 0000 0x01a3de1a: shr $0x9,%esi ;...c1ee09 0x01a3de1d: movb $0x0,0x1104800(%esi) ;...c6860048 100100 0x01a3de24: lock addl $0x0,(%esp) ;...f0830424 00 ;*putstatic instance ; - Singleton::getInstance@24 通过对比发现,关键变化在于有volatile修饰的变量,赋值后(前面mov %eax,0x150(%esi)这句便是赋值操作)多执行了一个“lock addl $0x0,(%esp)”操作,这个操作相当于一个内存屏障,只有一个CPU访问内存时,并不需要内存屏障;但如果有两个或更多CPU访问同一块内存,且其中有一个在观测另一个,就需要内存屏障来保证一致性了。 伪共享(False Sharing)我更喜欢称之为“错误共享”,CPU(Intel 4 core)结构Sample: [16] CPU访问缓存及内存的速度比较: [25] 缓存一致性协议(MESI)在MESI协议中,每个Cache line有4个状态,可用2个bit表示,它们分别是 [26]:
状态迁移图: 锁(Locking )在Java中通过锁来实现线程安全,Java通过monitorenter与monitorexit这两个控制多线程同步的bytecode原语实现锁,是JVM依赖操作系统互斥(mutex)来实现的,所以在JVM中锁的性能相对较大。互斥是一种会导致线程挂起,并在较短的时间内又需要重新调度回原线程的,是较为消耗资源的操作。 [13] Java中对锁的优化Java对象头结构(Java Object Header) [10] [Mark Word ] 32/64 bits [Class Metadata Address] 32/64 bits [Array Length ] 32/64 bits 对象为两个字(Word),数组为3个字(增加了一个Array Length) 对象的 Mark Word,同步相关的状态(32 bits): [ bitfields ][ tag bits ] [ state ] ----------------------------------------------------------------------------------- [ hash (25)][ age (4)][ 0 (1)][ 01 (2)] [ unlocked ] [ ptr to lock record (29)][ 00 (2)] [ lightweight locked ] [ ptr to heavyweight record (29)][ 10 (2)] [ inflated ] [ (29)][ 11 (2)] [ marked for GC ] [ thread id (23)][ epoch (2)][ age (4)][ 1 (1)][ 01 (2)] [ biasable ] 通过参数-XX:+UseBiasedLocking判断当前是否使用偏向锁(在JDK6,7中默认打开该参数),同步锁转换图如下: [17] 轻量级锁(Lightweight Locking)早期的研究表明,在Java中的大部分锁是不存在竞争的,基于这个研究结果发明了轻量级锁(当发生竞争时则膨胀到重量级锁) [19]。轻量级锁本意是为了减少多线程进入互斥的几率,并不是要替代互斥。它利用了CPU原语Compare-And-Swap(CAS,汇编指令CMPXCHG),尝试在进入互斥前,进行补救。 [13] 偏向锁(Biased Locking)IBM位于东京的实验室发现,Java程序中的锁不但竞争很少,而且是不共享的,基于这个发现则发明了偏向锁 [20]。偏向锁性能非常好,当正常工作时仅需约2~4个CPU周期。 [11] 锁膨胀(Lock Coarsening)如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(膨胀)到整个操作序列的外部,这样只需要加锁一次就可以了。参数-XX:+EliminateLocks。 [14][21] public Point moveNW (Point p) { synchronized (p) { p.x += 10; } synchronized (p) { p.y += 10; } } public Point moveNW (Point p) { synchronized (p) { p.x += 10; p.y += 10; } } 锁削除(Lock Elision through Escape Analysis)锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。锁削除的主要判定依据来源于逃逸分析(参数:-XX:+DoEscapeAnalysis)的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。 [14][21] 嵌套锁削除(Eliminate nested locks of the same object)对于嵌套锁同一个对象时,因为我们都是可重入锁,所以里面的锁其实是可以削除的,而嵌套锁削除则是对这种场景进行优化。如代码: synchronized (reader) { // 同步对象,持有锁:reader if (reader.getCount() > 0) { // 调用同步方法:getCount(),但程序已经持有该锁:reader reader.incrementReserveCount(1); } 显然上例中“reader.getCount()”的锁是可以忽略掉的。参数:-XX:+EliminateNestedLocks。[30] 自旋锁与自适应自旋(Spinning Locking & Adaptive Spinning)互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一会”,但不放弃处理器的执行时 间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只须让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。 可重入锁(ReentrantLock)java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。 ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。) [2] 排队自旋锁(FIFO Ticket Spinlock)[Linux]排队自旋锁(FIFO Ticket Spinlock)是 Linux 内核 2.6.25 版本引入的一种新型自旋锁,它通过保存执行线程申请锁的顺序信息解决了传统自旋锁的“不公平”问题。排队自旋锁的代码由 Linux 内核开发者 Nick Piggin 实现,目前只针对 x86 体系结构(包括 IA32 和 x86_64)。 [23] 并发支持JVM@Contended Sample: [28][29] . @Contended public static class ContendedTest2 { private Object plainField1; private Object plainField2; private Object plainField3; private Object plainField4; } JUC(java.util.concurrent)JDK自带 rt.jar(或classes.jar) Disruptorhttp://code.google.com/p/disruptor/
参考资料[1]. http://www.infoq.com/cn/articles/zzm-java-hsdis-jvm |
||||||||||