Java 并发详解 ③:Synchronized
synchronized 发生异常会释放锁
不能拿 String、Integer、Long 等基础类型做锁
synchronized 和 Lock 能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。但是同步块里的非原子操作依旧可能发生指令重排
1. notify
- wait ():令当前资源挂起并放弃 CPU、同步资源,不再抢资源,除非唤醒
- notify ():当其他线程的运行使得这个条件满足时,其它线程会调用 notify () 唤醒正在排队等待同步资源的下城中优先级最高者结束等待
- notifyAll ():当其他线程的运行使得这个条件满足时,其它线程会调用 notifyAll () 唤醒正在排队等待资源的所有线程结束等待
这三个方法只有在 synchrozied 方法或 synchrozied 代码块中才能使用
2. 使用
同步方法:锁为当前对象,即 this,所以不能用在继承方法的线程上
1 | public class Bank { |
同步阻塞:
1 | public class Window2 implements Runnable { |
3. 用户态与内核态
synchronized 加锁需要程序由用户态切换到内核态,效率低
Intel x86 架构的 CPU 来说一共有 0~3 四个特权级,0 级最高,3 级最低。
硬件上在执行每条指令时都会对指令所具有的特权级做相应的检查。相关的概念有 CPL、DPL 和 RPL。
Linux 中当程序运行在 3 级特权级上时称之运行在用户态,运行在 0 级特权级上时称之运行在内核态。
用户态通过申请外部资源(申请堆内存、读写磁盘文件。。。)切换至内核态
用户态切换到内核态的 3 种方式:
- 系统调用
- 中断
- 异常
具体的切换操作: 以 open 函数调用为例
open 函数调用时,会通过中断陷入内核,从而调用 sys_open 函数。
- 系统调用触发 0x80 中断: 并且将系统调用号存储在 eax 寄存器中,然后陷入内核,内核开始执行中断处理程序,在系统调用表中查找系统调用号对应的系统内核函数并且调用,执行完成后又将返回值通过 eax 寄存器返回给用户空间
- 中断机制: 中断处理程序(内核 )
计算机处于执行期间,系统内发生了非寻常或非预期的急需处理事件,CPU 暂时中断当前正在执行的程序而转去执行相应的事件处理程序,处理完毕后返回原来被中断处继续执行处理中断源的程序称为中断处理程序。中断的实现由软件和硬件综合完成,硬件部分叫做硬件装置,软件部分称为软件处理程序。
4. 锁升级
对象头 markword 记录了
锁升级步骤:
4.1. 偏向锁
偏向锁假定将来只有第一个申请锁的线程会使用锁。因此,只需要在 Markword 中 CAS 记录 当前线程指针,如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁。
以后当前线程记录的这个线程就可以零成本的直接获得锁;否则,说明有其他线程竞争,膨胀为轻量级锁
源码: fast_enter 方法中在 safepoint 的时候上锁,失败则调用 slow_enter 方法升级为自旋锁。
在明确知道会有多个线程竞争的情况下,偏向锁会涉及锁撤销,比自旋锁效率低,所以这时不会启用偏向锁
例如:JVM 启动过程会有很多线程竞争,所以默认情况启动时不打开偏向锁,过一段时间才会打开
-XX:BiasedLockingStartupDelay=4(默认 4 秒),刚开始未偏向任何一个线程,所以称为匿名偏向
4.2. 自旋锁
轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步。详情查看自旋锁。
原理:
每个线程在其线程栈内部生成一个 LockRecord,拿到锁后记录在 markword 中。即演化为多个 LockRecord 使用 CAS 竞争写入 markword 的场景。
对应 slow_enter 方法,首先进入自旋,自旋锁不行则调用 inflate 方法膨胀为重量级锁。
升级:
- 1.6 之前,有线程超过 10 次自旋(-XX:PreBlockSpin),或者自旋线程数超过 CPU 核数的一半。则升级为重量级锁。
- 1.6 之后,加入自适应自旋 Adapative Self Spinning,由 JVM 自己控制何时升级成重量级锁。
4.3. 重量级锁
synchronized 编译为字节码后代码由 monitorenter
和 monitorexit
包围,表示上锁和释放锁。
synchronized 加锁需要程序由用户态切换到内核态,效率低。
内核态 ObjectMonitor 对象中有 WaitSet 队列,抢锁的线程都会进入等待队列,不需要消耗 CPU 资源,由操作系统进程调度
4.4. 可重入锁
重入次数必须记录,因为加锁次数必须和解锁次数对应
- 偏向锁 / 自旋锁:记录在线程栈中,每重入一次,LockRecord + 1。LockRecord 指向备份的 markword(displace head),里面记录了 HashCode,重入的 LockRecord 指向 null
- 重量级锁:记录在 ObjectMonitor 一个字段上
5. 锁优化
5.1. 锁消除
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除
如下 concatString 方法:
每个 append () 方法中都有一个 synchronized 同步块。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString () 方法内部。也就是说,sb 的所有引用永远不会逃逸到 concatString () 方法之外,其他线程无法访问到它,因此可以进行消除
1 | public static String concatString(String s1, String s2, String s3) { |
原理: 锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除
5.2. 锁粗化
如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗
虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,则会把加锁的范围扩展(粗化)到整个操作序列的外部。
6. Monitor
- Monitor 是一种用来实现同步的工具
- 与每个 java 对象相关联,所有的 Java 对象是天生携带 monitor
- Monitor 是实现 Sychronized(内置锁)的基础
- Monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现
1 | ObjectMonitor() { |