谈谈线程安全与锁优化

写在前面

本文是作为阅读《深入理解Java虚拟机》第13章线程安全与锁优化的读书笔记;

线程安全是以多个线程之间存在共享对象为前提的。它并不是一个非黑即白的问题。Brian Goetz 曾就线程安全的“安全程度” 按以下由强至弱做了划分:

  1. 不可变;这类对象不需要进行任何线程安全的保障措施,只要这个对象被构建出来,其外部的可见状态永远也不会改变。如果共享数据是一个基本数据类型,那么只要在定义时使用 final 关键字修饰它,就可以保证它是不可变的。如果共享数据是一个对象类型,那就需要保证对象的行为不会对其状态产生任何影响才行,像 String 类的对象。

  2. 绝对线程安全;这类的划分的定义和不可变很像,但不可变更加的纯粹,简单。如果一个对象要是绝对线程安全的,那么需要保证多个线程中,对该对象的各种行为调用组合,都会得到想要的结果。举个例子来说,Vector 是线程安全的,但并不是绝对线程安全,比如,在一个线程对一定容量大小的 Vector 进行 remove 操作,另一线程进行 get 操作,最终,你会得到一个数组越界异常,这并不是我们想要的结果。其实,绝对线程安全是一种很苛刻的条件,所以,Java 中所讲的线程安全,通常指的是下面的相对线程安全。

  3. 相对线程安全;针对单个对象的单独操作是线程安全的。像上面 Vector 的 get 、remove 等操作。

  4. 线程兼容;这表明该对象本身并不是线程安全的,但可以使用同步手段来实现保证对象在并发环境中安全地使用。我们平常所说的一个类不是线程安全的,大多也是指的这种情况。

  5. 线程对立;这是一种极端的情况,无论你是否采取了同步措施,都无法在多线程环境中并发使用的代码。书上所举的例子是 Thread 类的 suspend 和 resume ,这两个方法已经被 JDK 声明废弃了。书上是这样描述的:“如果有两个线程同时持有一个线程对象,一个尝试去中断线程,一个尝试去恢复线程,如果并发进行的话,无论调用时是否进行了同步,目标线程都是存在死锁风险的,如果 suspend 中断的线程就是即将要执行 resume 的那个线程,那就肯定要产生死锁了”。思考了下,采取同步措施,还是能够避免死锁的。网上查了一些废弃的原因,总结下来大概下面几种:

    1. 容易产生死锁,这个已经讲的很清楚了。
    2. suspend 某个线程,会造成该线程停留在逻辑上不合理的状态。我觉得这算一种合理地解释,被挂起的线程处于运行状态,但按照线程的状态划分,它应该属于无限期等待,因为它需要显示唤醒。
    3. 不释放锁,这个完全站不住脚,sleep 方法也不释放锁呀,唯一可以解释的是它没法终止这种状态,除了调用 resume 方法,而 sleep 在等待时,还可以被打断。
    4. 不同步,这个意思大概讲的是共享数据处于一个不同步状态,这是线程安全的问题,并不能算作其废弃的原因。

    总结下来,第一种和第二种都是很重要的原因,并且,还有一点,我认为也是值得一提的,那就是对于 suspend 和 resume 的调用,可以很随意,不需要同步任何对象或者获取任何锁,至少我们在使用 wait 和 notify 方法时,是需要一定条件的。


上面对线程安全做了一个讨论,下面谈谈实现线程安全的方法:

同步是指在多个线程并发访问共享数据对象时,保证共享数据对象在同一个时刻只被一条(或者是一些,在使用信号量的时候)线程使用。

  1. 互斥同步:互斥是实现同步的一种手段,Java 中最常见的就是 synchronizedReentrantLock。一个表现为原生语法层面的互斥锁,一个表现为 API 层面的互斥锁。它们之间的区别在于 ReentrantLock 更加灵活,可以提供等待可中断,可实现公平锁,以及锁可以绑定多个条件等高级功能。在选择使用哪种同步手段时,并不应该基于性能考虑而去使用 Lock,更应该基于功能上的考虑去做选择。

  2. 非阻塞同步:互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步,它属于一种悲观策略。非阻塞同步是一种基于冲突检测的乐观策略,它认为数据竞争大多数情况下都不会发生,如果共享数据发生了冲突,那就再进行其他的补偿措施,这种策略的实现不需要把线程挂起,因此被称为非阻塞同步。Java 中 则是 CAS 操作了,比较然后交换值。

    CAS 接收三个参数,第一个为变量的内存地址,第二个为旧的预期值,第三个为新值,当且仅当变量内存地址所代表的值符合旧预期值时,才用新的值更新内存地址的值,看起来这里有几步操作,但在处理器层面能够保证它的原子性。CAS 操作有一个逻辑上的漏洞,称之为 ABA 问题,对于中间的数据值,没法感知,所以又提供了版本号等来解决这个问题。

  3. 无同步方案:如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。

    1. 可重入代码:代码执行的任何时刻都可以中断它,转而去执行另外一段代码,而在控制权返回后,原来的程序不会出现任何错误。这类代码并不依赖存储在堆上的数据和公用的系统资源,用到状态量都由参数中传入等。
    2. 线程本地存储:这类限制共享数据的可见性在同一个线程中,常见的就是 Java 中的 ThreadLocal 了。

