《深入理解java虚拟机》读书笔记
线程安全与锁优化
《java concurrency in Practice》的作者认为:“当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,不需要进行额外的同步,也不需要在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。”
1.java语言中的线程安全
1. 不可变
描述的是被final关键字修饰的情况,如果一个对象是不可变的,那么自然也就不会带来线程不安全的情况。(final关键字的使用场景和原理分析也值得一看)
2. 绝对线程安全
“绝对”的线程安全是很严格的,严格在于调用方也不需要进行其他的协调操作,通常我们保证线程安全的手段除了使用线程安全的集合类外,还是会在调用方使用锁来保证安全,这样一来就算不上是绝对的线程安全了。作者举例子的时候并没有提到原子类是不是绝对线程安全的,但是我们知道原子类底层使用的是cas操作,cas会有aba问题(可以使用版本号解决),绝对线程安全是一种理想的状态。
3. 相对线程安全
我们通常意义上讲的线程安全,大部分的线程安全类都属于这种类型。Vector/hashTable/Collections的synchronizedCollection()方法包装的集合等。
4. 线程兼容
线程兼容指对象本身不是线程安全的,可以在调用端正确的使用同步手段保证对象在并发环境中可以安全的使用。说白了就是在使用如ArrayList这类非线程安全的对象的时候通过一些手段(各种锁)达到线程安全。
5. 线程对立
无论用什么手段,并发环境下都无效。
Thread中的suspend()和resume(),两个线程同时持有一个对象,一个一直尝试中断,另一个一直在尝试恢复,就带来了死锁的风险。
线程安全的实现方法
1. 互斥同步
互斥同步式常见的一种并发正确性保障手段,同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个线程使用。互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方法。
synchronize
synchronize经过编译后,会形成monitoenter和monitorexit两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。如果java程序中的synchronize明确制定了对象参数,那就是这个对象的reference;如果没有明确指定,就看synchronize修饰的是实例方法还是类方法。
synchronize拥有锁重入的功能,也就是在使用synchronize的时候,当一个线程得到了一个对象的锁之后,再次请求此对象是可以再次得到该对象的锁。。也因为synchronize拥有可重入的功能,所以不会出现把自己锁死的问题。被synchronize锁住的区域称为同步快,同步快在已进入的线程执行完成之前,会阻塞后面其他线程的进入,。
2. 非阻塞同步
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,这种同步也称为阻塞同步。
在深入java虚拟机中,作者说互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题,无论数据是否会竞争,它都会加锁。
随着硬件指令集的发展,有了另一种选择:基于冲突检测的乐观并发策略,解释下这种方法:
就是先进性操作没如果没有其他线程争用共享数据,那操作就成功了,如果共享数据有争用,那就产生了冲突。产生了冲突就采取其他的补偿措施。这种方法被认为是乐观的,因为不需要把线程都挂起。
实例:实现这种方式需要依靠硬件指令,常用的有
- 测试并设置
- 获取并增加
- 交换
- 比较并交换(CAS)
- 加载连接/条件存储
3. 无同步方法
如果以个方法本来就不涉及共享数据,那他自然就无须任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的,有这样两类代码:
可重入代码:可以在任何时刻中断它,转而去执行另一段代码,而在控制权返回后,原来的程序不会出现任何错误。可重入代码都是线程安全的,但是线程安全的不一定都是可重入代码。
线程本地存储:如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行,如果能保证,我们就可以吧共享数据的课件范围限制在同一个线程之内。
线程本地存储这个条件,说简单点,就是在一个线程内把要做的事情都做了,把需要用到的数据都用了,那么无须同步也能保证线程之间不出现数据争用的问题.
锁优化
1. 自旋锁与自适应自旋
互斥同步对性能最大的影响是阻塞的实现,因为挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能带来了很大的压力.
自旋锁:如果物理机器有一个以上的处理器,能让两个或者以上的线程同时并行执行,就可以让后面的那个线程处于一个忙循环中,”等待一下”,这个忙循环便是自旋.
自旋锁的适用场景:适用于锁被占用的时间很短的场景,如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费.
自适应的自旋锁:自适应是在JDK 1.6之后引入的,自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的.
2. 锁消除
锁消除是指:在虚拟机即时编译器运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除.
如何判断共享数据竞争是否存在?
主要判定技术是来源于逃逸分析的数据支持.
3. 锁粗化
如果一段程序要多次请求锁,锁之间的代码执行时间比较少,就应该整合成一个锁,前提是不用同步的部分执行时间短。例如for循环里面申请锁,如果for循环时间不长,可以在for外面加锁。
4. 轻量级锁
自旋锁的目标是降低线程切换的成本。如果锁竞争激烈,我们不得不依赖于重量级锁,让竞争失败的线程阻塞;如果完全没有实际的锁竞争,那么申请重量级锁都是浪费的。轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。
顾名思义,轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。
缺点:如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁,那么维持轻量级锁的过程就成了浪费。
5. 偏向锁
在没有实际竞争的情况下,还能够针对部分场景继续优化。如果不仅仅没有实际竞争,自始至终,使用锁的线程都只有一个,那么,维护轻量级锁都是浪费的。偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。
“偏向”的意思是,偏向锁假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁),因此,只需要在Mark Word中CAS记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner就可以零成本的直接获得锁;否则,说明有其他线程竞争,膨胀为轻量级锁。
偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定