第13章 线程安全与锁优化
文章目录
13.2 线程安全
13.2.1 Java语言中的线程安全
- 不可变
- 基本数据类型:在定义时使用final关键字修饰
- 对象:需要保证其行为不会对其状态产生任何影响(String),最简单的方法就是把对象里面带有状态的变量都声明为final
- 绝对线程安全
付出的时间和空间成本都非常大 - 相对线程安全
大部分声称线程安全的类都属于这种类型 - 线程兼容
对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用 - 线程对立
线程对立是指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码
13.2.2 线程安全的实现方法
互斥同步(Mutual Exclusion & Synchronization):最常见也是最主要的并发正确性保障手段,悲观的并发策略,也被称为阻塞同步(Blocking Synchronization)
概念
- 同步:在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条(或者是一些,当使用信号量的时候)线程使用
- 互斥:实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是常见的互斥实现方式
使用
synchronized(推荐)
- 被synchronized修饰的同步块对同一条线程来说是可重入的
- 被synchronized修饰的同步块在持有锁的线程执行完毕并释放锁之前,会无条件地阻塞后面其他线程的进入
重入锁(ReentrantLock)
- 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情
- 公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁
- 锁绑定多个条件:一个ReentrantLock对象可以同时绑定多个Condition对象
区别
- synchronized是在Java语法层面的同步,清晰,简单
- Lock应该确保在finally块中释放锁,否则一旦受同步保护的代码块中抛出异常,则有可能永远不会释放持有的锁
- Java虚拟机更容易针对synchronized来进行优化
非阻塞同步:共享数据冲突,再进行补偿措施,最常用的补偿措施是不断地重试,直到出现没有竞争的共享数据为止。这种乐观并发策略的实现不再需要把线程阻塞挂起,使用这种措施的代码也常被称为无锁(Lock-Free)编程
处理器指令
- 测试并设置(Test-and-Set)
- 获取并增加(Fetch-and-Increment)
- 交换(Swap)
- 比较并交换(Compare-and-Swap,下文称CAS)
- 加载链接/条件储存(Load-Linked/Store-Conditional,下文称LL/SC)
无同步方案
可重入代码(Reentrant Code):这种代码又称纯代码(Pure Code),是指可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误,也不会对结果有所影响 如果一个方法的返回结果是可以预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的
线程本地存储(Thread Local Storage):如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题.ThreadLocal
13.3 锁优化
13.3.1 自旋锁与自适应自旋
自旋锁:为了让线程等待,我们只须让线程执行一个忙循环(自旋) 自适应:由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的
13.3.2 锁消除
即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除(逃逸分析)
13.3.3 锁粗化
将同步块的作用范围限制得尽量小——只在共享数据的实际作用域中才进行同步
13.3.4 轻量级锁
不同状态下对象头的存储内容
工作过程
- 在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(这份拷贝加了Displaced前缀,即Displaced Mark Word)
- 使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针。Mark Word的锁标志位将转变为“00”
13.3.5 偏向锁
偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需 要再进行同步
状态转换关系