[Home]   [TOC]

Study_Java_HotSpot_Concurrent  
Concurrent in Java, HotSpot
Updated Feb 24, 2014 by jht5...@gmail.com

现在的计算机的频率已经很难提升,但现在/将来的趋势是多核,如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]
在JMM中,多线程间共享数据需要内存屏障 内存屏障(Memory barrier),也被称为 membar 、 memory fence 或 fence instruction,是一类强制CPU或编译器使用对内存有序操作的指令。 [15]
鉴于CPU和编译器都可能对指令进行重排序,所以我将内存屏障分为两种:
* 编译器级别的:防止编译器对指令进行重排序,例如GCC的asm volatile("" ::: "memory")等等。
* 处理器级别的:防止处理器对指令进行重排序,例如X86的lock,lfence(),sfenec()等等 [24]
对于在内存屏障的实现参看如下代码:

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访问同一块内存,且其中有一个在观测另一个,就需要内存屏障来保证一致性了。
指令“addl $0x0,(%esp)”显然是一个空操作,关键在于lock前缀,查询IA32手册,它的作用是使得本CPU的Cache写入了内存,该写入动作也会引起别的CPU invalidate其Cache。所以通过这样一个空操作,可让前面volatile变量的修改对其他CPU立即可见。
那为何说它禁止指令重排序呢?从硬件架构上讲,指令重排序是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理。但并不是说指令任意重排,CPU需要能正确处理指令依赖情况保障程序能得出正确的执行结果。譬如指令1把地址A中的值加10,指令2把地址A中的值乘以2,指令3把地址B中的值减去3,这时指令1和指令2是有依赖的,它们之间的顺序不能重排——(A+10)*2A*2+10显然不相等,但指令3可以重排到指令1、2之前或者中间,只要保证CPU执行后面依赖到A、B值的操作时能获取到正确的A和B值即可。所以在本内CPU中,重排序看起来依然是有序的。因此,lock addl $0x0,(%esp)指令把修改同步到内存时,所有之前的操作都已经执行完成,这样便形成了“指令重排序无法越过内存屏障”的效果。 [1]

伪共享(False Sharing)

我更喜欢称之为“错误共享”,CPU(Intel 4 core)结构Sample: [16]


内存在缓存系统中常以缓存行(cache line)为单位缓存。缓存行的大小则是2的指数,通常在 32~256 字节范围内,最常见的大小为 64 字节。伪共享则是一个术语,在同时修改相关缓存行的独立变量时,在不知不觉的情况下影响不同线程的性能表现。 [9]
详细图例如下:

CPU访问缓存及内存的速度比较: [25]

缓存一致性协议(MESI)

在MESI协议中,每个Cache line有4个状态,可用2个bit表示,它们分别是 [26]

状态 描述
M(Modified) 这行数据有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。
E(Exclusive) 这行数据有效,数据和内存中的数据一致,数据只存在于本Cache中。
S(Shared) 这行数据有效,数据和内存中的数据一致,数据存在于很多Cache中。
I(Invalid) 这行数据无效

状态迁移图:

锁(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]
偏向锁取消: [10]
1. 退回到轻量级锁
2. 等待一个全局Safepoint(无字节码执行)
3. 遍历线程栈,枚举锁信息
4. 更新对象头锁信息

锁膨胀(Lock Coarsening)

如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(膨胀)到整个操作序列的外部,这样只需要加锁一次就可以了。参数-XX:+EliminateLocks[14][21]
锁膨胀的例子如下 [12]:

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)

互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一会”,但不放弃处理器的执行时 间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只须让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
在JDK 1.6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间, 比如100个循环。另一方面,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虚拟机就会变得越来越“聪明”了。 [20]

可重入锁(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)

Disruptor

http://code.google.com/p/disruptor/
High Performance Inter-Thread Messaging Library



参考资料

[1]. http://www.infoq.com/cn/articles/zzm-java-hsdis-jvm
[2]. http://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html
[3]. http://kenwublog.com/illustrate-memory-reordering-in-cpu
[4]. http://hi.baidu.com/ajf8/blog/item/05889719727b691934fa4178.html
[5]. http://www.langyuweb.com/a/wangluozhishi/2012/0326/14674.html
[6]. http://www.infoq.com/cn/articles/cf-java-thread
[7]. http://www.goldendoc.org/2011/05/juc/
[8]. http://cloud.csdn.net/a/20100926/279856.html
[9]. http://mechanical-sympathy.blogspot.com/2011/07/false-sharing.html
[10]. http://edu.netbeans.org/contrib/slides/java-overview-and-java-se6.pdf
[11]. http://www.azulsystems.com/blog/wp-content/uploads/2011/03/2011_WhatDoesJVMDo.pdf
[12]. http://wiki.jvmlangsummit.com/pdf/10_Pampuch_vm_opts.pdf
[13]. http://kenwublog.com/theory-of-lightweight-locking-upon-cas
[14]. http://icyfenix.iteye.com/blog/1018932
[15]. http://en.wikipedia.org/wiki/Memory_barrier
[16]. http://jakub.wartak.pl/dl/j2ee/sunJVM-on-intel-multicoreservers.pdf
[17]. https://wikis.oracle.com/display/HotSpotInternals/Synchronization
[18]. http://kenwublog.com/explain-java-memory-model-in-detail
[19]. http://www.oracle.com/technetwork/java/javase/tech/biasedlocking-oopsla2006-preso-150106.pdf
[20]. https://code.google.com/p/hatter-source-code/wiki/Study_Java_HotSpot_Glossay
[21]. http://home.comcast.net/~pjbishop/Dave/MustangSync.pdf
[22]. http://www.javaol.net/2010/10/java-memory-model/
[23]. http://www.ibm.com/developerworks/cn/linux/l-cn-spinlock/index.html
[24]. http://www.khotyn.com/2011/12/15/memory_barrier/
[25]. http://qconlondon.com/dl/qcon-london-2012/slides/MartinThompson_and_MichaelBarker_LockFreeAlgorithmsForUltimatePerformance.pdf
[26]. http://www.tektalk.org/2011/07/11/cache%E4%B8%80%E8%87%B4%E6%80%A7%E5%8D%8F%E8%AE%AE%E4%B8%8Emesi2/
[27]. https://blogs.oracle.com/dave/entry/biased_locking_in_hotspot
[28]. https://blogs.oracle.com/dave/entry/java_contented_annotation_to_help
[29]. http://mail.openjdk.java.net/pipermail/hotspot-dev/2012-November/007309.html
[30]. http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7125896