第十三章 线程安全与锁优化
线程安全
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。
一、java语言中的线程安全
我们下面讨论的线程安全限定于多个线程之间存在共享数据访问的这种情况。如果不共享数据,那么从线程的角度来看它就是安全的。
我们将java语言中各种操作共享的数据分为5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立
1)不可变(final)
final修鉓一经初始化(init)值不可改变
基本数据类型就不说了。String类型也是final的,字符串加法是怎么回事,它是又生成了一个新值,原来的字符串没有发生变化。
2)绝对安全
不管运行时环境如何,调用者都不需要任何额外的同步措施。
在使用了java中安全的类之后还在加入同步synchronized才可以
3)相对线程安全
这就是我们通常讲的线程安全,它需要保证对这个对象单独的操作是线程安全的。
如Vector、HashTable、Collections的synchronizedCollection()方法包装的集合等
4)线程兼容
线程兼容是指对象本身并不是线程安全的,但是可以通过调用端正确地使用同步手段来保证对象在并发环境中可以安全的使用。我们说的非线程安全类都是这种情况。如arrayList\hashMap
5) 线程对立
指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。
二、线程安全的实现方法
1)互斥同步
互斥同步是常见的一种并发正确性保障手段。
同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个线程使用。而互斥是实现同步的一种手段
最基本的互斥同步手段就是synchronized关键字,经过编译之后会在同步块前后分别形成monitorenter 和monitorexit这两个字节码指令。
如果synchronized明确指定了对象参数,那就是这个对象的reference;如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或class 对象来作为参数。
2)非阻塞同步
互斥同步主要的问题就是进行线程阻塞和唤醒所带来的性能问题,互斥同步也称为阻塞同步。属于一种悲观的并发策略。
基于冲突检策的乐观并发策略,也就是说先进行操作,如果没有其它线程争用共享数据,那操作就成功了。如果共享数据有争用,产生了冲突,那就再采取其它的补偿性措施。(如不断的重试)这种策略不需要把线程挂起,因些这种同步操作称为非阻塞同步。
CAS(比较并交换)指令需要三个参数(地址、旧值、新值)。进行比较如果旧值和现在对象上存的值相等说明没被修改过,上次还是自己在操作。
CAS是一条基于平台的处理器指令。
java的Usafe类提供了这种非阻塞同步机制。
这个有点类似于数据库里面常使用的乐观锁操作。
3)无同步方案
如果不涉及共享数据,那就无须任何同步措施去保证正确性。
三、锁优化
1)自旋锁与自适应自旋
如果物理机有一个以上的处理器,能让两个或以上的线程同时并行执行,我们可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间。为了让线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
2)锁消除
锁消除是指虚拟机即时编译器在运行时,对一些代码上的同步,对被检测到不可能存在共享数据竞争的锁进行消除。
3)锁粗化
不要在多个小块内频繁的加锁,可以把锁的力度加大,一个包含刚才那几个小块
4)轻量级锁
"轻量级"是相对于使用操作系统互斥来实现的“重量级”锁而言的。它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
先来说下对象头的markword概念:
对象头分为两部分:
1)存储对象自身的运行时数据,如hashcode\GC分代年龄等。这部分称为"Mark Word"
2)存储指向方法区对象类型数据的指针
具体的执行流程:
- 代码进入同步块,如果此对象没有被锁定(锁标识位为01),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(lock record)的空间,用于存放锁对象目前的mark word的拷贝
- 虚拟机将使用CAS操作尝试将对象的 mark word更新为指向 Lock Record的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁。并且对象的mark word锁标识位变成00, 即表示此对象处理轻量级锁定状态。
- 如果这个更新操作失败了,看现在的锁是哪个对象拿着,如果是自己,则直接进入,如果是别的线程,那么说明出现了并发,要膨胀为重量级锁,把锁的状态值变为 “10”
- 解锁的过程也是通过CAS操作来进行,如果对象的mark word仍然指向着线程锁记录,那就用CAS操作把对象当前的mark word和线程中复制的displaced mark word替换回来。如果替换成功,那整个上步过程不完成了。如果替换失败,说明有其它的线程尝试去获取爱江山更爱美人,那就要在释放锁的同时,唤醒被挂起的线程。
5)偏向锁
它的目的是消除数据在无竞争情况下的同步原语,进一步提高运行性能。
如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同肯使用的互斥量,
那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。
这个锁会偏向于第一个获得它的线程。
具体流程:
- 当锁对象第一次被线程获取的时候,虚拟机将地把对象头标志位设为“01”,即偏向模式
- 同时使用CAS操作把获取到这个锁的线程ID记录在对象的mark word中
- 如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块,虚拟机都不再进行任何同步。
- 当另一个线程去尝试获取这个锁时,偏向模式宣告结束。根据对象目前是否处于被锁定的状态,撤销偏向后恢复到未锁定(01)或轻量级锁(00)状态,后续的执行按上面的轻量级锁去执行。
偏向锁可以提高带有同步但无竞争的程序性能。它是一个带有效益权衡性质的优化。也就是说并不一定总是对程序运行有利,如果大多数情况下都是多线程并发的,那偏向模式就是多余的。
ps:能够写出高伸缩性的并发程序是一门艺术,而了解并发在系统底层是如何实现的,则是掌握这门艺术的前提条件。