1 synchronized是什么

synchronized是多线程锁机制的一种,能够实现多线程的同步,解决多线程的同步问题。它是Java的一个关键字,可以用来修饰方法或者代码块,可以将代码块或方法锁起来。
它是一种互斥锁:一次只能允许一个线程进入被锁住的代码块。
它是一种内置锁/监视器锁:Java中每个对象都有一个内置锁(监视器,也可以理解为锁标记)。而synchronized就是使用对象的内置锁(监视器)来将代码块/方法锁定的。它锁住的是对象,但我们同步到是方法/代码块。
synchronized能够保证线程的可见性、原子性及有序性。
synchronized实现同步的基础:Java中每一个对象都可以作为锁,具体有以下三种形式

  • synchronized修饰普通同步方法(成员方法)时,锁的是当前实例对象;
  • synchronized修饰静态同步方法(静态方法)时,锁的是当前类的Class对象;
  • synchronized修饰代码块时,锁的是synchronized括号里配置的对象,也就是我们指定加锁对象,但是尽量不要使用String类型对象作为加锁对象,因为字符串常量池具有缓存功能。

在JDK1.5之前synchronized是一个重量级锁,相对于Lock接口下的锁,显得很笨重,随着JDK1.6对它进行优化后,synchronized并不会显得那么重量级了。

2 synchronized释放锁的时机

  • 当方法(代码块)执行完毕后会自动释放锁,不需要做任何操作;
  • 当一个线程执行的代码出现异常时,其持有的所会自动释放;

不会由于异常导致出现死锁现象的。

3 synchronized的原理(底层实现)

要谈synchronized的底层实现,就不得不谈数据在JVM内存的存储时的两个东西:Java对象头以及Monitor对象监视器。
在JVM中(在其堆内存中),一个Java对象到底包含些什么呢?
概括起来可以分为对象头、对象体和对齐字节;其中对象头又包含Mark Word(标记字)、Class Pointer(类对象指针)和Array Length(数组长度)三部分。

image.png3.1 对象头、对象体、对齐字节

在JVM中,对象在内存中的布局分分为三块区域:对象头、实例数据(对象体)和对齐填充(对齐字节):
image.png

  • 对象体(实例数据):存放类的属性数据信息,包括父类的属性信息;这部分数据按4字节对齐(按四字节对齐的意思是:比如现在的数据占用了一个字节,那么就要分给它四个字节;如果数据占用了五个字节,那就要分给它八个字节;也即所分的内存只能是四的整数倍)
  • 对象头:对象头分为三个部分:
    • Mark Word(标记字):用来存储自身运行时的数据,比如:对象的分代年龄、hashCode、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等信息。 Mark W在32位JVM中长度是32bit,在64位JVM中长度shi64bit,在不同的锁状态下,存储的内容不同。
    • Class Pointer(类对象指针):用来存储方法区中字节码对象的地址,JVM通过这个指针来确定这个对象时属于哪个类的实例;
    • Array Length(数组长度):如果对象是数组,则该字段记录数组的长度;如果不是数组,则该字段不存在。这是一个可选字段。
  • 对齐字节(对齐填充):对齐字节的作用是用来保证Java对象实例所占内存字节数为8的倍数。HotSpot虚拟机的内存管理,要求对象的起始地址必须是8字节的整数倍。对象头本身是8的整数倍,当对象的实例变量数据不是8的倍数时,便需要填充数据来保证8字节的对齐。

通过上述描述,我们可以了解到Java对象在内存中的结构,其中,Java内置锁的很多信息都存放在对象的Mark Word字段中。
synchronized是一个内置锁,它用锁就是存放在Java对象头的Mark Wrod部分中的。Mark Word时实现轻量级所锁和偏向锁的关键。
对象头中的信息是与对象自身定义的数据无关的额外存储成本,Mark Word被设计成一个非固定的数据结构,以便在极小的空间内存中存储尽量多的数据,它会根据对象的状态复用自己的存储空间。即:Mark Word会随着程序的运行发生变化。
变化举例:
在32位JVM下,无锁状态下Mark Word的存储结构:
image.png
它可能变化为存储以下四中数据:
image.png
在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:
image.png
可以看到,Mark Word的最后两位存储了锁的标志位。01代表初始状态,未加锁,Mark Word里存储的是对象本身的哈希码,随着锁级别的不同,对象头里会存储不同的内容。偏向锁存储的是当前占用此对象的线程的id;而轻量级锁则存储指向线程栈中锁记录的指针;重量级锁存储的是指向重量级锁的指针。
其中Mark Word的标志位及存储内容以及锁状态如下:
image.png
在线程进入同步代码块的时候,如果此同步对象没有被锁定,即它的锁标志位是01,则虚拟机首先在当前线程的栈中创建Lock Record(锁记录)空间用于存储对象的Mark Word的拷贝,官方把这个拷贝称为Displaced Mark Word。
Lock Record是线程私有的数据结构,每个线程都有一个可用Lock Record列表,同时还有一个全局的可用列表。每一个被锁住的对象Mark Word都会和一个Lock Record关联(关联方式是对象头的Mark Word中的Lock Word指向Lock Record的起始地址)。同时Lock Record中有一个Owner字段存放拥有该锁的线程的唯一标识(或者object mark word),表示该锁被这个线程占用。
Lock Record的内部结构:
image.png

3.2 监视器Monitor

