Java多线程之锁的升级
先说说为什么会有锁升级
因为Sycronized是重量级锁(也是悲观锁),每次在要进行锁的请求的时候,如果当前资源被其他线程占有要将当前的线程阻塞加入到阻塞队列,然后清空当前线程的缓存,等到锁释放的时候再通过notify或者notifyAll唤醒当前的线程,并让其处于就绪状态。这样线程的来回切换是非常消耗系统资源的,而且有的时候,线程刚挂起资源就释放了。而Java的线程是映射到操作系统的原生线程之上的每次线程的阻塞或者唤醒都要经过用户态到核心态或者核心态到用户态的转化,这样是十分浪费资源的,这样就会造成性能上的降低,因此JVM对Sychronized进行了优化,将Sycronized分为三种锁的级别:偏向锁,轻量级锁,重量级锁。
很多的时候,对于一个可能发生并发访问的对象而言,其实很少会被竞争,就算有些资源存在竞争也是在很少的一段时间资源就会被释放,而这样的情况下将线程挂起是十分浪费性能的。
偏向锁(乐观锁):
当锁对象第一次被线程获取的时候,虚拟机会将锁对象的对象头中的锁标志位设置成为01,并将偏向锁标志设置为1,线程通过CAS的方式将自己的ID值放置到对象头中(因为在这个过程中有可能会有其他线程来竞争锁,所以要通过CAS的方式,一旦有竞争就会升级为轻量级锁了),如果成功线程就获得了该轻量级锁。这样每次再进入该锁对象的时候不用进行任何的同步操作,直接比较当前锁对象的对象头是不是该线程的ID,如果是就可以直接进入。
偏向锁升级为轻量级锁
偏向锁是一种无竞争锁,一旦出现了竞争大多数情况下就会升级为轻量级锁。现在我们假设有线程1持有偏向锁,线程2来竞争偏向锁会经历以下几个过程:
1. 首先线程2会先检查偏向锁标记,如果是1,说明当前是偏向锁,那么JVM会找到线程1,看线程1是否还存活着2
2. 如果线程1已经执行完毕,就是说线程1不存在了(线程1自己是不会去释放偏向锁的),那么先将偏向锁置为0,对象头设置成为无锁的状态,用CAS的方式尝试将线程2的ID放入对象头中,不进行锁升级,还是偏向锁
3. 如果线程1还活着,先暂停线程1,将锁标志位变成00(轻量级锁)然后在线程1的栈帧中开辟出一块空间(Display Mark Word)将对象头的Mark Word置换到线程一的栈帧当中,而对象头中此时存储的是指向当前线程栈帧的指针。此时就变成了轻量级锁。继续执行线程1,然后线程2采用CAS的方式尝试获取锁。
轻量级锁与偏向锁最大的不同之处
轻量级锁和偏向锁的不同之处就在于轻量级锁对于获取锁对象采用CAS的同步方式而偏向锁直接是把整个同步过程给取消。
轻量级锁(乐观锁)
轻量级锁如何创建在上面已经讲过了,接下来说说轻量级锁如何获取锁对象,轻量级锁是通过CAS也就是自旋的方式尝试获取锁对象,一旦失败会先检查,对象头中存储的是否是指向当前线程栈帧的指针,如果是,就可以获取对象,如果不是说明存在竞争那么就要膨胀为重量级锁。轻量级锁的解锁也是通过CAS的方式尝试将对象头的Mark Word和线程中的Display Mark Word替换回来,如果成功,就释放锁,如果失败说明还有许多其他等待锁的线程(说明此时已经不是轻量级锁而是重量级锁了),会将这些线程唤醒,然后释放锁。
轻量级锁膨胀为重量级锁
一旦有两条以上的线程竞争锁,轻量级锁膨胀为重量级锁,锁的状态变成10,此时对象头中存储的就是指向重量级锁的栈帧的指针。而且其他等待锁的线程要进入阻塞状态,等待重量级锁释放再来被唤醒然后去竞争。