`
bsr1983
  • 浏览: 1097166 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

深入理解JVM学习笔记——第十三章 线程安全与锁优化

    博客分类:
  • JVM
阅读更多

注:本系列文章均摘录自《深入理解Java虚拟机:JVM高级特性与最佳实践》,作者周志明,我看的是第一版,现在第二版已经出了,

第十三章 线程安全与锁优化
    1."当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协同操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的"。
    2.按照线程安全的“安全程度”由强到弱来排序,可以将Java语言中各种操作共享的数据分为以下五类:
    (1)不可变。在Java语言里里面(特指JDK1.5以后,即Java内存模型被修正之后的Java语言),不可变(Immutable)的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再进行任何的线程安全保障措施。
    Java语言中,如果共享数据是一个基本数据类型,那么只要在定义时使用final关键字修饰它就可以保证它是不可变的。如果共享的数据是一个对象,那就需要保证对象的行为不会对其状态产生任何影响才行。
    保证对象行为不影响自己状态的途径有很多中,其中最简单的就是把对象中带有状态的变量多声明为final,这样在构造函数结束之后,它就是不可变的。
    (2)绝对线程安全
    绝对的线程安全完全满足Brian Goetz给出的线程安全的定义。在Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全。
    (3)相对线程安全
    相对线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用段使用额外的同步手段来保证调用的正确性。
    (4)线程兼容
    线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中安全地使用,我们平常说一个雷不是线程安全的,绝大多数指的都是这种情况。
    (5)线程对立
    线程对立是指不管调用端是否采用了同步措施,都无法在多线程环境中兵法使用的代码。
    线程对立的例子:Thread类的suspend()和resume()方法、System.setIn()、System.setOut()、System.runFinalizersOnExit()等。
    3.互斥同步(Mutual Exclusion&Synchronization)是最常见的一种并发正确性保障手段,同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条线程(或是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区(Critical  Section)、互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。因此在这四个字里面,互斥是因,同步是果,互斥是方法,同步是目的。
    在Java里面,最基本的互斥同步手段就是synchronized关键字。
    除了synchronized之外,我们还可以使用java.util.concurrent(下文称J.U.C)包中的重入锁(ReentrantLock)来实现同步,在基本用法上,ReentrantLock与synchronized很相似,都具备线程可重入性,只是代码写法有区别,一个表现为API层面的互斥锁(lock()和unlock()方法配合try/finally语句块来完成),一个表现为原生语法层面的互斥锁。不过ReentrantLock 比synchronized增加了一些高级功能,主要有以下三项:
    (1)等待可中断。
    等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间长的同步块很有帮助。
    (2)可实现公平锁。
    公平锁是指多个线程在等待同一个锁的时候,必须按照申请锁的事件顺序来依次获得锁;而非公平锁在不保证这一点,在锁被释放时,任何一个等待的锁的线程都有机会获得锁。synchronized 中的锁是非公平锁,ReentrantLock默认情况下也是非公平锁,但可以通过带布尔值的构造函数要求使用公平锁。
    (3)锁可以绑定多个条件
    锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和nofity()或nofityAll()方法可以实现一个隐含的条件,如果要和多于一个的条件关联时,就不得不额外地添加一个锁,而ReentrantLock则无须这样做,只需要多次调用newCondition()方法即可。
    JDK1.6发布之后,人们就发现synchroni和ReentrantLock在性能上完全持平了。
    4.非阻塞同步
    互斥同步最重要的问题就是尽心线程阻塞和唤醒所带来的性能问题,因此这种同步也被称为阻塞同步(Blocking Synchronization)。另外,它属于一种悲观的并发策略,总是认为只要不去做正确的同步措施(加锁),那就肯定会出现问题。
    基于冲突检测的乐观并发策略,通俗地说就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再进行其它的补偿措施(最常见的补偿措施就是不断地重试,知道试成功为止)。这中乐观的并发策略的许多实现都不需要把线程挂起,因此被称为非阻塞同步(Non-Blocking Synchronization)。
    在JDK1.5之后,Java程序中才可以使用CAS操作,该操作由sun.misc.Unsafe类里面的compareAndSwapInt()和compareAndSwapLong()等几个方法包装提供,虚拟机在内部对这些方法做了特殊处理,即时编译出来的结果就是一条平台相关的处理器CAS之类,没有方法调用的过程,或者可以认为是无条件内联进去了。
    5.无同步方案
    要保证线程安全,并不一定就要进行同步,两者没有因果关系。同步只是保障共享数据争用时的正确性手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性,因此会有一些代码天生就是现场安全的。
    (1)可重入代码(Reentrant Code):这种代码也叫纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。相对于线程安全来说,可重入性是更基本的特性,它可以保证线程安全,即所有可重入的代码都是线程安全的,但并非所有的线程安全的代码都是可重入的。
    (2)线程本地存储(Thread Local Storage):如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。
    6.锁优化
    高效并发是JDK1.6的一个重要主题,HotSpot虚拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)等,这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。
    (1)为了让线程等待,只须让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
    自旋锁在JDK1.4.2中就已经引入,只不过默认是关闭的,可以使用-XX:+UseSpinning参数来开启,在JDK1.6中就已经默认开启了。自旋次数的默认值时10次,用户可以使用参数-XX:PreBlockSpin来更改。
    在JDK1.6中引入了自适应的自旋锁。自适应一位置自旋的时间不在固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
    7.锁消除
    锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持。
    8.锁粗化
    如果虚拟机探测到有一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。
    9.轻量级锁
    轻量级锁是JDK1.6中加入的新型锁机制,它名字中的“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就被称为“重量级”锁。
    HotSpot虚拟机的对象头(Object Header)分为两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄(Generational GC Age)等,这部分数据的长度在32位和64位的虚拟机中分别为32个和64个Bits,官方称它为“Mark Word”,它是实现轻量级锁和偏向锁的关键。
    另外一部分用于存储指向方法区对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分用于存储数组长度。
    在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储所对象目前的Mark Word的拷贝(官方把这份拷贝加了一个 Displaced前缀,即Displaced Mark Word)。
    然后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象的Mark Word的锁标志位(Mark Word的最后两个Bits)将转变为"00",即表示此对象处于轻量级锁定的状态。
    如果这个更新操作失败了,虚拟机会首先检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程抢占了。如果有两条以上的线程争用同一个锁,那轻量锁就不再有效,要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
    10.偏向锁
    偏向锁也是JDK1.6中引入的一项锁优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。
    偏向锁的“偏”,就是偏心的“偏”,偏袒的“偏”。它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。
    偏向锁启用参数:-XX:+UseBiasedLocking,这是JDK1.6的默认值,禁用参数-XX:-UseBiasedLocking。

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics