首页 > 编程语言 >Java并发02---Synchronized的实现原理、锁的升级、锁的膨胀、对象头、锁的消除、偏向锁、轻量级锁、轻量级锁

Java并发02---Synchronized的实现原理、锁的升级、锁的膨胀、对象头、锁的消除、偏向锁、轻量级锁、轻量级锁

时间:2024-04-26 18:22:06浏览次数:17  
标签:02 加锁 Java synchronized 对象 线程 偏向 轻量级

@

目录

何为synchronized

我们知道,synchronized关键字能够将其修饰的代码块、方法、静态方法变成同步代码。我们在前文中已经介绍过了,使用volatile关键字修饰能保证变量在内存中的可见性,但不保证操作的原子性,而synchronized既能保证可见性又能保证原子性与互斥性。

关于保证变量的内存可见性synchronized的实现原理是:线程获取到锁,进入synchronized代码段的时候会强制从堆内存中加载代码段内部变量的值,并且在解锁的时候会将栈内存中变量的最新值写回堆内存,从而保证变量的内存可见性。

关于互斥性,继续往下看!

前置知识:对象头

我们回顾一个经典的八股文:Java中对象的内存布局。
一个对象由三部分组成,对象头、对象的实例内容、对齐填充。其中,对象头包括了这个对象的哈希值、GC分代年龄、锁状态信息、偏向线程ID、偏向时间戳;对齐填充会使整个对象的长度为8的倍数。
Java中对象的内存布局

但其实,对象头不是同时包含这么多信息的。根据这个对象所处的不同锁状态,对象头存储的是不同的内容:
对象头信息
我们知道 synchronized会把一个对象当作锁

  • synchronized作用对象为代码段时,若不指定,加锁的对象默认为this;若指定,如synchronized (obj),加锁对象为obj
  • synchronized作用对象为实例方法时,加锁的对象为this
  • synchronized作用对象为静态方法时,加锁的对象该类的class对象。

而每个对象所持有的锁的信息正是存储在对象头中,在后文中我们介绍锁的升级的时候会使用到当前对象头中所存储的信息。

锁的升级(锁的膨胀)

现在让我们看看synchronized到底是怎么加锁的。

在JDK1.6之前,synchronized都是重量级锁,但是重量级锁的加锁、解锁、阻塞涉及到用户态到内核态的切换,以及线程上下文的切换,导致性能开销较大。所以引入了锁的升级。

升级过程为:无锁->偏向锁->轻量级锁->重量级锁。并且锁只有升级没有降级。

接下来我将依次介绍各个锁的加锁过程。

偏向锁

假如一个线程需要重复进入一个同步代码段,这个线程会重复的进行加锁和解锁的操作,造成不必要的资源开销,所以JVM引入了偏向锁。

  1. 若该对象处于无锁状态,当一个线程尝试加锁时会将对象头的无锁状态改为偏向锁状态,并将这个线程的ID记录到对象头中,将Epoch置为1.
  2. 此时该对象处于偏向锁状态,当其他线程尝试加锁时,会首先检测对象头中的锁的偏向线程是不是自己,若是自己则可以获取锁。这样就省去了有关锁申请的操作。若不是自己JVM会撤销该偏向锁,并且当所有持有锁的线程到达安全点的时候将偏向锁升级为轻量级锁。
  3. 当某一个类(Class)的偏向锁被撤销到一定次数(默认为20)的时候,JVM会宣布该类往后所有的偏向锁进入下一代,之后该类所有的对象的对象头中Epoch会加一。
    同时JVM会遍历查找当前已有的该类的对象,当这些对象处于安全点的时候将他们的Epoch加一。
  4. 当Epoch达到阈值(默认为40)的时候,JVM会撤销该类的所有偏向锁,在之后的锁的升级的过程中会直接升级为轻量级锁,不会升级为偏向锁。

轻量级锁

轻量级锁与锁的自旋是为了避免线程因为获取不到锁而发生阻塞,进而引发用户态和内核态切换。

在线程试图获取轻量级锁的过程中会自旋,而不是直接阻塞,这样就可以节省不必要的性能开销。

不理解的可以回顾自旋锁CAS操作

轻量级锁加锁过程如下:

  1. 当一个线程尝试为一个轻量级锁加锁的时候,会尝试将对象头中的锁记录(Lock Record)使用CAS操作修改为当前线程。
  2. CAS操作成功执行,则获取到锁。
  3. CAS操作失败,说明还有别的进程在竞争该锁,且已经获得了该锁,那么该线程会进行自旋,重新尝试获取锁,当自旋次数达到阈值时,若还没有成功获取到锁该锁会升级为重量级锁。

在轻量级锁的加锁过程中是不会发生ABA问题的,感兴趣的读者可以思考一下。

轻量级锁

由于对该锁的竞争实在是过于激烈,该对象的锁最终会升级为重量级锁,锁对象的对象头会再次发生变化,Mark Word会指向一个监视器(ObjectMonitor)对象,它是一个cpp类,其包含一个队列来登记和管理排队的线程。

重量级锁加锁过程如下:

  1. 当线程想要获取重量级锁时会执行ObjectMonitor::enter()指令,其也是通过CAS操作尝试获取锁,若成功获取锁,ObjectMonitor会将该锁的拥有者设置为该线程。
  2. 若没有成功获取锁,ObjectMonitor会将该线程阻塞,并加入竞争者队列等待下一次竞争,在操作系统角度,整个过程是基于互斥量mutex进行的操作。
  3. 当线程执行完毕后会执行ObjectMonitor::exit()指令,此时ObjectMonitor会将该锁的拥有者设置为null,并通知等待队列中的线程,让他们进行竞争。

我们可以发现,在整个锁的升级过程中,在偏向锁和轻量级锁阶段,是没有线程被真正阻塞的,只有在重量级锁阶段才会被阻塞。

由于重量级锁的竞争比较复杂,我这里介绍的是一种比较理想的形式,想要深入研究可见Java面试常见问题:Monitor对象是什么?

锁的消除

朋友们读到这一节可能会疑惑,锁不是只有升级没有降级吗,锁的消除又是啥?

锁的消除其实是虚拟机另外一种锁的优化,JVM在进行JIT编译时会对对象进行逃逸分析,若逃逸分析发现该对象不会产生线程安全问题会将其持有的锁消除。

让我们回顾一个经典的八股文:Java是所有的对象都储存在堆中吗?
这个分析的过程就涉及到逃逸分析,简单来说就是如果JVM发现一个对象是局部变量,生存周期仅包含了当前的栈帧,JVM就会将该对象储存在栈中,并不会在堆中为其分配内存。

然后我们会发现,如果一个对象的生存周期仅包含当前的栈帧,那么就不会产生线程安全问题是不是,那这个对象加不加锁也没有影响,这时就会进行锁的消除。

不理解的可以回顾JMM

我们来看一个例子,引自大彻大悟synchronized原理,锁如何升级
StringBufferappend是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。

public class StringBufferRemoveSync {
    public void add(String str1, String str2) {
        //StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
        //因此sb属于不可能共享的资源,JVM会自动消除内部的锁
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
    }

    public static void main(String[] args) {
        StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
        for (int i = 0; i < 10000000; i++) {
            rmsync.add("abc", "123");
        }
    }
}

标签:02,加锁,Java,synchronized,对象,线程,偏向,轻量级
From: https://www.cnblogs.com/Alanxtl/p/18160643

相关文章

  • javascript高级编程系列 - 使用fetch发送http请求
    fetch采用模块化设计,api分散在多个对象上(Response对象,Request对象,Header对象),fetch通过数据流(stream对象)处理数据可以分块读取,有利于提高网站性能。发送GET请求fetch函数只传递一个url,默认以get方法发送请求。promisefetch(url).then(response=>response.json()).......
  • [BJDCTF2020]EasySearch
    [BJDCTF2020]EasySearch打开环境页面中与源代码没有发现什么有用的信息,通过扫描工具扫描,看看有没有什么有用的文件结果发现无论是dirsearch、dirmap还是御剑,通通扫描不出来什么有用的东西,查了师傅们的WP才知道,这里的文件名是index.php.swp<?php ob_start(); functionget_......
  • Java树形结构
    表结构createtablecommon_tree(idbigintnotnullcomment'主键'primarykey,p_idbigintnullcomment'父节点id',tree_codevarchar(100)nullcomment'树形区分',tree_describevarch......
  • java反汇编命令手册
    1.栈和局部变量操作1.1将常量压入栈的指令指令功能描述aconst_null将null对象引用压入栈iconst_m1将将int类型常量-1压入栈iconst_0将int类型常量0压入栈iconst_1将int类型常量1压入栈iconst_2将int类型常量2压入栈iconst_3将int类型常量3压入......
  • Java四种实现单例模式
    饿汉式/***1.饿汉式:线程安全,耗费资源*场景:*资源共享:当需要在多个模块中共享同一个实例时*全局访问点:作为全局唯一的访问点,例如日志记录器、配置管理器等。*线程安全要求高:饿汉式单例模式在类加载时就创建实例,因此不存在线程安全问题,适合多线程环境下使用。*避......
  • 2024-04-26 moment.js和day.js之用法和优缺点对比
    用法对比:Moment.js:时间戳转时间格式:moment(timestamp).format('YYYY-MM-DDHH:mm:ss')时间转时间戳:moment(dateString,'YYYY-MM-DDHH:mm:ss').valueOf()Day.js:时间戳转时间格式:dayjs(timestamp).format('YYYY-MM-DDHH:mm:ss')时间转时间戳:......
  • 从 Java 8 转换到 Java 11
    截至目前(2024年),十年前发布的Java8依然是Java中应用最广泛的版本,占比37%,其次是Java11。而目前的JDK最新版本为22,最新的LTS版本为JDK21。从Java8迁移到Java11可能意味着很大的工作量。潜在问题包括:删除的API、弃用的包、内部API的使用、对类加载程序的更......
  • Optimizing Nintendo 64 Code with Kaze Emanuar——程序员-听播客-学英语-02
    主题:OptimizingNintendo64CodewithKazeEmanuar来源/音频链接:https://softwareengineeringdaily.com/2024/04/05/bonus-episode-optimizing-nintendo-64-code-kaze-emanuar/全文(带中文翻译和注释):EPISODE1666[INTRODUCTION][0:00:00.0]ANNOUNCER:KazeEmanuarisaRO......
  • 2024 年 4 月 24 日 周三 雨转阴 凉(820 字)
    今晚应酬,因为想去上课,所以提前溜了。应酬的那家餐厅居然有油茶。真的十分纳罕,这个玩意自我初中之后就没再见到过了。尝了一口,和我小时候印象不太一样,这家做得不好吃。那家的尖椒鸡很好吃,虽然很辣,但是上头,停不下来。一直挑肉吃,吃四五口要辣得喝一口唯依,要不然真的辣得人心......
  • 24数媒Java上机2
    对于一个包含N个非负整数的数组A[0..n-1],如果有0<=i<j<n,且A[i]>A[j],则称(A[i],A[j])为数组A中的一个逆序对。现给定一组数,请你算出这组数中共有多少组逆序对。输入格式:共两行,第一行是一个整数N(N≤1000),表示这组数的个数。第二行有N个整数,用空格分隔,为这组数。测试......