前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >synchronized 关键字

synchronized 关键字

作者头像
付威
发布2021-03-07 13:20:19
6720
发布2021-03-07 13:20:19
举报

Synchronized 是 Java 中的一种锁的方式,是在 JVM 层面一种锁。在 jdk 1.6以前是一种重量级锁,在经历过优化后 Synchronized 锁已经没有那么“重”了。

Synchronized 有 3 种使用方式:

  1. 普通同步方法,锁是当前实例对象
  2. 静态同步方法,锁是当前类的Class对象
  3. 同步代码块,锁是Synchonized括号里配置的对象
private static int i;
	//1. 修饰同步方法
	public synchronized void add() {
		i++;
	}
	//2. 修饰同步静态方法
	public static synchronized void addStatic() {
		i++;
	}
	//3. 修饰同步代码块,此处锁的方式和 1 加锁方式相同
	public void addCodePieceInstance() {
		synchronized (this) {
			i++;
		}
	}
	//4. 修饰同步代码块,此处锁的方式和 2 加锁方式相同
	public void addCodePiece() {
		synchronized (SynDemo.class) {
			i++;
		}
	}

	//5. 锁定同步代码块
	public void addSyncCode(){
		synchronized (obj) {
				
		}
	}

当一个线程试图访问同步代码块时,首先必须获得锁,那么这个锁的位置到底在哪呢?锁到底包含了那些信息?

要想弄清楚上面两个问题,需要引入一个对象头的概念。

1. java 对象头

首先看下Java 的对象结构,如图 1:

图 1
图 1

Java 对象分为 3 个部分,对象头实例数据对齐空间。我们重点关注的对象头,其他的两项对于我们 synchronized 没有影响。

synchronized 用的锁是存在 Java 对象头里的。如果对象是数组类型,则虚拟机用 3 个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽等于 4 字节,即32bit。

虚拟机位数

头对象结构

说明

32/64bit

Mark Word

存储对象的hashCode、锁信息或分代年龄或GC标志等信息

32/64bit

Class Metadata Address

类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。

32/32bit

ArrayLength

这个标记一般没有,除非锁定的对象是数组,这个表示是数组的长度

其中 Mark Word 在默认情况下存储着对象的 HashCode、分代年龄、锁标记位等以下是32位 JVM 的 Mark Word 默认存储结构

锁状态

25bit

4bit

1bit是否是偏向锁

2bit 锁标志位

无锁状态

对象HashCode

对象分代年龄

0

01

在运行期间,Mark Word 里存储的数据会随着锁标志位的变化而变化。Mark Word 可能变化为存储以下 4 种数据,如图2 :

图2
图2

2. 锁膨胀

上面讲到锁有四种状态,并且会因实际情况进行膨胀升级,其膨胀方向是:

无锁可偏向——>偏向锁——>轻量级锁——>重量级锁

无锁可偏向——>无锁不可偏向——>轻量级锁——>重量级锁

并且膨胀方向不可逆(某些苛刻的条件是可逆的)

2.1 偏向锁

如果想升级偏向锁,需要有两个条件:

  1. 打开偏向锁开关(-XX:BiasedLockingStartupDelay=0 ),1.8 版本是默认 JVM 启动 40s 后开启
  2. 不能运行 hashcode 方法,因为偏向锁需要 java 对象头存储线程的 ID ,如果计算了 hashcode 对象头的空间会被 hashcode 占用。

一句话总结它的作用:减少统一线程获取锁的代价。在大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得,那么此时就是偏向锁。

核心思想:

如果一个线程获得了锁,那么锁就进入偏向模式,此时 Mark Word 的结构也就变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查 Mark Word的锁标记位为偏向锁以及当前线程ID等于 Mark Word 的 ThreadID 即可,这样就省去了大量有关锁申请的操作。

底层通过调用 faster_enter 和 slow_enter

2.2 轻量级锁

轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块。

实现逻辑

轻量级锁的实现是在每一个线程中产生一个 lock record,在竞争锁的时候,采用 CAS 的方式把 Mark Word 的指针指向当前 Lock Record 的地址。

    轻量级锁 01
轻量级锁 01
轻量级锁 02
轻量级锁 02

2.3 重量级锁

重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。

重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景。

实现逻辑:

从上面的分析中,我们可以得知,Synchronized 关键字的锁存在 Java 对象头中,通过判断对象头的标记位来判断当前锁的状态,如果线程拿不到锁会一直等待,这个是怎么实现的呢?

在查询 JVM 规范可以得到 Synchronized 执行对应了两个 JVM 指令,MonitorEnter 和 MonitorExit 两个指令。同步方法是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的

首先我们先了解下 monitor (管程) 这个对象,在 Java 中所有对象都有一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。

在Java虚拟机 (HotSpot) 中,monitor 是由 ObjectMonitor 实现的,其主要数据结构如下(位于 HotSpot 虚拟机源码 ObjectMonitor.hpp 文件:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

ObjectMonitor中有两个队列,_WaitSet_EntryList,用来保存 ObjectWaiter 对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner 指向持有 ObjectMonitor 对象的线程,

  1. 当多个线程同时访问一段同步代码时,首先会进入_EntryList 集合
  2. 当线程获取到对象的 monitor 后进入_Owner区域并把 monitor 中的 owner 变量设置为当前线程,同时monitor 中的计数器 count 加 1
  3. 若线程调用 wait() 方法,将释放当前持有的 monitor,owner 变量恢复为 null,count 自减 1,同时该线程进入 WaitSet 集合中等待被唤醒。
  4. 若当前线程执行完毕也将释放 monitor(锁) 并复位变量的值,以便其他线程进入获取 monitor(锁) 。
img
img

2.4 总结下锁升级的过程

image-20210303231256957
image-20210303231256957

3. 其他特性

3.1 锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。比如下面代码的method1和method2的执行效率是一样的,因为object锁是私有变量,不存在所得竞争关系。

private void syncMethod1() {
	Object obj = new Object();
	//无效的锁,会自动消除
	synchronized (obj) {
		//....
	}
}

3.2 锁粗化

锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁。比如下面method3经过锁粗化优化之后就和method4执行效率一样了。

  private void syncMethod2() {
for (int i = 0; i < 100; i++) {
	synchronized (Object.class) {
		//....
	}
}

//锁粗化会自动改成这种方式,提高性能
synchronized (Object.class) {
	for (int i = 0; i < 100; i++) {
		//....
	}
}
  }
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-03-042,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. java 对象头
  • 2. 锁膨胀
    • 2.1 偏向锁
      • 2.2 轻量级锁
        • 2.3 重量级锁
          • 2.4 总结下锁升级的过程
          • 3. 其他特性
            • 3.1 锁消除
              • 3.2 锁粗化
              相关产品与服务
              对象存储
              对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档