java创建对象

new Object()创建对象时在内存中的分布–正对于64位

1. 对象头

1) mark word:标记字段 32位长度为32bit,64位系统上长度为64bit

   hashcode、GC分代年龄、锁信息(锁标志、锁记录)等  

在32位系统上mark word长度为32bit,64位系统上长度为64bit。为了能在有限的空间里存储下更多的数据,其存储格式是不固定的,在32位系统上各状态的格式如下:

avatar

可以看到锁信息也是存在于对象的mark word中的。
> * 当对象状态为偏向锁(biasable)时,mark word存储的是偏向的线程ID;
> * 当状态为轻量级锁(lightweight locked)时,mark word存储的是指向线程栈中Lock Record的指针;
> * 当状态为重量级锁(inflated)时,为指向堆中的monitor对象的指针。


2) klass word :类型指针

3) array size :数组对象:数组长度

2. 实例数据

所有属性加起来的大小

3.对齐填充

实例数据不一定有,取决于实例数据,针对于JVM来说实例数据是8的整数倍,当实例数据不满足这一条件时,对其填充会将其补足为8的整数倍。

如对象属性加起来为63字节,那么对其填充就是1字节,则实例数据就是64字节。

重量级锁

重量级锁是我们常说的传统意义上的锁,其利用操作系统底层的同步机制去实现Java中的线程同步。

重量级锁的状态下,对象的mark word为指向一个堆中monitor对象的指针。

一个monitor对象包括这么几个关键字段:cxq(下图中的ContentionList),EntryList ,WaitSet,owner。

其中cxq ,EntryList ,WaitSet都是由ObjectWaiter的链表结构,owner指向持有锁的线程。

avatar

当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成一个ObjectWaiter对象插入到 cxq 的队列尾部,然后暂停当前线程。当持有锁的线程释放锁前,会将cxq中的所有元素移动到EntryList中去,并唤醒EntryList的队首线程。

如果一个线程在同步块中调用了Object#wait方法,会将该线程对应的ObjectWaiterEntryList移除并加入到WaitSet中,然后释放锁。当wait的线程被notify之后,会将对应的ObjectWaiterWaitSet移动到EntryList中。

以上只是对重量级锁流程的一个简述,其中涉及到的很多细节,比如ObjectMonitor对象从哪来?释放锁时是将cxq中的元素移动到EntryList的尾部还是头部?notfiy时,是将ObjectWaiter移动到EntryList的尾部还是头部?

关于具体的细节,会在重量级锁的文章中分析。

轻量级锁

JVM的开发者发现在很多情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此JVM引入了轻量级锁的概念。

线程在执行同步块之前,JVM会先在当前的线程的栈帧中创建一个Lock Record,其包括一个用于存储对象头中的 mark word(官方称之为Displaced Mark Word)以及一个指向对象的指针。下图右边的部分就是一个Lock Record

1. 加锁过程

  1. 在线程栈中创建一个Lock Record ,将其 obj (即上图的Object reference)字段指向锁对象。
  2. 直接通过CAS指令将Lock Record 的地址存储在对象头的make word中 ,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。如果失败,进入到步骤3。
  3. 如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分(Displaced Mark Word)未null,起到了一个重入计数器的作用。
  4. 执行到这一步说明发生了竞争,需要膨胀为重量级锁。

2. 解锁过程

  1. 遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record
  2. 如果Lock RecordDisplaced Mark Word为null,代表这是一次重入,将obj设置为null后continue。
  3. 如果Lock RecordDisplaced Mark Word不为null,则利用 CAS 指令将对象头的 mark word恢复成为Displaced Mark Word。如果成功,则continue,否则膨胀为重量级锁。


偏向锁

1. 对象创建

当JVM启用了偏向锁模式(1.6以上默认开启),当新创建一个对象的时候,如果该对象所属的class没有关闭偏向锁模式(什么时候会关闭一个class的偏向模式下文会说,默认所有class的偏向模式都是是开启的),那新创建对象的mark word将是可偏向状态,此时mark word中的thread id(参见上文偏向状态下的mark word格式)为0,表示未偏向任何线程,也叫做匿名偏向(anonymously biased)。

2. 加锁过程

case 1:当该对象第一次被线程获得锁的时候,发现是匿名偏向状态,则会用CAS指令,将mark word中的thread id由0改成当前线程Id。如果成功,则代表获得了偏向锁,继续执行同步块中的代码。否则,将偏向锁撤销,升级为轻量级锁。

case 2:当被偏向的线程再次进入同步块时,发现锁对象偏向的就是当前线程,在通过一些额外的检查后(细节见后面的文章),会往当前线程的栈中添加一条Displaced Mark Word为空的Lock Record中,然后继续执行同步块的代码,因为操纵的是线程私有的栈,因此不需要用到CAS指令;由此可见偏向锁模式下,当被偏向的线程再次尝试获得锁时,仅仅进行几个简单的操作就可以了,在这种情况下,synchronized关键字带来的性能开销基本可以忽略。

case 3.当其他线程进入同步块时,发现已经有偏向的线程了,则会进入到撤销偏向锁的逻辑里,一般来说,会在safepoint中去查看偏向的线程是否还存活,如果存活且还在同步块中则将锁升级为轻量级锁,原偏向的线程继续拥有锁,当前线程则走入到锁升级的逻辑里;如果偏向的线程已经不存活或者不在同步块中,则将对象头的mark word改为无锁状态(unlocked),之后再升级为轻量级锁。

由此可见,偏向锁升级的时机为:当锁已经发生偏向后,只要有另一个线程尝试获得偏向锁,则该偏向锁就会升级成轻量级锁。当然这个说法不绝对,因为还有批量重偏向这一机制。

3. 解锁过程

当有其他线程尝试获得锁时,是根据遍历偏向线程的lock record来确定该线程是否还在执行同步块中的代码。因此偏向锁的解锁很简单,仅仅将栈中的最近一条lock record的obj字段设置为null。需要注意的是,偏向锁的解锁步骤中并不会修改对象头中的thread id。

总结

Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。当条件不满足时,锁会按偏向锁->轻量级锁->重量级锁 的顺序升级。JVM种的锁也是能降级的,只不过条件很苛刻,不在我们讨论范围之内。该篇文章主要是对Java的synchronized做个基本介绍,后文会有更详细的分析。

Synchronized和ReentrantLock的区别

原理弄清楚了,顺便总结了几点Synchronized和ReentrantLock的区别:

  1. Synchronized是JVM层次的锁实现,ReentrantLock是JDK层次的锁实现;
  2. Synchronized的锁状态是无法在代码中直接判断的,但是ReentrantLock可以通过ReentrantLock#isLocked判断;
  3. Synchronized是非公平锁,ReentrantLock是可以是公平也可以是非公平的;
  4. Synchronized是不可以被中断的,而ReentrantLock#lockInterruptibly方法是可以被中断的;
  5. 在发生异常时Synchronized会自动释放锁(由javac编译时自动实现),而ReentrantLock需要开发者在finally块中显示释放锁;
  6. ReentrantLock获取锁的形式有多种:如立即返回是否成功的tryLock(),以及等待指定时长的获取,更加灵活;
  7. Synchronized在特定的情况下对于已经在等待的线程是后来的线程先获得锁(上文有说),而ReentrantLock对于已经在等待的线程一定是先来的线程先获得锁;