锁优化技术是为了达到高效并发的目的,像在前文提到的基于功能去选择阻塞同步方法,那也是因为 synchronized 关键字做了大量的优化,在与 Lock 进行比较时,我们不需担心它的性能。

  1. 自旋锁:为了避免频繁的线程切换,在获取锁时,先不放弃处理器的执行时间(在拥有多个处理器时),让线程处于一个忙循环(自旋),看看持有锁的线程是否会很快释放锁。

  2. 适应性自旋锁:如果在同一个锁对象上,自旋等待刚刚成功获得过锁,那么就允许自旋等待持续相对更长的时间,但如果对于某个锁,自旋很少成功获得过,那么可能以后在获取这个锁时,就会省略掉自旋过程,以避免处理器资源浪费。随着程序运行和性能监控信息不断完善,虚拟机对程序锁的状况预测就会越来越准确。

  3. 锁消除:在即使编译器运行中,发现某些代码要求同步,但检测到不可能存在共享数据竞争,那么就可以对锁就行消除,主要的判断依据来源于逃逸分析的数据支持。

  4. 锁粗化:如果一系列的连续操作都对同一个对象进行加锁和解锁,那么虚拟机将会把加锁同步的范围粗化到整个操作序列的外部。

  5. 轻量级锁:首先明确一点,轻量级锁并不是用来取代重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

    轻量级锁是利用 CAS 操作来做的,实现过程还是比较复杂,这里简单记录下。对象的头信息具有一块 Mark Word 空间,存放着一些标志位(能够区分对象当前的状态)。

    • 01 未锁定状态,存储内容:对象哈希码、对象分代年龄
    • 00 轻量级锁定,存储内容:指向锁记录的指针
    • 10 重量级锁定,存储内容:指向重量级锁的指针
    • 11 GC标记,空,不需要记录信息
    • 01 可偏向,存储内容:偏向线程ID、偏向时间戳、对象分代年龄

    在线程对该对象进行加锁时,如果对象的 Mark Word 处于一个未被锁定的状态,那么先拷贝对象的 Mark Word 放在自己的栈帧中,在利用 CAS 尝试将对象的 Mark word 改为指向自己栈帧中的 Mark word(称之为锁记录)的一个指针记录,如果操作成功,则将 Mark word 改为轻量级锁的状态,如果操作失败,则去检测对象的 Mark word 是否指向当前线程的栈帧,如果是说明当前线程已经拥有了这个对象的锁,可以直接进入同步块继续执行,否则说明这个锁对象已经被其它线程抢占了。如果有两个线程争用同一个锁,轻量级锁不再有效,要膨胀为重量级锁,修改标志位,Mark word 中存储的就是执行重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。

    解锁过程也是通过 CAS 的,如果对象的 Mark word 仍然指向着线程的锁记录,那么就用 CAS 操作把对象当前的 Mark word 和线程中复制的 Mark word 替换回来,如果替换成功,整个同步过程就完成了,如果替换失败,说明有其他线程尝试过获取该锁,那么就要在释放锁的同时,唤醒被挂起的线程。

    轻量级锁能提升程序同步性能的依据是"对于绝大部分的锁,在整个同步周期内都是不存在竞争的",如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了 CAS 操作,因此有竞争的情况下,轻量级锁会更慢。

  6. 偏向锁:在无竞争的情况下把整个同步都消除掉。锁对象第一次被获取的时候,会改变状态为偏向模式,并利用 CAS 将获取到这个锁的线程的 ID 记录在对象的 Mark Word 中,如果 CAS 操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,都可以不再进行任何同步操作。 当有另外一个线程尝试去获取这个锁时,偏向模式就宣告结束了。

这些技术都是为了避免在系统级别上阻塞或唤醒线程。这里在网上找到一个总结,关于偏向锁、轻量级锁、重量级锁三者各自的应用场景(理想情况下):

  • 偏向锁:只有一个线程进入临界区;
  • 轻量级锁:多个线程交替进入临界区;
  • 重量级锁:多个线程同时进入临界区;

参考博文


JVM锁优化以及区别


我与风来


认认真真学习,做思想的产出者,而不是文字的搬运工
错误之处,还望指出

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值