synchronized

296 阅读5分钟

synchronized介绍

sychornized是Java关键字,能够保证在同一时刻最多只有一个线程执行某段代码,以达到保证并发安全的效果。

synchronized使用

作用于实例方法

sychornized作用于实例方法,在进入同步方法前要获得当前实例对象的锁

public synchronized void xxx(){
}

作用于静态方法

sychornized作用于静态方法,在进入同步方法前要获得当前类对象的锁

pubic static synchronized void xxx(){
}

作用于代码块

synchronized作用于代码块,在进入同步代码块前要获得指定对象的锁,可以是实例对象也可以是class对象

public void xxx(){
    synchronized(xxx){
    }
}
public void xxx(){
    synchronized(xxx.class){
    }
}

synchronized实现原理

Java对象内存布局

在JVM中,Java对象保存在堆中,由以下三部分组成:

  1. 对象头(Object Header):包括了关于堆对象的布局、类型、GC状态、同步状态和标识哈希吗的基本信息。Java对象和虚拟机内部对象都有一个共同的对象头格式。
  2. 实例数据(Instance data):存放类的数据信息、父类的信息、对象字段属性等。
  3. 对齐填充(Padding):为了字节对齐,填充的数据,不是必须的。

对象头

HotSpot官方文档中对对象头的描述可以看出,它是Java对象和虚拟机内部对象都有的共同格式,由mark word和klass pointer组成。如果对象是一个数组,那么在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据确定Java对象的大小,但是从数组的元数据无法确认数组的大小。

mark word

用于存储对象自身的运行时数据,如hashCode、GC年龄、锁状态标志、线程持有的锁、偏向线程id、偏向时间戳等等。mark word在32位虚拟机中的长度是32bit,在64位虚拟机中的长度是64bit。打开openjdk的源码包,在/openjdk/hotspot/src/share/vm/oops,mark word对应的C++的markOop.hpp,可以从注释中看到:

1162587-20200918154704187-795332100.png mark word在32位虚拟机中:

1162587-20200918154115022-312986152.png mark word在64位虚拟机中: 1162587-20200918154125385-1537793659.png 虽然在不同位数的虚拟中的长度不一样,但是基本组成内容是一致的。

  1. lock:占两位,锁标志位,为11时表示对象待GC回收状态
  2. biased_lock:占一位,是否偏向锁,由于无所和偏向锁lock标志都是01,所以需要一个是否偏向锁标志
  3. age:记录对象的年龄,达到阈值(15)时,移到老年代
  4. hash:对象的hashCode
  5. JavaThread(偏向锁线程ID):某个线程持有锁对象的时候,这里会设置当前线程的ID,在后续操作中就无需再获进行尝试获取锁的操作
  6. epoch:偏向锁在CAS过程中,偏向性标识,标识对象更偏向哪个锁
  7. ptr_to_lock_record:在轻量级锁定的情况下,JVM通过CAS操作在对象的标题字中设置指向锁记录的指针。
  8. ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针。
klass pointer

指针类型,是对象指向它的类元数据的指针,虚拟机通过这个指针来确定对象是哪个类的实例。

instance data

如果对象有属性字段,则这里会有数据信息。根据字段类型的不同占不同的字节。

padding

默认情况下,虚拟机堆中对象的起始地址需要对齐至8的倍数,如果一个对象用不到8N个字节则需要对其进行填充,以此来补齐对象头和实例数据占用内存之后剩余的空间。

为什么要有padding?

原因之一是让字段只出现在同一CPU的缓存行中,如果字段不是齐的,那么有可能出现跨缓存行的情况。

如何查看对象布局?

以下示例基于64位jdk1.8环境

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.14</version>
</dependency>
public static void main(String[] args) {
    Object o = new Object();
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
    String[] strings = new String[10];
    System.out.println(ClassLayout.parseInstance(strings).toPrintable());
    UserInfo userInfo = new UserInfo();
    System.out.println(ClassLayout.parseInstance(userInfo).toPrintable());
}

76103a1fa9ae156439d501dc7e9d5924.png

Monitor

monitor是一种同步工具,或者是一种同步机制,通常被描述成一个对象,这个对象所有方法都被互斥的执行 在Hotspot中,monitor基于c++实现,由ObjectMonitor实现,主要有以下几个关键属性:
_owner:指向持有对象的线程(持有锁的线程)
_WaitSet:存放处于wait状态的线程
_EntrySet:存放处于block状态的线程
_recursions:锁的重入次数
_count:用来记录该线程获取锁的次数
当多个线程同时访问一段同步代码时,首先会进入_EntrySet队列中,当某个线程获取到对象的monitor后进入到_owner区域并将monitor中的owner设置为当前线程,同时将count加一,即获得对象锁。 若持有monitor的线程调用wait方法,将释放当前持有的monitor,owner重置为null,count自减一,同时进入_WaitSet中等待被唤醒。若当前线程执行完毕也将释放monitor,并复位变量的值,由其他线程再去获取monitor。