前段时间在看Synchronized的原理,对于Synchronized关键字中偏向锁、轻量级锁、重量级锁的概念、升级过程的学习中,一直感觉不够清晰。这里记录一下我对这几种锁的理解。
首先,Synchronized关键字开始是没有偏向锁、轻量级锁概念,而是只有重量级锁,重量级锁是基于操作系统的互斥量(mutex)实现的,而这种实现方式非常低效,因为每一次加锁和解锁都需要进行内核态和用户态之间的转换(内核态和用户态的简单理解放到最后)。为了减少这种性能消耗,Java SE在1.6版本映入了偏向锁、轻量级锁的概念。
偏向锁和轻量级锁应对的场景
偏向锁:
经研究发现,大多数情况下,锁不但不存在竞争的情况,而且常常被同一个线程获取。基于这个情况,如果一个线程每次获取锁的时候都使用重量级锁,那么开销将会非常大。偏向锁就是用于应对这样的场景,当一个线程获取了一次偏向锁,那么这个线程再一次进入同步代码块的时候,只需要比较对象头里面存的线程ID和当前线程ID(synchronized原理,对象头存储锁信息),如果两个线程ID相同,就直接进入同步代码块,大大减少了使用重量级锁时加锁、解锁还要进行内核态用户态转换的开销。
偏向锁只能应对一个线程重复进入同步代码块的场景(一个线程进入同步代码块后退出,然后另一个线程进入同步代码块,这种没有竞争的情况也适用),如果出现两个线程需要同时进入一个同步代码块,这时必然存在锁的竞争情况,偏向锁无法应对这种情况,只能升级为轻量级锁。
轻量级锁:
对于锁竞争不是特别激烈的情况,如同步代码块比较简单,线程持有锁的时间很短。这种情况如果使用重量级锁,加锁和解锁的开销可能比锁住的同步代码块的执行开销还大。轻量级锁则用于应对这样的情况,当发生锁竞争时,没有拿到锁的线程采用自旋的方式来获取锁,如果在短时间能拿到锁,那么开销会远远小于使用重量级锁产生的用户态内核态转换的开销。
轻量级锁的自旋不是一直自旋的,而是有一定次数限制,当自旋次数超过了阈值,则会升级为重量级锁。
当清楚了偏向锁。轻量级锁的应用场景后,再深入研究Synchronized关键字锁膨胀过程就会清晰不少,这里粘一个我从别人那里盗的图。(Synachronized关键字锁膨胀过程图)。具体概念就可以找找大神们的博客了。
内核态和用户态的理解
1.为什么要区分用户态和内核态:
对于一个计算机来说,对硬件的操作权限只对操作系统开发,而不应该对一般应用系统开发,应为对硬件的操作十分复杂,并且风险极高,一旦出错会导致整个机器的宕机。工作在在内核态的程序拥有的权限最高,可以直接访问硬件。而在用户态的应用拥有的权限很低,不能直接访问硬件。
例如:现在有一个应用A需要从网卡读取数据,A不能直接访问网卡,而需要通过操作系统来访问网卡读取数据。通过操作系统访问网卡的操作就需要在内核态进行。此时就发生了用户态到内核态的切换。
2.如何进行用户态和内核态的切换(或者:用户态切换到内核态到入口在哪里呢):
这是Linux操作系统的架构图:
从上图我们可以看出来通过系统调用将Linux整个体系分为用户态和内核态,为了使应用程序访问到内核的资源,如CPU、内存、I/O,内核必须提供一组通用的访问接口,这些接口就叫系统调用。
学过C语言或C++语言的同学对库函数肯定不陌生,比如我们要创建一块内存空间,需要使用malloc函数,malloc就是库函数,而malloc使用的系统调用是sbrk函数。所以应用在分配内存,就会通过系统调用进入内核态,完成内存分配,然后退出内核态。
3.内核态和用户态的切换为什么开销大呢
内核态和用户态的切换会做以下几个事情:
1.保留用户态现场(上下文、寄存器、用户栈等)
2.复制用户态参数,用户栈切到内核栈,进入内核态
3.额外的检查(因为内核代码对用户不信任)
4.执行内核态代码
5.复制内核态代码执行结果,回到用户态
6.恢复用户态现场(上下文、寄存器、用户栈等)