Java--synchronize关键字
Java–synchronize关键字
synchronize
是JDK
中的一个关键字。是用来实现线程安全的。接下来,我们好好的研究下synchronize
的魅力。
使用场景
- 修饰实例方法 — 这个锁是作用在当前实例对象上的。当我们要进入到同步代码之前,我们需要当前实例对象获得锁。在执行完代码后,要释放锁。
- 修饰静态方法 — 这个锁是作用在类对象上的。当我们要执行同步代码的时候,要获取到类对象的锁
- 修饰代码块 — 指定了锁的对象,默认情况下,我们会使用
this
作为加锁条件。当我们使用this
的时候,就相当于是第一种情况的当前实例对象。
注意,这三种情况的获取锁、释放锁都是JDK
自己完成的。不需要我们手动的完成。
synchronize关键字原理
既然我们说了这么多,那么在JDK
的底层中,对于synchronize
关键字是怎么处理的呢?在这里,我们在解释这个问题,要先引申出另一个问题 — 什么是Monitor
?
Monitor解释
对于Monitor
,我们可以把它理解成一个同步工具,也可以理解为一种同步机制,但是更多的,我们将它描述为一个对象。对于Monitor
,它是线程私有的数据结构,而这个Monitor
在什么地方呢?其实,Monitor
在Java的对象头
中。
那么Monitor
到底是什么呢?其实Monitor
只是一种数组结构,在Monitor
中,包含了以下的信息:
- _owner:指向持有ObjectMonitor对象的线程
- _WaitSet:存放处于wait状态的线程队列
- _EntryList:存放处于等待锁block状态的线程队列
- _recursions:锁的重入次数
- _count:用来记录该线程获取锁的次数
当多个线程同时访问一段同步代码时,首先会进入_EntryList
队列中,当某个线程获取到对象的monitor
后进入_Owner
区域并把monitor
中的_owner
变量设置为当前线程,同时monitor
中的计数器_count
加1。即获得对象锁。
若持有monitor
的线程调用wait()
方法,将释放当前持有的monitor
,_owner
变量恢复为null
,_count
自减1,同时该线程进入_WaitSet
集合中等待被唤醒。若当前线程执行完毕也将释放monitor
(锁)并复位变量的值,以便其他线程进入获取monitor
(锁)。
而对于synchronize
而言,我们获取锁的过程,其实就是对象的一个从Monitor
的进入
到走出
的过程。当对象获得锁的时候,此时使用的是monitorenter
指令,而对象释放锁的时候,此时使用的是monitorexit
指令。对于JDK1.5
以及之前的synchronize
,采用的就是这样的方式,但是这样的方式需要与操作系统进行打交道,因此,我们经常称呼为重量级
锁。而线程之所以能够知道当前线程需不需要锁,也是通过方法上修饰的ACC_SYNCHRONIZED
来进行判断的。
Java的对象头
那么什么是Java
的对象头呢?其实,在JVM中,我们的每个对象,都是由对象头
和实例数据
两部分组成的。对象头保存了一个对象的元数据信息,而其他的数据,则是存在了实例对象
中,那么对象头,都是由什么组成的呢?其实,对象头是由markword
、类型指针
和数组长度
(可选,只有对象为数组的时候,才存在这个值)组成的。
markword
接下来,我们解释一下各个字段的含义
biased_lock
在JDK1.6
的版本之后,JDK
的库工程师们对于synchronize
关键字进行了优化,从之前的重量级锁
,改成了可以是无锁
、偏向锁
、轻量级锁
和之前就有的重量级锁
。而这个字段表示的就是当前的锁状态是否是偏向锁,如果是1
的话,则表示的是当前对象启用了偏向锁
,而0
表示的是当前对象并未启用偏向锁
。在markword
中,这个字段仅仅占用1
位。
lock
这个字段表示的是锁的一个状态,由2
位组成。我们可以将这个状态以一个表格的形式展示出来
biased_lock | lock | 含义 |
---|---|---|
0 | 01 | 无锁 |
1 | 01 | 偏向锁 |
0 | 00 | 轻量级锁 |
0 | 10 | 重量级锁 |
0 | 11 | GC标记 |
age
这个字段表示的是Java
对象的一个年龄。由4
位组成。在GC
中,如果对象在Survivor
中存活一次,则直接age加1
,当超过了设置的年龄阈值的时候,此时对象会晋升到老年代。默认情况下,并行GC
的年龄阈值为15
,并发GC
的年龄阈值为6
。由于age
只有4
位,所以最大值为15
,这就是-XX:MaxTenuringThreshold
选项最大值为15
的原因。
identity_hashcode:
25
位的对象标识Hash
码,采用延迟加载技术。调用方法System.identityHashCode()
计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor
中。
thread
持有偏向锁的线程ID
epoch
偏向时间戳
ptr_to_lock_record
指向栈中锁记录的指针
ptr_to_heavyweight_monitor
指向管程Monitor的指针
类型指针
这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM
的一个字大小,即32
位的JVM
为32
位,64
位的JVM
为64
位。
如果应用的对象过多,使用64
位的指针将浪费大量内存。为了节约内存可以使用选项+UseCompressedOops
开启指针压缩,其中,oop
即ordinary object pointer
普通对象指针。开启该选项后,下列指针将压缩至32
位:
- 每个Class的属性指针(即静态变量)
- 每个对象的属性指针(即对象变量)
- 普通对象数组的每个元素指针
当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM
不会优化,比如指向PermGen
的Class
对象指针(JDK8
中指向元空间的Class
对象指针)、本地变量
、堆栈元素
、入参
、返回值
和NULL指针
等。
数组长度
如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM
架构的不同而不同:32
位的JVM
上,长度为32
位;64
位JVM
则为64
位。64
位JVM
如果开启+UseCompressedOops
选项,该区域长度也将由64
位压缩至32
位。
synchronize优化
jdk1.6
以后对synchronized
的锁进行了优化,引入了偏向锁
、轻量级锁
,锁的级别从低到高逐步升级:无锁->偏向锁->轻量级锁->重量级锁
。
synchronize锁升级过程
下面,我们可以借用网上的一张图片来进行说明
总结
对于同步方法,synchronize
可谓是元老级的任务。对于synchronize
的研究,在日后的学习中,还是很有必要的。对我们的帮助也是很大的。
转载请注明来源,欢迎指出任何有错误或不够清晰的表达。可以邮件至 gouqiangshen@126.com
文章标题:Java--synchronize关键字
文章字数:1.8k
本文作者:BiggerShen
发布时间:2019-11-05, 20:49:00
最后更新:2024-01-16, 03:51:15
原始链接:https://shengouqiang.cn/JavaLock/JavaLockDay02/版权声明: 转载请保留原文链接及作者。