JVM:线程安全与锁优化
一、目标
1、了解线程并发的正确性以及安全性
2、了解JVM中JDK1.6以后优化的锁
二、带着问题阅读
1、什么是线程安全。
2、哪些类型制药声明了final就真的是不可变了
3、共享数据从线程安全的角度分为哪5类,并简要说明此5类。
4、线程安全的实现方法,并简述。
5、JDK1.5到JDK1.6的锁优化
6、CAS的原理,以及CAS的ABA问题
三、文章摘要
1、什么是线程安全。
“当多个线程访问一个对象时,如果不考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方面进行任何其他的协作操作,调用这个对象的行为都可以获得正确的结果,那么这个对象是线程安全的”--《JAVA concurrency in practice》(我看过此书的中文版,由于此书的前几章及其的晦涩难懂,因此跳过去了,现在要去翻书复习了),这个定义比较严谨,通常我们也用不到安全级别这么高的线程,因此适当弱化:把“调用这个对象的行为”限定为“单次调用”,这个定义的其他描述也能成立的话,我们认为是线程安全。这对于编写程序而言会轻松许多,但是调用对象时就要注意。
2、哪些类型只要声明了final就真的是不可变了
刚接触java时,我看到项目中用final声明的对象感到比较陌生,因为这些final声明的对象并不final,对象里面的值还是可以照常设置,这个final到底有什么用呢。现在这个了解一点:final修饰的变量是对栈中的值无法改变,也就是不能改变对象的引用。
回归正题:如果一个数据是被final声明的基本数据类型,那么它是不可变的,不可变的数据,用来共享也一定是线程安全的。
3、共享数据从线程安全的角度分为哪5类,并简要说明此5类
·不可变:final声明的基本类型,枚举,String,Long和Double,BigInteger和BigDecimal等。如果想要一个对象不可变,那么使它的行为不会影响它的状态(不是很理解)。
·绝对的线程安全:完全满足Brian Goetz给出的线程安全的定义,书中给出的例子很好的描述了这种情况P389。
·相对线程安全:通常意义上的线程安全,在一个线程中是安全的,但是在多个线程的环境下,对于一些特定顺序的连续调用,就可能需要加入额外的同步手段。
·线程兼容:指一个对象本身线程不安全,但是通过正确的同步手段来保证对象在并发环境中安全使用。平常所说的线程不安全就是指这个。
·线程对立:不管怎么弄,在多线程中就是不安全,这种代码是有害的。
4、线程安全的实现方式
(1)互斥同步:互斥是同步的一个手段,临界区(Critical Section)、互斥量(Mutex)、信号量(Semaphore)都是主要的互斥实现方式。java中主要是同过synchronized关键字来进行互斥,synchronized经过编译后会在同步块的前后分别形成monitorenter和monitorexist这两个字节码指令,在执行monitorenter指令时,首先要尝试获取对象的锁。如果这个对象没有被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,在执行monitorexit指令时会将计数器减1,当计数器为0时,锁就会被释放。synchronized对同一条线程来说是可重入的,不会把自己锁死的问题,同步块在已经进入的线程执行完之前,会阻塞后面其他线程的进入。synchronized是java语言中一个重量级的操作,有经验的程序员都会在确实必要的情况下才使用这种操作。除了synchronized之外,还可以使用ReentrantLock来实现同步,性质差不多,只是写法上不一样。相比于synchronized,ReentrantLock增加了一些高级功能,主要有以下3项:等待可中断、可实现公平锁、以及锁可以绑定多个条件。在JDK1.5之前ReentrantLock比synchronized性能要好很多,但是在JDK1.6之后,性能就相差无几了,JVM对synchronized的优化仍然在进行中,因此要优先考虑synchronized来进行同步。
(2)非阻塞同步:互斥同步的问题属于一种悲观的并发策略,而非阻塞同步是属于乐观的并发策略
悲观:无论数据是否真的出现竞争,它都要进行加锁
乐观:先进行操作,如果没有其他线程争用共享数据,则成功修改,否则,就采用补救措施(最常见的就是不断地重试,直到成功为止),这种乐观的并发策略许多实现都不需要把线程挂起来,因此名为非阻塞同步。非阻塞同步是基于硬件来实现的,这些指令包括:测试并设置(TestAndSet),获取并增加(FetchAndIncrement)、交换(Swap)、比较并交换(CAS)、加载链接/条件存储(LL/SC)。其中最主要的是CAS操作。
(3)无同步方案:同步只是解决共享数据的争用问题,如果某些方法本身就不涉及共享数据,则不需要进行同步:
(a)可重入代码:如果一个方法,它的返回结果是可以预测的,只要输入了相同的数据,都能返回相同的结果,那么它就满足可重入性的要求,也是线程安全的。特征:状态量都是由参数传入、不调用非可重入方法等。
(b)线程本地存储:如果一个变量要被多个线程访问,可以使用volatile来声明,如果一个变量要被某个线程独享,则可以通过ThreadLocal类来实现,即ThreadLocal不是用来解决共享数据的争用问题的,如果将一个共享对象放到ThreadLocal中,还是会发生并发问题。每一个线程的Thread对象中都有一个ThreadLocalMap对象,这个对象存储量一组以ThreadLocal.threadLocalHashCode为键,以本地线程变量为值的K-V值对,ThreadLocal就是当前线程的ThreadLocalMap的访问入口,每一个ThreadLocal对象都包含了一个独一无二的threadLoacalHashCode值,使用这个值就可以在线程K-V值对中找对应的本地线程变量。
5、JDK1.5-JDK1.6的锁优化
(1)适应性自旋与自旋
如果是多核cpu,当一个线程A被cpu调度进来(假设线程B已经获得锁1),它要获取锁1,此时会获取失败,原本是要挂起的,但线程进入了一个忙循环(可能是100次的空循环),来等待线程B释放锁1,这就是自旋锁。如果锁被占用的时间很短,自旋等待的效果会很好(可以避免线程被交换出去),否则,不但得不到锁又浪费了cpu的时间。
自适应自旋锁在自旋锁的基础上建立的,它的循环次数是根据前一次在锁上花费的时间以及锁的拥有者的状态来决定的。因此,随着程序运行和性能监控信息的不断完善,程序会执行的越来越快
(2)锁消除:对一些同步代码没有检测到锁竞争时,把这些锁消除掉,有些锁事java内部自动加的,比如String之间的相加。
(3)锁粗化:若在一块代码中对一个对象连续地加锁和释放锁,则还不如在此代码块中将对象锁一次。
(4)轻量级锁:在无竞争的情况下使用CAS操作去消除同步使用的互斥量,对一些没有产生竞争的锁先上轻量级锁,存在竞争时,就膨胀为重量级锁。在没有竞争时,轻量级锁效率比较高,2个以上竞争时,重量级锁效率高。
(5)偏向锁:偏向锁是在无竞争的情况下,把整个CAS操作都消除,连CAS都不做了。偏向锁、轻量级锁、重量级之间存在着转换,JVM中用一个对象头信息(mark world)来描述,对象头信息是与对象自身定义的数据无关的额外存储成本,出于效率的考虑,它的数据结构是不定的,各个状态下的存储信息如表1所示。
锁之间的转换以及MarkWord存储信息如下图所示: