栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Java锁--深入理解synchronized原理

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Java锁--深入理解synchronized原理

文章目录
  • 1.基本原理
    • 1.1 基本用法
    • 1.2 通过class信息分析synchronized
      • 1.2.1. 同步方法
      • 1.2.2. 同步代码块
  • 2.了解synchronized的基础
    • 2.1.对象头
    • 2.2 Monitor
  • 3.锁优化
    • 3.1 自旋锁
    • 3.2 自旋锁
    • 3.3 锁粗化
    • 3.4 锁升级
      • 3.4.1 重量级锁
      • 3.4.2 轻量级锁
      • 3.4.3 偏向锁
  • 4. 几种锁状态的对比

1.基本原理

synchronized关键字的作用,简单得总结就是
保证在运行的时候,只有一个方法能够访问临界区,并且它能够保存共享变量的在内存中的可见性。
suchronized是重量级锁,java SE 1.6对其进行了优化。

1.1 基本用法

synchronized使用的三种情况:

  1. 普通方法同步
public synchronized void test1() {
		// 同步代码
    }
  1. 代码块同步
public void test2() {
		synchronized(this){
			// 同步代码
		}
    }
  1. 静态方法同步
public static synchronized void test3() {
		// 同步代码
    }
1.2 通过class信息分析synchronized

通过对javap查看文件的class信息

上图为同步方法与同步代码块反编译信息

1.2.1. 同步方法

同步方法通过ACC_SYNCHRONIZED标记符进行实现。通过方法的指令可以查看该方法在常量池中是否包含ACC_SYNCHRONIZED标记符,如果存在,那么需要方法调用前请求锁。

1.2.2. 同步代码块

同步代码块是通过monitorenter、monitorexit指令实现。它们分别对应同步代码块的开始和结束的位置。当执行到monitorenter的时候,线程会尝试获取对象Monitor的所有权。任何对象都有一个 Monitor 与之相关联,当且一个 Monitor 被持有之后,他将处于锁定状态。可以理解为:当获取到对象的监控器的所有权,那么就同时获得了同步锁。

2.了解synchronized的基础 2.1.对象头

简单得聊一下对象头。Hotspot 虚拟机的对象头主要包括两部分数据:Mark Word(标记字段),Klass Pointer(类型指针)。

  • Klass Pointer:指向对象元数据的指针。它表示当前对象是哪个类的实例。
  • Mark Word:对象运行时自身的数据。在不同的锁状态,Mark Word的数据结构有所不同。

在不同的锁状态,Mark Word的状态变化如下

  • 32位虚拟机
  • 64位虚拟机

    对于 32 位无锁状态,有 25 bits 没有使用。
2.2 Monitor

Monitor – 监视器。每一个对象都有一个监控器

  • 在同步代码块中,JVM通过monitorenter和monitorexist指令实现同步锁的获取和释放功能
  • 当一个线程获取同步锁时,即是通过获取monitor监视器进而等价为获取到锁

Monitor Record(统一简称MR)

  • Monitor Record是每个线程私有的数据结构。每个线程都有一个MR列表,并且
  • 一个被锁住的对象会有一个关联的MR的列表。(对象头中的Mark Word的Lock Word将指向MR列表的起始地址)
  • 下图是MR列表每个字段的作用
3.锁优化

JDK1.6对锁进行了大量优化。在1.6之前,JVM通过monitorenter以及monitorexit加锁解锁的方式,实际是通过底层的Mutex Lock实现。Mutex Lock会将当前线程挂起,并且将操作系统从用户态转换为内核态,这种切换代价昂贵,严重影响性能。

3.1 自旋锁
  • 优化场景:线程频繁的阻塞和唤醒,对CPU的负担很重。当对象获取到锁的执行时间很短的时候,这种频繁和唤醒的操作是十分不值得的。
  • 解决办法:自旋锁。让线程不被挂起,而是进行自旋(即进行无异议的自旋)。
  • 优点:当线程持有锁的时间不长,不将线程挂起,减轻了CPU性能。
  • 缺点:如果线程持有锁的时间比较长,线程会一直进行自旋,这无疑是浪费了资源,得不偿失。
  • 关于缺点的解决:为了防止线程一直自旋,我们可以通过命令对自旋的次数或者自选的等待时间进行设置。JDK1.6引入了自适应的自旋锁,让JVM自己对自旋进行优化。
  • 关于自适应自旋锁:自旋锁的自选次数
    • 如果本次线程自旋成功获取到了锁,虚拟机会认为下次该线程也很有可能自旋成功,那么下次该线程允许的自旋次数就会增多。
    • 相反的,如果本次线程自旋失败,那么之后虚拟机会减少该线程的自旋次数
3.2 自旋锁

顾名思义,锁消除就是取消加锁的操作。锁消除的依据是逃逸分析的数据支持。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定。
有的时候,我们作为开发人员认为不会存在数据逃逸以及锁竞争的情况,就不会在代码里面进行加锁的操作。但是你没有手动加锁就百分之百确定代码里面没有隐藏的的加锁操作了吗?看看下面代码。

	public void vectorTest(){
	    Vector vector = new Vector();
	    for (int i = 0 ; i < 10 ; i++){
	    	vector.add(i + "");
	    }
	    System.out.println(vector);
	}

在上述的代码中,我们并没有手动操作,但是在Vector的内部的#add()存在隐性的加锁操作。(StringBuffer、HashTable都存在加锁操作)。运行这段代码的时候,JVM可以感知不存在数据逃逸的情况,所以JVM此时会消除Vector中#add()方法的加锁操作,这便是我们所说的锁消除。

3.3 锁粗化
  • 出现场景:在我们开发中,有时候会可以得缩小同步块的作用返回,实例代码如下:
	public void test(){
		for(int i = 0; i <10; i++){
			synchronized(this){
				// 执行语句
			}
		}
	}

这样做本身没有什么问题,这样可以缩短拿到锁的执行时间,让其他等待线程快速拿到锁。但是如果频繁的加锁和解锁操作,(如上面的例子,每次循环都要进行加锁和解锁)反而会增加不必要的损耗。此时我们需要进行锁粗化。

  • 概念:锁粗化,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁
    基于锁的粗化,可以对上面实例进行优化:
	public void test()	{
		synchronized(this){
			for(int i = 0; i <10; i++){
				// 执行语句
			}
		}
	}
3.4 锁升级

涉及到锁升级我们必须了解到锁的四种状态

  • 无锁状态
  • 偏向锁状态
  • 轻量级锁状态
  • 重量级锁状态
    注意:锁的状态只能进行升级,不能降级。
3.4.1 重量级锁

上面我们提到,重量级锁是基于Monitor监控器实现的。在底层是通过Mutex Lock进行实现的。需要操作系统从用户态切换为内核态,切换成本高。

3.4.2 轻量级锁

在没有多线程竞争的情况下,重量级锁的对系统消耗较大,故引入轻量级锁。当我们关闭偏向锁或者偏向锁存在锁竞争的情况下,会从偏向锁状态升级为轻量级锁。

  • 获取锁

    1. 如果当前对象是无锁状态,JVM会在当前线程的栈帧中创建一个名为Lock Record(锁记录)的空间,这个空间存放Mark Word的拷贝,官方将这份拷贝命名为 Displaced Mark Word。如果当前不是无锁状态,则执行步骤3
    2. JVM尝试通过CAS将当前Mark Word更新为Lock Record的指针,如果更新成功,那么说明获取到锁,此时锁的状态将更新为00(即轻量级锁)。如果更新失败,说明没有获取到锁,执行步骤3
    3. 判断当前Mark Word是否为Lock Record的指针。如果是,说明加锁成功。如果没有,说明该锁被其他线程抢占,那么线程将进行自旋来获取锁。如果没有成功获取,那么轻量级锁将升级会重量级锁,锁标志会升级为10,此时线程会阻塞。
  • 释放锁

    1. 取出当前Displaced Mark Word的值。
    2. 此时Mark Word是指向Lock Record的指针,JVM将通过CAS将Mark Word更新为Displaced Mark Word的值。如果更新成功,说明释放锁成功。如果失败,此时执行3
    3. 当2步骤失败,说明锁被其他线程占用。此时轻量级锁会膨胀成重量级锁,需要在释放锁的时候唤醒被阻塞的线程。

锁的膨胀过程如图所示

其中,绿框的 0 指的是无偏向锁,01 指的是无锁状态。

值得一提的是,轻量级锁只有在数据竞争不多的情况下才会有性能优势,如果竞争线程比较多,会导致导致大量线程进行CAS,反而浪费资源。

3.4.3 偏向锁

经过上面关于轻量级锁的介绍,我们知道轻量级锁的实现主要通过CAS。但是当无多线程竞争的情况下,这些CAS是多余的并且对性能产生影响,这个时候变引入了偏向锁。在上面锁膨胀的图中

这里的0代表是否是偏向锁,01代表锁标识。

  • 获取偏向锁
    1. 检测 Mark Word是 否为可偏向状态,即是否为偏向锁的标识位为 1 ,锁标识位为 01。
    2. 若为可偏向状态,则测试线程 ID 是否为当前线程 ID ?如果是,则执行步骤(5);否则,执行步骤(3)。
    3. 如果线程 ID 不为当前线程 ID ,则通过 CAS 操作竞争锁。竞争成功,则将 Mark Word 的线程 ID 替换为当前线程 ID ,则执行步骤(5);否则,执行线程(4)。
    4. 通过 CAS 竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块。
    5. 执行同步代码块

偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。
偏向锁的撤销需要等待全局安全点(这个时间点是上没有正在执行的代码)
下图是偏向锁的获取和释放流程:

4. 几种锁状态的对比

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/331914.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号