Java--synchronize关键字

Java–synchronize关键字

synchronizeJDK中的一个关键字。是用来实现线程安全的。接下来,我们好好的研究下synchronize的魅力。

使用场景

  1. 修饰实例方法这个锁是作用在当前实例对象上的。当我们要进入到同步代码之前,我们需要当前实例对象获得锁。在执行完代码后,要释放锁。
  2. 修饰静态方法这个锁是作用在类对象上的。当我们要执行同步代码的时候,要获取到类对象的锁
  3. 修饰代码块指定了锁的对象,默认情况下,我们会使用this作为加锁条件。当我们使用this的时候,就相当于是第一种情况的当前实例对象。

注意,这三种情况的获取锁、释放锁都是JDK自己完成的。不需要我们手动的完成。

synchronize关键字原理

既然我们说了这么多,那么在JDK的底层中,对于synchronize关键字是怎么处理的呢?在这里,我们在解释这个问题,要先引申出另一个问题什么是Monitor

Monitor解释

对于Monitor,我们可以把它理解成一个同步工具,也可以理解为一种同步机制,但是更多的,我们将它描述为一个对象。对于Monitor,它是线程私有的数据结构,而这个Monitor在什么地方呢?其实,Monitor在Java的对象头中。

那么Monitor到底是什么呢?其实Monitor只是一种数组结构,在Monitor中,包含了以下的信息:

  1. _owner:指向持有ObjectMonitor对象的线程
  2. _WaitSet:存放处于wait状态的线程队列
  3. _EntryList:存放处于等待锁block状态的线程队列
  4. _recursions:锁的重入次数
  5. _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

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位的JVM32位,64位的JVM64位。

如果应用的对象过多,使用64位的指针将浪费大量内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩,其中,oopordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:

  1. 每个Class的属性指针(即静态变量)
  2. 每个对象的属性指针(即对象变量)
  3. 普通对象数组的每个元素指针

当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGenClass对象指针(JDK8中指向元空间的Class对象指针)、本地变量堆栈元素入参返回值NULL指针等。

数组长度

如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64JVM则为64位。64JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。

synchronize优化

jdk1.6以后对synchronized的锁进行了优化,引入了偏向锁轻量级锁,锁的级别从低到高逐步升级:无锁->偏向锁->轻量级锁->重量级锁

synchronize锁升级过程

下面,我们可以借用网上的一张图片来进行说明

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/

版权声明: 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