synchronized笔记

来源:互联网 发布:缤纷童年吧事件知乎 编辑:程序博客网 时间:2024/06/16 13:46

synchronized

synchronized 可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。在Java SE1.6之前,为了达到这个目的,synchronized作了一个很重量级的实现,为了减少性能的消耗,SE1.6 对其进行了优化,引入了偏向锁和轻量级锁,使它在有些情况下它并不是那么的重。

锁状态

在JavaSE1.6中锁一共有4中状态,无锁状态、偏向锁状态、轻量级锁状态、和重量级锁状态。这几个状态会随着锁的竞争情况逐渐升级,但是锁的状态不可以降级。其中锁升级流程如下
锁升级

实现锁的数据结构

对象头

锁存在Java对象头里。如果对象是数组类型,则虚拟机用3个Word(字宽)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,一字宽等于四字节,即32bit。

长度内容说明32/64bitMark Word存储对象的hashCode或锁信息等。32/64bitClass Metadata Address存储到对象类型数据的指针32/64bitArray length数组的长度(如果当前对象是数组)

Java对象头里的Mark Word里默认存储对象的HashCode,分代年龄和锁标记位。32位JVM的Mark Word的默认存储结构如下:

25 bit4bit1bit是否是偏向锁2bit锁标志位无锁状态对象的hashCode对象分代年龄001长度内容说明32/64bitMark Word存储对象的hashCode或锁信息等。32/64bitClass Metadata Address存储到对象类型数据的指针32/64bitArray length数组的长度(如果当前对象是数组)在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下4种数据:锁状态

25 bit

4bit

1bit2bit23bit2bit是否是偏向锁锁标志位轻量级锁指向栈中锁记录的指针00重量级锁指向互斥量(重量级锁)的指针10GC标记空11偏向锁线程IDEpoch对象分代年龄101

线程锁记录monitor record

线程锁记录列表是线程栈帧中的一块内存空间,每一个线程都有一个可用锁记录列表,当线程持有对象锁时,对象头中的Mark Word指向锁记录的起始地址,而锁记录中会保存持有该锁的线程的唯一标识,表示该锁被这个线程占用。
锁记录组成部分:

Owner:初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;

EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程。

RcThis:表示blocked或waiting在该monitor record上的所有线程的个数。

Nest:用来实现重入锁的计数。重入则递增

HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。

偏向锁

JavaSE1.6之前,当锁不存在竞争时,单线程访问同步代码块也会有很大的开销。然而,大多数情况下,锁不存在竞争,只是由同一个线程多次获得锁,为了减少锁的开销,JavaSE1.6引入偏向锁。
当线程访问同步代码块时,判断Mark Word中是否记录了当前线程唯一标识。

  • 如果记录了当前线程,表示当前线程持有该锁,继续执行;
  • 如果没有记录当前线程,使用CAS替换Mark Word中的唯一标识

    • 如果成功,则该线程取得偏向锁,并在Mark Word中记录当前线程的唯一标识;
    • 如果已经被其他线程持有,则等到全局安全点,暂停当前线程,检测持有锁的线程是否存活
      • 如果不存活,则将Mark Word设置为无锁状态
      • 如果存活,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,膨胀为轻量级锁,最后唤醒暂停的线程

    《并发编程的艺术》中偏向锁流程图如下:

轻量级锁

线程执行到同步块时,当前线程锁记录列表新增一条锁记录,复制Mark Word到锁记录,然后CAS自旋尝试使Mark Word指向栈中锁记录。如果成功则获得锁,锁记录owner指向对象头,失败则膨胀为重量级锁。
解锁时,CAS操作将锁记录中的Mark Word还原到对象头,,如果成功表示没有发生竞争,如果失败,表示当前锁存在竞争,释放锁并唤醒被挂起的线程

详细过程引用其他博客描述

获取锁(monitorenter)的大概过程如下:

(1)当对象处于无锁状态时(RecordWord值为HashCode,状态位为001),线程首先从自己的可用moniter record列表中取得一个空闲的moniter record,初始Nest和Owner值分别被预先设置为1和该线程自己的标识,一旦monitor record准备好然后我们通过CAS原子指令安装该monitor record的起始地址到对象头的LockWord字段来膨胀(原文为inflate,我觉得之所以叫inflate主要是由于当对象被膨胀后扩展了对象的大小;为了空间效率,将monitor record结构从对象头中抽出去,当需要的时候才将该结构attach到对象上,但是和这篇Paper有点互相矛盾,两种实现方式稍微有点不同)该对象,如果存在其他线程竞争锁的情况而调用CAS失败,则只需要简单的回到monitorenter重新开始获取锁的过程即可。

(2)对象已经被膨胀同时Owner中保存的线程标识为获取锁的线程自己,这就是重入(reentrant)锁的情况,只需要简单的将Nest加1即可。不需要任何原子操作,效率非常高。

(3)对象已膨胀但Owner的值为NULL,当一个锁上存在阻塞或等待的线程同时锁的前一个拥有者刚释放锁时会出现这种状态,此时多个线程通过CAS原子指令在多线程竞争状态下试图将Owner设置为自己的标识来获得锁,竞争失败的线程在则会进入到第四种情况(4)的执行路径。

(4)对象处于膨胀状态同时Owner不为NULL(被锁住),在调用操作系统的重量级的互斥锁之前先自旋一定的次数,当达到一定的次数时如果仍然没有成功获得锁,则开始准备进入阻塞状态,首先将rfThis的值原子性的加1,由于在加1的过程中可能会被其他线程破坏Object和monitor record之间的关联,所以在原子性加1后需要再进行一次比较以确保LockWord的值没有被改变,当发现被改变后则要重新进行monitorenter过程。同时再一次观察Owner是否为NULL,如果是则调用CAS参与竞争锁,锁竞争失败则进入到阻塞状态。

释放锁(monitorexit)的大概过程如下:

(1)首先检查该对象是否处于膨胀状态并且该线程是这个锁的拥有者,如果发现不对则抛出异常;

(2)检查Nest字段是否大于1,如果大于1则简单的将Nest减1并继续拥有锁,如果等于1,则进入到第(3)步;

(3)检查rfThis是否大于0,设置Owner为NULL然后唤醒一个正在阻塞或等待的线程再一次试图获取锁,如果等于0则进入到第(4)步

(4)缩小(deflate)一个对象,通过将对象的LockWord置换回原来的HashCode值来解除和monitor record之间的关联来释放锁,同时将monitor record放回到线程是有的可用monitor record列表。

《并发编程的艺术》中轻量级锁流程图如下

参考文献:
http://ifeve.com/java-synchronized/
http://www.cnblogs.com/javaminer/p/3889023.html

0 0