java并发编程 —— synchronized与对象头

1  synchronized关键字

使用synchronized关键字有以下三种使用方式:

  1. 同步代码块
  2. 同步方法
  3. 静态同步方法

通过编译的class文件可以看到synchronized代码块使用了monitorenter和monitorexit两个指令分别获取锁标记和释放锁标记,而synchronized方法使用了ACC_SYNCHRONIZED来完成锁的获取与释放的。也就是锁的获取与释放synchronized关键字自动帮我们完成了。

对于使用synchronized关键字实现的同步机制由如下几点补充说明:

如果同一个方法有多个线程访问,那么每个线程都有自己的线程拷贝(拷贝存储在工作内存中)

类锁主要用于控制对static成员变量的并发访问


synchronized块(可以是同步方法或者同步代码块)是可重入的,每次重入会把锁的计数器加1,每次退出将计数器减1,当计数器的值为0的时候,锁便被释放了
Java SE 1.6 为了减少获得锁和释放锁的性能消耗引入了偏向锁和轻量级锁

1.偏向锁

之所以引入偏向锁,是为了让线程获得锁的代价更低。当一个线程访问同步块并获取锁的时候,会在对象的对象头(对象头包括两部分的信息:一部分是”Mark Word“,主要存放的是哈希码、对象的分代年龄、锁的标记等信息;另一部分是对象的类型指针)和栈帧中的锁记录中存储锁偏向的ID,以后该线程在进入方法的同步块的时候,就检查这个ID(可以理解为一种标记,是一种身份的标识),如果测试成功,表明对象已经获得了锁;如果测试失败,继续测试偏向锁的标识是否设置为1(1的话就是偏向锁),如果没有则使用CAS(Compare And Swap)锁。

2.轻量级锁

分为加锁和解锁。当线程执行到同步块之前,JVM会首先检查当前线程的栈帧中创建用于存储记录锁记录的空间,并将对象头中Mark Word复制到锁记录中,也称为Displaced Mark Word,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,则线程获得锁,否则当前线程尝试使用自旋来获取锁。这就是加锁的过程。

这里多次提到CAS,那么CAS是个什么鬼?CAS是Compare and swap(比较和替换)的简写,具体而言就是:当进行CAS操作的时候,需要输入两个数值,一个是旧值,该旧值是原来的值,另一个是新值,也就是发生改变的值,得到这两个值后,在CAS操作期间会去比较旧值是否发生变化,如果没有发生变化就用新值进行替换,如果发生了变化就不进行替换。

那么解锁的过程又是怎样的呢?就是使用CAS操作将Displaced Mark Word替换回对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀,膨胀的结果是导致锁的升级,并进入阻塞状态。直到需要释放锁的线程释放锁并唤醒其他等待的线程。

 

notify/notifyAll和wait方法依赖于monitor对象,在前面的分析中,我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字可以获取 monitor ,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因


wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行,而sleep方法只让线程休眠并不释放锁。同时notify/notifyAll方法调用后,并不会马上释放监视器锁,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁

 

2 Java对象保存在内存中时,由以下三部分组成:

1,对象头

2,实例数据

3,对齐填充字节

一,对象头


java的对象头由以下三部分组成:

1,Mark Word

2,指向类的指针

3,数组长度(只有数组对象才有)

 

1,Mark Word


Mark Word记录了对象和锁有关的信息,当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。

Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。

Mark Word在不同的锁状态下存储的内容不同,在32位JVM中是这么存的:

java并发编程 —— synchronized与对象头_第1张图片

其中无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态。

JDK1.6以后的版本在处理同步锁时存在锁升级的概念,JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁。

 

JVM一般是这样使用锁和Mark Word的:

1,当没有被当成锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。

2,当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。

3,当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。

4,当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。

5,偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。

6,轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7。

7,自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。

 

轻量级锁加锁:线程在执行同步块之前, JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,
官方称为Displaced Mark Word。然后线程尝试使用 CAS 将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,

表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,锁标志

的状态值变为”10”,Mark Word中存储的就是指向重量级(互斥量)的指针。

轻量级锁解锁:轻量级解锁时,会使用原子的 CAS 操作来将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,

表示当前锁存在竞争,锁就会膨胀成重量级锁。
 

代码进入同步块时,如果此同步对象没有被锁定(markword锁标志为01),jvm首先在当前线程的栈帧中建立一个名为锁记(Lock Record)的空间,用于存储锁对象目前的MarkWord拷贝(8byte存当前markword值拷贝,8byte存当前对象地址)。

=》如果此同步对象已被锁定(markword锁标志为00/10),则进入锁等待

然后尝试使用CAS将当前锁对象的MarkWord更新为指向Lock Record的指针。如果更新成功,则这个线程就获取了该对象的锁,并且该对象MarkWord的锁标识位(最后2bit)将转变为00,即表示该对象当前处于轻量级锁状态。

若更新操作失败,jvm会先检查该对象MarkWord是否已经是指向当前线程的栈帧,若是则说明已经获取过锁了,就直接进入同步块执行。否则说明当前锁对象已经被其他线程抢占了。

如果有多个线程同时争用同一个锁,此时轻量级锁要膨胀为重量级锁,锁标志位状态值变为10,Mark Word中存储的是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。(膨胀为重量级锁后,会生成一个新的锁Lock Record记录。锁对象markword指向新生成的重量级锁Lock Record记录,并把锁标志位置为10。)

轻量级锁的解锁也是通过CAS来做的,将Lock Record存储的之前markword值,CAS更新回锁对象的MarkWord中,更新成功则整个同步块完成;更新失败,则说明有其他线程尝试过获取该锁,那就要在释放锁的同时唤醒被挂起的线程。

=》多线程争用锁时,锁对象markword会变更为指向 新的重量级锁Lock Record地址;=》锁对象markword指向的重量级锁Lock Record地址 和 当前线程栈顶Lock Record地址不一致,说明锁膨胀为重量级锁了,存在多线程竞争。则当前线程释放锁,同时唤醒被挂起的线程。 

 

2,指向类的指针


该指针在32位JVM中的长度是32bit,在64位JVM中长度是64bit。

Java对象的类数据保存在方法区。

 

3,数组长度
只有数组对象保存了这部分数据。

该数据在32位和64位JVM中长度都是32bit。

 

二,实例数据


对象的实例数据就是在java代码中能看到的属性和他们的值。

 

三,对齐填充字节


因为JVM要求java的对象占的内存大小应该是8bit的倍数,所以后面有几个字节用于把对象的大小补齐至8bit的倍数,没有特别的功能。
 


 

你可能感兴趣的:(锁)