Monitor是什么:可以把它理解为一个同步工具或者一种同步机制,它通常被描述为一个对象。
任何一个对象都有一个Monitor与之关联,且当一个Monitor被持有后,它将处于锁定状态。
synchronized在JVM里的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但都可以通过成对的MonitorEnter和MonitorExit指令来实现。

  • MonitorEnter:插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即:尝试获取该对象的锁;
  • MonitorExit:插入在方法结束和异常处,JVM保证没有MonitorEnter必须有对应的MonitorExit。

在Java,一切接对象,所有的Java对象都是天生的Monitor,每一个Java对象都又称为Monitor的潜质。因为在Java的设计中,每个Java对象自创建出来就带了一把看不见的锁,它叫做内部所或者Monitor锁,也就是通常说的synchronized的对象锁。synchronized的对象所锁,Mark Word锁标志位为10,其中指针指向的是Monitor对象的起始地址。
在java虚拟机(HotSpot)中,Monitor是有ObjectMonitor实现的,数据结构如下(由C++实现):

  1. ObjectMonitor() {
  2. _header = NULL;
  3. _count = 0; // 记录个数
  4. _waiters = 0,
  5. _recursions = 0;
  6. _object = NULL;
  7. _owner = NULL;
  8. _WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
  9. _WaitSetLock = 0 ;
  10. _Responsible = NULL ;
  11. _succ = NULL ;
  12. _cxq = NULL ;
  13. FreeNext = NULL ;
  14. _EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
  15. _SpinFreq = 0 ;
  16. _SpinClock = 0 ;
  17. OwnerIsThread = 0 ;
  18. }

ObjectMonitor中有两个队列,_WaitSet和_EntryList,用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:

  • 首先会进入_EntryList集合,当线程获取到对象的monitor后,进入_owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器conut+1;
  • 若线程调用wait()方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入waitset集合中等待被唤醒(notify()/notifyAll())
  • 若当前线程执行完毕,也将释放monitor(锁),并复位count的值,以便其他线程进入获取monitor(锁)。

同时,monitor对象存于每个java对象的对象头的Mark Word区域中(存储的是指针,指针指向monitor对象),synchronized锁便是通过这种方式获取锁的,也是为什么java中任意对象都可以作为锁的原因。同时,notify()/notifyAll()/wait()等方法会使用到monitor锁对象,所以必须在同步代码块中使用。
monitor监视器有两种同步方式:

  • 互斥同步:多线程环境下,线程之间如果需要共享数据,需要解决互斥访问数据的问题,监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。
  • 协作同步:一个线程向缓冲区写数据,另一个线程从缓冲区读数据,如果多线程发现缓冲区为空就会等待,当写线程向缓冲区写入数据,就会唤醒多线程,这里多线程和写线程就是一个合作关系。

monitor与Java对象的关系:
image.png

3.3 synchronized原理总结

image.png
对于synchronized修饰同步代码块的情况:

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("synchronized 代码块");
        }
    }
}

synchronized同步代码块的实现,使用的是monitorenter和monitorexit指令来实现。其中monitorenter指令指向同步代码块开始的位置,monitorexit指令指名同步代码块的结束位置。

  • 在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为0则表示锁可以被获取,获取后将锁的计数器设为1,也就是加1;
  • 对象锁的拥有者线程才可以执行monitorexit指令来释放锁;
  • 在执行了monitorexit指令后,将锁的计数器设为0,表示锁被释放,其他线程可以尝试获取锁;
  • 如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

synchornized修饰方法的情况:
synchronized修饰的方法并没有monitoreter指令和monitorexit指令,取而代之的是ACC_SYNCHRONIZED标识,该标识指明了该方法是个同步方法。JVM通过该ACC_SYNCHRONIZED访问表示来辨别一个方法是否是同步方法,从而执行相应的同步调用。

  • 先从同步方法的常量池中检查是否ACC_SYNCHRONIZED标识,如果有,就先获取锁(也就是monitor监视器锁);
  • 如果是实例方法,JVM会尝试获取实例对象的锁;如果是静态方法,JVM会尝试获取当前Class的锁;
  • 获取道监视器锁(monitor)后,开始执行方法;
  • 方法执行之后再释放监视器锁;
  • 这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住;
  • 如果方法执行过程中发生了异常,并且方法内部没有处理该异常,那么在异常被抛到方法外面之前,监视器锁会被自动释放。

总结:
synchronized同步代码块的实现,使用的是monitorenter和monitorexit指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置;
synchronized修饰的方法并没有monitorenter和monitorexit指令,而是用ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法;
二者本质上,都是对对象监视器monitor的获取。monitorenter、monitorexit和ACC_SYNCHRONIZED都是基于monitor监视器实现的,在JVM(HotSpot中),monitor是基于C++实现的,由于ObjectMonitor实现,ObjectMonitor类提供了几个方法,如enter、exit、wait、notify、notifyAll等。synchronized加锁的时候,会调用ObjectMonitor的enter方法,解锁的时候会调用exit方法。

4 synchronized锁的升级顺序

锁解决了数据的安全问题,但是同样带来了性能的下降。
HotSpot虚拟机的作者发现,大部分情况下,加锁的代码不仅仅不存在多线程竞争,而且总是是由同一个线程多次获得。基于这种情况,synchronized在JDK1.6之后做了一些优化。
JDK1.6之后,synchronized为了减少获得锁和释放锁带来的性能开销,引入了偏向锁、轻量级锁、自旋锁、重量级锁等锁,锁的状态根据竞争激烈的程度从低到高不断升级。
锁存在四种状态,依次是无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。这些状态会随着竞争的激烈而主键升级。但是要注意,锁可以升级不可以降级,这种策略是为了提高获得锁和释放锁的效率。
这里还涉及更深入的东西,后续再看。