Please enable Javascript to view the contents

讲的最清楚的java锁

 ·  ☕ 4 分钟

锁的使用很难避免,如何尽量提高锁的性能就显得比较重要了

内核态与用户态

当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(或简称为内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。即此时处理器在特权级最低的(3级)用户代码中运行。当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态。因为中断处理程序将使用当前进程的内核栈。这与处于内核态的进程的状态有些类似。

锁偏向

所谓的偏向锁是指在对象实例的Mark Word(说白了就是对象内存中的开头几个字节保留的信息,如果把一个对象序列化后明显可以看见开头的这些信息),为了在线程竞争不激烈的情况下,减少加锁及解锁的性能损耗(轻量级锁涉及多次CAS操作)在Mark Word中有保存这上次使用这个对象锁的线程ID信息,如果这个线程再次请求这个对象锁,那么只需要读取该对象上的Mark Word的偏向锁信息(也就是线程id)跟线程本身的id进行对比,如果是同一个id就直接认为该id获得锁成功,而不需要在进行真正的加解锁操作。其实说白了就是你上次来过了,这次又来,并且在这两次之间没有其他人来过,对于这个线程来说,锁对象的资源随便用都是安全的。这次用缓存来换取性能的做法,不过偏向锁在锁竞争不激烈的情景下使用才能获取比较高的效率。当使用CAS竞争偏向锁失败,表示竞争比较激烈,偏向锁升级为轻量级锁。

轻量级锁

所谓轻量级锁是比偏向锁更耗资源的锁,实现机制是,线程在竞争轻量级锁前,在线程的栈内存中分配一段空间作为锁记录空间(就是轻量级锁对应的对象的对象头的字段的拷贝),拷贝好后,线程通过CAS去竞争这个对象锁,试图把对象的对象头子段改成指向所记录空间,如果成功则说明获取轻量级锁成功,如果失败,则进入自旋(说白了就是循环)取试着获取锁。如果自旋到一定次数还是不能获取到锁,则进入重量级锁。

自旋锁

说白了就是获取锁失败后,为了避免直接让线程进入阻塞状态而采取的循环一定次数去试着获取锁的行为。(线程进入阻塞状态和退出阻塞状态是涉及到操作系统管理层面的,需要从用户态进入内核态,非常消耗系统资源),为什么能这样做呢,是因为试验证明,锁的持有时间一般是非常短的,所以一般多次尝试就能竞争到锁。

重量级锁

所谓的重量级锁,其实就是最原始和最开始java实现的阻塞锁。在JVM中又叫对象监视器。这时的锁对象的对象头字段指向的是一个互斥量,所有线程竞争重量级锁,竞争失败的线程进入阻塞状态(操作系统层面),并且在锁对象的一个等待池中等待被唤醒,被唤醒后的线程再次去竞争锁资源。

总结:所谓的锁升级,其实就是从偏向锁à轻量级锁(自旋锁)à重量级锁,之前一直被这几个概念困扰,网上的 文章解释的又不通俗易懂,其实说白了,一切一切的开始源于java对synchronized同步机制的性能优化,最原始的synchronized同步机制是直接跳过前几个步骤,直接进入重量级锁的,而重量级锁因为需要线程进入阻塞状态(从用户态进入内核态)这种操作系统层面的操作非常消耗资源,这样的话,synchronized同步机制就显得很笨重,效率不高。那么为了解决这个问题,java才引入了偏向锁,轻量级锁,自旋锁这几个概念。拿这几个锁有何优化呢?网上也没有通俗易懂的解释,其实说白了就是,偏向锁是为了避免CAS操作,尽量在对比对象头就把加锁问题解决掉,只有冲突的情况下才指向一次CAS操作,而轻量级锁和自旋锁呢,其实两个是一体使用的,为的是尽量避免线程进入内核的阻塞状态,这对性能非常不利,试图用CAS操作和循环把加锁问题解决掉,而重量级锁是最终的无奈解决方案,说白了就是能通过内存读取判断解决加速问题优于〉通过CAS操作和空循环优于〉CPU阻塞,唤醒线程。