synchronized锁升级

513 阅读9分钟

锁(Synchronized)

Synchronized的基本认识

synchronized的基本认识在多线程并发编程中synchronized一直是元老级角色,很多人都会称呼它为重量级锁。但是,随着Java SE 1.6对synchronized进行了各种优化之后,有些情况下它就并不那么重,Java SE 1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。这块在后续我们会慢慢展开

synchronized的基本语法

synchronized有三种方式来加锁,分别是

1.修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁

2.静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁。

3.修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

synchronized的应用

package com.example.demo.demo.threaddemo;
/**
 * @author wangjian
 * @createDate 2021/2/18
 * @describe
 **/
public class demotest {
    public static  int count = 0;
    public synchronized static void incr(){
        try{
            Thread.sleep(1);
        }catch (Exception e){
            e.printStackTrace();
        }
        count ++ ;
    }
    public static void main(String[] args) throws InterruptedException {
        for(int i =0 ;i<1000 ;i++ ){
            new Thread(()->demotest.incr()).start();
        }
        Thread.sleep(2000);
        System.out.println(count);
    }
}

对象在内存中的布局

在HotSpot虚拟机中,对象在内存中的布局可以分为3个区域:对象头(Header),实例数据(Instance Data),对齐填充(Padding)

image-20210220153535475.png

MarkWord

Mark word 记录了对象和锁有关的信息,当某个对象被synchronized 关键字当成同步锁时,那么围绕这个锁的一系列操作都和Mark word 有关系。Mark Word 在 32 位虚拟机的长度是 32bit、在 64 位虚拟机的长度是 64bit。 Mark Word 里面存储的数据会随着锁标志位的变化而变化, Mark Word 可能变化为存储以下 5 中情况

image-20210220160154358.png

为什么任何对象都可以实现锁

1 首先,java中的每个对象都派生自Object类,而每一个java object 在jvm内部都有一个native的c++对象oop/oopdesc 对应

2 线程在获得锁的时候实际上就是获取一个monitor监视器对象,monitor 可以认为是一个同步对象,所有的Java 对象是天生携带 monitor。多个线程访问同步代码快的时候,相当于争抢这个监视器对象,修改对象中的锁标志位

synchronized 锁的升级

markword中,存在4种锁变化的情况。分别为 无锁 偏向锁 轻量级锁 重量级锁,锁虽然能够实现数据的安全性,但是使用所会导致性能的下降。不使用锁能够基于线程并行提升程序性能,但是却不能保证线程安全性,那么如何解决因为锁带来性能的开问题呢?hotspot 虚拟机的作者经过调查发现,大部分情况下,加锁的代码不仅仅不存在多线程竞争,而且总是由同一个线程多次获得。所以基于这样一个概率,是的 synchronized 在JDK1.6 之后做了一些优化,为了减少获得锁和释放锁带来的性能开销,引入了偏向锁、轻量级锁的概念。因此大家会发现在synchronized 中,锁存在四种状态。分别是:无锁、偏向锁、轻量级锁、重量级锁; 锁的状态根据竞争激烈的程度从低到高不断升级。

img

无锁

无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。

偏向锁

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。

当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的ID,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了

偏向锁的获取和撤销逻辑

1 首先获取锁对象的markword,判断是否处于可偏向的状态(biased_lock=1、且 ThreadId 为空)

如果是可偏向状态,则通过cas操作,把当前的线程id写入到Mark word,

​ a> 如果cas成功,表示已经获得了锁对象的偏向锁,接着执行同步代码块

​ b> 如果cas 失败则表明其他线程已经获得偏向锁,说明存在锁竞争,需要撤销已获得偏向锁的线程,并且把它持有的锁升级为轻量级 锁

3 如果是已偏向状态,则要检查mark word中的线程id是否等于当前线程id

a>如果相等,则不需要获取偏向锁,执行同步代码快

b> 如果不相等,说明当前锁偏向于其他线程,需要撤销偏向锁并升级到轻量级锁

偏向锁的撤销

​ 偏向锁的撤销并不是把对象恢复到无锁可偏向状态(因为偏向锁并不存在锁释放的概念),而是在获取偏向锁的过程中,发现 cas 失败也就是存在线程竞争时,直接把被偏向的锁对象升级到被加了轻量级锁的状态。

偏向锁的撤销存在2种情况 :

a> 原获得偏向锁的线程如果已经退出了临界区,也就是同步代码块执行完了,那么这个时候会把对象头设置成无锁状态并且争抢锁的线程可以基于 CAS 重新偏向当前线程

b> 如果原获得偏向锁的线程的同步代码块还没执行完,这个时候会把原获得偏向锁的线程升级为轻量级锁后继续执行同步代码块

流程分析:(图片是copy的)

image-20210220163032319.png

轻量级锁加锁和解锁逻辑

锁升级为轻量级锁之后,对象的 Markword 也会进行相应的的变化。

a> 线程在自己的栈桢中创建锁记录LockRecord。

b> 将锁对象的对象头中的MarkWord 复制到线程的刚刚创建的锁记录中。

c> 将锁记录中的Owner 指针指向锁对象

d>将锁对象的对象头的MarkWord 替换为指向锁记录的指针

image-20210220164606117.png

自旋锁(默认10次)

轻量级锁在加锁过程中,用到了自旋锁。所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。注意,锁在原地循环的时候,是会消耗 cpu 的,就相当于在执行一个啥也没有的for 循环。

自适应自旋锁

​ 在 JDK1.6 之后,引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过, 那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源

轻量级锁的解锁

​ 轻量级锁的锁释放逻辑其实就是获得锁的逆向逻辑,通过CAS 操作把线程栈帧中的 LockRecord 替换回到锁对象的MarkWord 中,如果成功表示没有竞争。如果失败,表示当前锁存在竞争,那么轻量级锁就会膨胀成为重量级锁

image.png

image.png

重量级锁加锁的过程

7.1 monitor对象简介

重量级锁的状态下,对象的 mark word为指向一个堆中monitor对象的指针。

一个monitor对象包括这么几个关键字段:

  • ContentionList:竞争队列,所有请求锁的线程首先被放在这个竞争队列中
  • EntryList:ContentionList中那些有资格成为候选资源的线程被移动到EntryList中
  • WaitSet:哪些调用wait方法被阻塞的线程被放置在这里
  • OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck

Owner:当前持有锁的线程

其中ContentionList,EntryList,WaitSet都是由ObjectWaiter的链表结构,owner指向持有锁的线程。

image.png

锁的升级方案简单总结

0

  • 偏向锁只会在第一次请求时采用 CAS 操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程中,持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被在同一时间只会被同一线程持有的情况(即不存在竞争关系)。

  • 轻量级锁采用 CAS 操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。它针对的是短时间之内的竞争关系。

  • 重量级锁会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况即竞争比较激烈的情况下。Java 虚拟机采取了自适应自旋,来避免线程在面对非常小的 synchronized 代码块时,仍会被阻塞、唤醒的情况