0%

Java并发面试题整理

背景知识

CPU 指令:CPU 的一些指令是非常危险的,误用可能导致严重后果,如清理内存、设置时钟等。Intel 将 CPU 指令分为4 个级别,RING0,RING1,RING2,RING3。Linux 使用 RING3级别运行用户态,RING0运行内核态。RING3 不能访问 RING0 的RING0 的地址空间,包括代码和数据。当用户态程序要执行文件操作、发送数据等操作时,需要通过 write、send 等系统调用,这些系统调用会调用内核中的代码来完成操作,必须切换到 RING0,完成后切换回 RING3,这样用户态程序就不能随意操作内核地址空间,有一定的安全保护作用。

从用户态到内核态有两种触发手段:

  1. 用户空间的应用程序通过提醒调用进入内核空间,用户空间进程将参数传递给内核,内核空间运行时也需要保存用户进程的寄存器、变量等。“进程上下文”可以看做用户进程传递给内核的参数、内核要保存的变量、寄存器和当时的环境等。
  2. 硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。硬件的变量和参数也要传递给内核,内核通过这些参数进行中断处理。“中断上下文”可以看做硬件传递过来的参数和内核需要保存的环境。

Synchronized

synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

synchronized 同步方法比普通方法多了 ACC_SYNCHRONIZED 标识,JVM 根据这个标识实现同步。调用方法时先检查是否有 ACC_SYNCHRONIZED标识,有的话线程先获取 monitor,获取成功才继续执行方法,方法执行完成后线程再释放 monitor,同一个 monitor 同一时刻只能被一个线程拥有。

两种方式都通过 monitor 来实现同步,monitor 的实现依赖底层操作系统的 mutex 互斥原语,操作系统实现线程之间的切换需要从用户态转到内核态,切换成本较大。

wait、notify 和 notifyAll 也依赖于 monitor 对象来完成的,所以要在同步方法或同步代码块中调用的原因(需要先获取对象的锁,才能执行)否则抛出 IllegalMonitorStateException 异常。

jdk1.6之后 synchronized 优化

重量级锁的实现基于操作系统的 mutex互斥原语,开销较大。JVM 对 synchronized 做了优化,先利用对象头实现锁的功能,如果线程竞争过大则将锁升级(膨胀)为重量级锁,也就是使用 monitor 对象。锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。这几个锁只有重量级锁需要操作系统底层 mutex 互斥原语实现,其它锁都使用对象头来实现。锁可以升级,但不能降级。

  1. 偏向锁:偏向锁在无竞争情况下把整个同步都消除掉,连 CAS 都不做。锁对象第一次被线程获取时,虚拟机将对象头中的标志位设为“01”,即偏向模式(JVM 参数可配置),同时使用 CAS 把获取到这个锁的线程 ID 记录在对象的 Mark Word 中,如果 CAS 操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块,虚拟机都不再进行任何同步操作(Locking、Unlocking、对 Mark Word 的 update 等)。当其它线程尝试获取这个锁时,偏向模式结束,恢复到未锁定(标志位 01)或轻量级锁定(标志位 00)的状态,后续按轻量级锁执行。
  2. 轻量级锁:在没有多线程竞争的前提下,减少传统重量级锁使用互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量,轻量级锁的加锁和解锁都使用 CAS 操作,如果有两条以上的线程争用同一个锁,轻量级锁直接膨胀为重量级锁。
  3. 自旋锁和自适应锁:一般线程持有锁的时间都不太长,仅仅为这一天时间去挂起/恢复线程得不偿失,为了让线程等待,只需要让线程执行一个忙循环(自旋),在 jdk1.6 中引入了自适应的自旋锁,自旋时间不再固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么 JVM 认为这次自旋也很有可能再次成功,进而允许自旋等待相对更长的时间,如 100 个循环;如果很少成功过,就忽略掉自旋过程,让虚拟机变得更“聪明”
  4. 锁消除和锁优化

参考资料

  1. 用户态和内核态
  2. 深入分析synchronized原理和锁膨胀过程