JVM同步

JVM中的同步是基于进入和退出对象monitor来实现的,每个对象的对象头中都会存放一个monitor

monitor的底层实现依赖与操作系统的互斥锁mutex,当线程获取到对象的monitor时,则线程持有了该mutex对象,该对象会将该线程标记为自己的monitor的owner,因为mutex是互斥的,即同一时刻只能被一个线程所持有,此时其他的线程如果也在争抢该mutex,那么这些线程将会被放入到一个EntryList集合中,进入阻塞状态

当持有对象monitor,也就是mutex的线程调用了对象的wait方法,那么该线程就会释放mutex,并且自己进入对象的monitor的等待集合中,等待其他的线程调用同一对象的notify或notifyAll方法来唤醒自己。当线程执行完方法后,也会释放monitor

monitor依赖于操作系统实现,这样线程会在内核态与用户态之间切换。先来简单了解一下什么是用户态,什么是内核态

参考文章:Linux探秘之用户态与内核态

15940856912562.jpg

用户态

用于运行用户程序,简单来说,我们日常编写的应用代码都是处于用户态的,应用程序必须依托于内核提供的资源,如CPU,存储,I/O等

用户态的程序可以通过3种方式来访问内核态资源

  1. 系统调用
  2. 库函数
  3. Shell脚本

内核态

控制计算机的硬件资源,并提供上层应用程序运行的环境,为了使上层应用能够访问到如CPU,存储等资源,内核需要提供上层用于访问内核的接口:系统调用

系统调用

系统调用是操作系统的最小功能单位,一个系统调用就意味着一个最小的不可再分割的操作,类似于原子操作,但是他们不是同一概念

15940856006906.jpg

通过观察上图可以看到,用户态(User Space)中的操作都需要经过系统调用来实现,通过系统调用,用户态程序可以访问内核态(Kernel)的资源

总结一下这张图,内核的作用为,对上层用户态应用提供系统调用的接口,对下层可控制计算机硬件,对内部可以管理进程,内存,文件,设备驱动,网络等各种系统资源

由于操作系统的资源是有限的,如果访问的资源过多,必然会消耗过多的资源,如果不对资源加以区分,很可能造成资源访问的冲突。

在Linux/Unix系统中,针对这一问题,实现了对不同资源操作的执行等级划分,即特权,特权等级越高能操作的关键资源越多。

Intel的X86架构的CPU提供了0到3四个特权级,数字越小,特权越高,Linux操作系统中主要采用了0和3两个特权级,分别对应的就是内核态和用户态。用户态进程可以访问和操作的资源都是有限的,而内核态中的进程在资源的使用上则没有限制,如果用户态进程需要访问到资源级别较高的资源,需要调用内核态进程,这就涉及到了内核态与用户态的切换

那什么时候会发生这种切换?通常有3种情况

  1. 系统调用:如C函数库中的malloc()函数,printf()函数等,他们都是使用了系统调用来完成分配内存、打印操作的
  2. 异常事件:当CPU在执行用户态程序时,如果突然发生异常就会触发从当前用户态执行的进程转向内核态执行相关的异常事件,如缺页异常
  3. 外围设备中断:当外围设备完成用户的请求操作后,会向CPU发出中断信号,此时CPU会暂停执行下一条将要执行的命令,而去执行中断信号对应的处理程序,如果之前执行的指令是在用户态下,则此时就会由用户态转入内核态

内核态与用户态的切换是需要消耗系统资源的


所有存在于EntryList集合和等待集合中的线程都处于阻塞状态,此时它们都会切换到内核态,进入内核调度状态,如果这种切换非常频繁,会增加性能开销,严重影响锁的性能

如何解决这种内核态与用户态频繁切换的问题?使用自旋锁

当多个线程对同一把monitor进行争抢时,如果monitor的owner线程能够在短时间内执行完代码并释放monitor,那么这些争抢该monitor的线程就可以进行短时间的等待,类似于执行了一段时间的while循环,此时这些线程处于忙状态,在owner线程释放monitor后,自旋的线程就会争抢,如果一段时间内争抢不到monitor,那么再切换为阻塞状态。因为自旋是需要消耗CPU资源的,而CPU资源是十分珍贵的,所以不能有大量的长时间自旋的线程存在,这样会严重影响系统的性能。

自旋锁,只有在多处理器、多核心的情况下才有用