Java锁性能提高机制
锁的使用很难避免,如何尽量提高锁的性能就显得比较重要了
锁偏向
所谓的偏向锁是指在对象实例的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阻塞,唤醒线程。
Java锁升级简述
什么是锁:在并发环境下,多线程针对同一个资源进行争抢,可能会导致数据出现异常,为了解决这个问题,就引入了锁机制。通过锁对资源进行锁定。
在JVM当中,寄存器、虚拟机栈、本地方法栈是线程独享(安全)的。但是Java堆(保存所有的Java对象和数组)、方法区、运行时常量池是线程共享的,就有可能出现线程安全问题。每个object对象都有一把锁,这个锁在对象头中,记录着这个对象在被哪个线程占用着。
对象头结构
Java的对象分为三个部分:对象头、实例数据、对齐填充字节。
对象头存放了对象本身的运行时信息。对象头包含了两部分:Mark Word和ClassPointer。相对于实际数据,对象头属于一些额外的内存开销。因此被设计的极小以提升效率。其中,Mark Word中,就包含了锁信息。
锁状态 | 25bit | 4bit | 1bit | 2bit | |
---|---|---|---|---|---|
23bit | 2bit | 是否偏向锁 | 锁标志位 | ||
无锁 | 对象的hashCode | 分代年龄 | 0 | 01 | |
偏向锁 | 线程id | Epoch | 分代年龄 | 1 | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 | |||
重量级锁 | 指向重量级锁的指针 | 10 | |||
GC标记 | 空 | 11 |
Mark Word(32位为例)
对象内存存在一把锁,锁信息放在了对象头的Mark Word当中。在最后两位中,代表了锁标志位:无锁、偏向锁、轻量级锁、重量级锁。
synchronized关键字
synchronized关键字可以用来同步线程,synchronized被编译后,会生成monitorenter,monitorexit两个字节码指令。JVM以此来进行线程同步。
monitor
monitor即为同步监视器,一个线程进入了monitor,其他线程只能够等待,当且只有这个线程退出,其他线程才有机会竞争到所资源进行执行。
Entry Set中聚集了一些想要进入Monitor的线程,它们处于waiting状态,假设某个名为A线程成功进入了Monitor,那么它就处于active状态。假设此时A线程执行途中,遇到一个判断条件,需要它暂时让出执行权,那么它将进入wait set,状态也被标记为waiting。这时entry set中的线程就有机会进入monitor,假设一个线程B成功进入并且顺利完成,那么它可以通过notify的形式来唤醒wait set中的线程A,让线程A再次进入Monitor,执行完成后便退出。
这就是synchronized的同步机制,但是synchronized可能存在性能问题,因为monitor是依赖于操作系统的Mutex Lock来实现的,Java线程事实上是对操作系统线程的映射,所以每当挂起或唤醒一个线程都要切换到操作系统的内核态,这个操作是比较重量级的。在一些情况下,甚至切换时间本身就会超出线程执行任务的时间,这样的话,使用synchronized将会对程序的性能产生影响。
从Java6开始,synchronized关键字就进行了优化,引入了“偏向锁”,“轻量级锁”,所以锁共有四种状态,分别为:无锁、偏向锁、轻量级锁、重量级锁。
锁的四种状态
无锁
- 无锁顾名思义就是没有对资源进行锁定,所有线程都能够访问同一资源。在出现资源竞争的情况下,不想对资源进行锁定,但是还是想通过某种手段进行多线程的控制。假如有一个累计值,我们不想通过锁定资源的方式进行限制,但是想控制只有一个线程能修改成功的话,就可以通过CAS操作(可以看AutomaticInteger中对unsafe类的操作)。但是当出现循环的时候,某个时间的cpu占用率会变得很高,而且可能出现ABA问题(通过版本号解决)。
偏向锁
- 当我们开始为对象进行加锁,假如某对象被加锁了,但在实际运行的时候,只有一条线程会获取这个对锁,那么我们最理想的方式,是不要通过系统状态切换,也不通过CAS获取锁,因为那样或多或少还是消耗了一些资源。我们设想的是最好对象能够认识这个线程,只要是这个线程过来,那么对象就直接把锁交出去。我们可以认为这个对象偏爱这个线程,所以被称为“偏向锁”。
- 那么偏向锁是怎么实现的呢?在Mark Word中,当锁标志为是01,那么判断倒数第三个bit是否为1,如果是1,那么代表当前对象的锁状态为偏向锁,于是再去读Mark Word的前23个bit,这23个bit就是线程ID,通过线程ID来确认想要获得对象锁的线程是不是之前所记录的线程ID。
轻量级锁
- 假如情况发生了变化,对象发现目前不只有一个线程,而是有多个线程正在竞争锁,那么偏向锁将会升级为轻量级锁。Mark
- Word发生变化,指向栈中Lock Record的指针。
重量级锁
- 倘若在轻量级锁的情况下,线程自旋次数过多,这个时候就会从轻量级锁转换成为重量级锁。Mark Word再次发生变化,指向重量级锁的指针。这个时候,其他线程试图获取锁时都会被阻塞,只有持有锁的线程释放锁之后才会唤醒这些线程。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程网。