synchronized详解

主要是看b站上这位老师的视频的学习笔记 https://www.bilibili.com/video/BV16J411h7Rd?p=85

以及下面两篇源码的文章:
https://mp.weixin.qq.com/s?__biz=MzUxNDA1NDI3OA==&mid=2247484986&idx=3&sn=565359c429b80736ffc16c46fb0d8591&chksm=f94a87d3ce3d0e
https://github.com/farmerjohngit/myblog/issues/15

synchronized

1、共享问题

Java并发出现的问题

两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

public class Test{

	static int counter = 0;
	public static void main(String[] args) throws InterruptedException {
		Thread t1 = new Thread(() -> {
			for (int i = 0; i < 5000; i++) {
				counter++;
			}
		}, "t1");
		Thread t2 = new Thread(() -> {
			for (int i = 0; i < 5000; i++) {
				counter--;
			}
		}, "t2");
		t1.start();
		t2.start();
		t1.join();
		t2.join();
//		log.debug("{}",counter);
		System.out.println(counter);
	}

}

在这里插入图片描述
问题分析

以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作。

从字节码来进行分析,
例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i

而对应 i-- 也是类似:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i

而 Java 的内存模型如下,完成静态变量(“共享变量”)的自增,自减需要在主存和工作内存中进行数据交换:
在这里插入图片描述
如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题。

但多线程下这 8 行代码可能交错运行:
在这里插入图片描述
临界区 Critical Section

  • 一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源
    • 多个线程读共享资源其实也没有问题
    • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

例如,下面代码中的临界区

static int counter = 0;
static void increment()
// 临界区
{
 counter++;
}
static void decrement()
// 临界区
{
 counter--;
}

竞态条件 Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

2、 synchronized的使用

1.修饰代码块

指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。synchronized(类.class) 表示进入同步代码前要获得当前 类的class对象的锁

synchronized(this) {
  //业务代码
}

2.修饰实例方法:

作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁,相当于锁住this

synchronized void method() {
  //业务代码
}

3.修饰静态方法:

也就是给当前类的class对象加锁,会作用于类的所有对象实例,进入同步代码前要获得当前class对象的锁。

因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份)。所以,如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现。

synchronized void staic method() {
  //业务代码
}

总结

  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的Class对象。
  • 对于同步方法块,锁是Synchonized括号里配置的对象。

上面并发出现问题的代码,就可以用synchronized解决

	static int counter = 0;
	static final Object room = new Object();
	public static void main(String[] args) throws InterruptedException {
		Thread t1 = new Thread(() -> {
			for (int i = 0; i < 5000; i++) {
				synchronized (room) {
					counter++;
				}
			}
		}, "t1");
		Thread t2 = new Thread(() -> {
			for (int i = 0; i < 5000; i++) {
				synchronized (room) {
					counter--;
				}
			}
		}, "t2");
		t1.start();
		t2.start();
		t1.join();
		t2.join();
//		log.debug("{}",counter);
		System.out.println(counter);
	}

在这里插入图片描述

3、 synchronized原理

3.1、Java对象头
3.1.1、对象头形式

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

普通对象

|--------------------------------------------------------------|
|                     Object Header (64 bits)                  |
|------------------------------------|-------------------------|
|        Mark Word (32 bits)         |    Klass Word (32 bits) |
|------------------------------------|-------------------------|

数组对象

|---------------------------------------------------------------------------------|
|                                 Object Header (96 bits)                         |
|--------------------------------|-----------------------|------------------------|
|        Mark Word(32bits)       |    Klass Word(32bits) |  array length(32bits)  |
|--------------------------------|-----------------------|------------------------|

**
说明**

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

这部分主要用来存储对象自身的运行时数据,如hashcode、gc分代年龄等。mark word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位。为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:

|-------------------------------------------------------|--------------------|
|                  Mark Word (32 bits)                  |       State        |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 |       Normal       |
|-------------------------------------------------------|--------------------|
|  thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 |       Biased       |
|-------------------------------------------------------|--------------------|
|               ptr_to_lock_record:30          | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
|               ptr_to_heavyweight_monitor:30  | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
|                                              | lock:2 |    Marked for GC   |
|-------------------------------------------------------|--------------------|

在这里插入图片描述

  • biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
  • age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
  • identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。
  • thread:持有偏向锁的线程ID。
  • epoch:偏向时间戳。
  • ptr_to_lock_record:指向栈中锁记录的指针。
  • ptr_to_heavyweight_monitor:指向管程Monitor的指针。
class pointer

  这一部分用于存储对象的类型指针该指针指向它的类元数据JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。

array length

  如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。

3.2、Monitor

Monitor被翻译为监视器或管程

每个Java对象都可以关联一个 Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的 Mark Word中就被设置指向 Monitor对象的指针。

Monitor结构如下:

在这里插入图片描述

  • 刚开始 Monitor中 Owner为null
  • 当 Thread-2执行 synchronized(obj)就会将 Monitor的所有者Owner置为 Thread-2,Monitor中只能有一个Owner
  • 在Thread-2上锁的过程中,如果 Thread-3, Thread-4, Thread-5也来执行 synchronized(obj),就会进入EntryList BlOCKED
  • Thread-2执行完同步代码块的内容,然后唤醒 Entry List中等待的线程来竞争锁,竟争的时是非公平的
  • 图中 WaitSet中的 Thread-0, Thread-l是之前获得过锁,但条件不满足进入WAITING状态的线程。

注意

  • synchronized必须是进入同一个对象的 monitor才有上述的效果不加
  • synchronized的对象不会关联监视器,不遵从以上规则

当Thread-2访问临界区代码,执行到synchronized(obj)时,就会尝试找一个Monitor(Monitor是操作系统实现的,Java访问不到)与obj关联。如果关联成功,就会修改Java对象头。
在这里插入图片描述
Java对象头变化,锁标志改为10,指向Monitor指针
在这里插入图片描述

如果只有Thread-2,没有其他线程竞争,Thread-2成为Monitor的所有者。
在这里插入图片描述
当Thread-1访问synchronized(obj)时,会先去看obj有没有关联一个Monitor锁,发现已经关联一个Monitor了。然后检查Monitor锁有没有主人,发现Owner已经是Threa-2了,获取不了锁了。然后只能与Monitor的EntryList关联,EntryList可以理解为阻塞队列。
在这里插入图片描述
Thread-3访问时,同样和Thread-1一样
在这里插入图片描述

当Thead-2临界区代码执行完毕后,Owner空出来了,会通知Monitor的EntryList中的线程,叫醒线程。
Thread-1和Thread-2竞争,谁获取根据JDK的底层实现决定(非公平竞争),假设是Thread-1竞争成功,则Thread-1成为Monitor的Owner主人。

在这里插入图片描述

3.3、synchronized 关键字的底层原理

  从JVM规范中可以看到Synchonized在JVM里的实现原理,JVM基于进入和退出Monitor对
象来实现方法同步和代码块同步,但两者的实现细节不一样。

  • 代码块同步: 通过使用monitorenter和monitorexit指令实现的.
  • 同步方法: ACC_SYNCHRONIZED修饰

反编译下面这段代码的字节码
在这里插入图片描述

	static int counter = 0;

	public synchronized void test(){
		counter++;
	}

	public void test2(){
		synchronized (this){
			counter++;
		}
	}

在这里插入图片描述

当执行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。

在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由ObjectMonitor实现的。每个对象中都内置了一个 ObjectMonitor对象。

在这里插入图片描述

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

3.4、synchronized优化

3.4.1、偏向锁

  在JDK1.6中为了提高一个对象在一段很长的时间内都只被一个线程用做锁对象场景下的性能,引入了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只会执行几个简单的命令,而不是开销相对较大的CAS命令。我们来看看偏向锁是如何做的。

3.4.1.1、偏向锁状态

jvm32位
在这里插入图片描述
jvm64位
在这里插入图片描述

  • 如果开启了偏向锁(默认开启),那么对象创建后, markward值为0x05即最后3位为101,这时它的thread、 epoch、age都为0
  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数-XX:BiasedLockingStartupDelay=0来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,markward值为0x01即最后3位为001,这时它的 hashcode、age都为0,第一次用到 hashcode时才会赋值。

测试

使用maven的方式,添加jol依赖

<dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.9</version>
        </dependency>

1、不加vm参数,打印对象头

@Slf4j(topic = "c.TestBiased")
public class TestBiased2 {

	public static void main(String[] args) throws InterruptedException {

		Dog dog=new Dog();

		log.debug(ClassLayout.parseInstance(dog).toPrintable());
//		ClassLayout.parseInstance(dog)
		Thread.sleep(6000);
		Dog dog2=new Dog();
		log.debug(ClassLayout.parseInstance(dog2).toPrintable());
		//查看jvm是多少位
		String property = System.getProperty("sun.arch.data.model");
		System.out.println(property);
	}
}

class Dog{

}

在这里插入图片描述

可以看到,jvm是32位的。普通对象的j对象头占2个字宽,32位jvm一个字等于4个字节,一个字节等于8位。所以Mark Word32位(64位jvm的Mark Word64位)。上面图中,前第一行4个字节表示Mark Word,第二行4个字节表示klass pointer(64位jvm指针压缩也是32位)。

因为是小端模式,可以看到第一次输出结果中第一个字节的后3位001,无锁状态。过了6秒后,输出的结果中,第一个字节的后3位为101,偏向锁状态。

在这里插入图片描述

小端模式
在这里插入图片描述


2、加VM参数 -XX:BiasedLockingStartupDelay=0 关闭偏向锁延迟
在这里插入图片描述

@Slf4j(topic = "c.TestBiased")
public class TestBiased2 {

	public static void main(String[] args) throws InterruptedException {

		Dog dog=new Dog();
		log.debug(ClassLayout.parseInstance(dog).toPrintable());

		Dog dog2=new Dog();
		log.debug(ClassLayout.parseInstance(dog2).toPrintable());

//		//查看jvm是多少位
//		String property = System.getProperty("sun.arch.data.model");
//		System.out.println(property);
	}
}

class Dog{

}

在这里插入图片描述

可以看到,第一次输出的结果中,Mark Word后3位就为101了。

3、对象加锁

@Slf4j(topic = "c.TestBiased")
public class TestBiased2 {

	public static void main(String[] args) throws InterruptedException {

		Dog dog=new Dog();
		log.debug(ClassLayout.parseInstance(dog).toPrintable());

		synchronized (dog){
			log.debug(ClassLayout.parseInstance(dog).toPrintable());
		}
		log.debug(ClassLayout.parseInstance(dog).toPrintable());


	}
}

class Dog{

}

在这里插入图片描述

可以看到对对象加锁和锁释放了,Mark Word多了线程ID.

4、测试禁用偏向锁,添加VM参数-XX:-UseBiasedLocking

在这里插入图片描述

@Slf4j(topic = "c.TestBiased")
public class TestBiased2 {

	public static void main(String[] args) throws InterruptedException {

		Dog dog=new Dog();
		log.debug(ClassLayout.parseInstance(dog).toPrintable());

		synchronized (dog){
			log.debug(ClassLayout.parseInstance(dog).toPrintable());
		}
		log.debug(ClassLayout.parseInstance(dog).toPrintable());

}

class Dog{

}

在这里插入图片描述

3.4.1.2、对象创建

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

3.4.1.3、加锁过程

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.4.1.4、 解锁过程

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

  撤销是指在获取偏向锁的过程因为不满足条件导致要将锁对象改为非偏向锁状态;解锁是指退出同步块时的过程

3.4.1.5、偏向锁撤销
  1. 调用对象hashCode()方法
  2. 其他线程使用对象
  3. 调用wait/notify

测试其他线程使用对象的情况,添加VM参数-XX:BiasedLockingStartupDelay=0

@Slf4j(topic = "c.TestBiased")
public class TestBiased2 {

	public static void main(String[] args) throws InterruptedException {

		Dog dog=new Dog();
		new Thread(()->{
			log.debug(ClassLayout.parseInstance(dog).toPrintable());
			synchronized (dog){
				log.debug(ClassLayout.parseInstance(dog).toPrintable());
			}
			log.debug(ClassLayout.parseInstance(dog).toPrintable());
			synchronized (TestBiased2.class){
				TestBiased2.class.notify();
			}
		},"t1").start();

		new Thread(()->{
			synchronized (TestBiased2.class){
				try {
					TestBiased2.class.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			log.debug(ClassLayout.parseInstance(dog).toPrintable());
			synchronized (dog){
				log.debug(ClassLayout.parseInstance(dog).toPrintable());
			}
			log.debug(ClassLayout.parseInstance(dog).toPrintable());

		},"t2").start();


}

class Dog{

}

在这里插入图片描述

在这里插入图片描述

可以看到,当线程1获得锁后,对象dog的偏向锁偏向线程1;线程1释放锁后,线程2竞争锁,锁升级为轻量级锁;线程2释放锁后,对象dog变为无锁状态。这里,线程1和线程2要错开竞争,不然就会升级为重量级锁。

3.4.1.6、批量重偏向和批量撤销

  从上文偏向锁的加锁解锁过程中可以看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时将偏向锁撤销为无锁状态或升级为轻量级/重量级锁。safe point这个词我们在GC中经常会提到,其代表了一个状态,在该状态下所有线程都是暂停的(大概这么个意思)。总之,偏向锁的撤销是有一定成本的,如果说运行时的场景本身存在多线程竞争的,那偏向锁的存在不仅不能提高性能,而且会导致性能下降。因此,JVM中增加了一种批量重偏向/撤销的机制。

存在如下两种情况:

  1. 一个线程创建了大量对象并执行了初始的同步操作,之后在另一个线程中将这些对象作为锁进行之后的操作。这种case下,会导致大量的偏向锁撤销操作。
  2. 存在明显多线程竞争的场景下使用偏向锁是不合适的,例如生产者/消费者队列。

批量重偏向(bulk rebias)机制是为了解决第一种场景。批量撤销(bulk revoke)则是为了解决第二种场景。

  在每次撤销偏向锁的时候,以类为单位,当某个类的对象撤销偏向次数达到一定阈值的时候JVM就认为该类不适合偏向模式或者需要重新偏向另一个线程。进行批量撤销或批量重偏向。

intx BiasedLockingBulkRebiasThreshold   = 20   默认偏向锁批量重偏向阈值 

intx BiasedLockingBulkRevokeThreshold  = 40   默认偏向锁批量撤销阈值

当然我们可以通过-XX:BiasedLockingBulkRebiasThreshold 
和 -XX:BiasedLockingBulkRevokeThreshold 来手动设置阈值
批量重偏向

当撤销偏向锁阈值超过20次后,ⅳm会这样觉得,我是不是偏错了呢,于是会在给这些对象加锁时重新偏向至加锁线程

@Slf4j(topic = "c.TestBiased")
public class TestBiased3 {
	static Thread t1,t2,t3;
	public static void main(String[] args) throws Exception {



		int loopNum=38;
		List<A> listA = new ArrayList<>();
		t1 = new Thread(() -> {
			for (int i = 0; i <loopNum ; i++) {
				A a = new A();
				listA.add(a);
				synchronized (a){
					log.debug(i+"\t"+ClassLayout.parseInstance(a).toPrintable());
				}
			}
			log.debug(ClassLayout.parseInstance(listA.get(loopNum-1)).toPrintable());
			LockSupport.unpark(t2);
            synchronized (listA){
				listA.notify();
			}
		},"t1");

		t1.start();

		//创建线程t2竞争线程t1中已经退出同步块的锁
		t2 = new Thread(() -> {
			synchronized (listA){
				try {
					listA.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			for (int i = 0; i <loopNum ; i++) {
				A a = listA.get(i);
				synchronized (a){
					log.debug(i+"\t"+ClassLayout.parseInstance(a).toPrintable());
				}
			}
			log.debug(ClassLayout.parseInstance(listA.get(loopNum-1)).toPrintable());

		},"t2");

		t2.start();

		t1.join();
		t2.join();
		A a = new A();
		log.debug(ClassLayout.parseInstance(a).toPrintable());

	}
}
class A{

}

在这里插入图片描述
在线程1里面,第一次对对象加锁,可以看到,前38个对象,都是偏向线程1。

在这里插入图片描述
在线程2里面,对前19个对象加锁,原本偏向线程1的偏向锁撤销,升级为轻量级锁

在这里插入图片描述

从第20次锁撤销开始,重新偏向线程2

批量撤销

当撤销偏向锁阈值超过40次后,jm会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的

@Slf4j(topic = "c.TestBiased")
public class TestBiased3 {
	static Thread t1,t2,t3;
	public static void main(String[] args) throws Exception {


		int loopNum=40;
		List<A> listA = new ArrayList<>();
		t1 = new Thread(() -> {
			for (int i = 0; i <loopNum ; i++) {
				A a = new A();
				listA.add(a);
				log.debug(i+"\t"+ClassLayout.parseInstance(a).toPrintable());
				synchronized (a){
					log.debug(i+"\t"+ClassLayout.parseInstance(a).toPrintable());
				}
				log.debug(i+"\t"+ClassLayout.parseInstance(a).toPrintable());
			}
			log.debug(ClassLayout.parseInstance(new A()).toPrintable());

			LockSupport.unpark(t2);

		},"t1");

		t1.start();

		//创建线程t2竞争线程t1中已经退出同步块的锁
		t2 = new Thread(() -> {
			LockSupport.park();

			for (int i = 0; i <loopNum ; i++) {
				A a = listA.get(i);
				log.debug(i+"\t"+ClassLayout.parseInstance(a).toPrintable());
				synchronized (a){
					log.debug(i+"\t"+ClassLayout.parseInstance(a).toPrintable());
				}
				log.debug(i+"\t"+ClassLayout.parseInstance(a).toPrintable());
			}
			log.debug(ClassLayout.parseInstance(new A()).toPrintable());
			LockSupport.unpark(t3);

		},"t2");

		t2.start();
		t3 = new Thread(() -> {
			LockSupport.park();
			for (int i = 0; i <loopNum ; i++) {
				A a = listA.get(i);
				log.debug(i+"\t"+ClassLayout.parseInstance(a).toPrintable());
				synchronized (a){
					log.debug(i+"\t"+ClassLayout.parseInstance(a).toPrintable());
				}
				log.debug(i+"\t"+ClassLayout.parseInstance(a).toPrintable());
			}
			log.debug(ClassLayout.parseInstance(new A()).toPrintable());

		},"t3");
        t3.start();
		t1.join();
		t2.join();
		t3.join();
		log.debug(ClassLayout.parseInstance(new A()).toPrintable());

	}
}
class A{

}

在这里插入图片描述

前40次,线程1第一次给对象加锁,偏向线程1,解锁后,仍是偏向线程1。
在这里插入图片描述

在线程2里,前19次给对象加锁,发生了19次偏向锁撤销,因为线程1已经执行完同步代码块了,所以恢复为无锁后,给对象加轻量级锁。

在这里插入图片描述

在这里插入图片描述

第20次锁撤销,重偏向线程2,解锁后,依旧还是偏向线程2的偏向锁。

在这里插入图片描述
第40次后,批量撤销,不再使用偏向锁。
在这里插入图片描述
批量锁撤销后,新建的对象,也不再使用偏向锁

3.4.2、轻量级锁

  轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。

3.4.2.1、加锁过程

1.在线程栈中创建一个Lock Record,将其Object reference字段指向锁对象。

2.直接通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。如果失败,进入到步骤3。

3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分(Displaced Mark Word)为null,起到了一个重入计数器的作用。然后结束。如果不是当前线程持有锁,进入步骤4。

4.走到这一步说明发生了竞争,需要膨胀为重量级锁,然后进入重量级锁。

3.4.2.2、解锁过程

1.遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。

2.如果Lock Record的Displaced Mark Word为null,代表这是一次重入,将obj设置为null后continue。

3.如果Lock Record的Displaced Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为Displaced Mark Word。如果成功,则continue,否则膨胀为重量级锁。

3.4.2.3、具体步骤

假设有两个代码块,利用同一对象加锁

public static void method1(){
		synchronized (object){
			//同步代码块
			method2();
		}
	}
	public static void method2(){
		synchronized (object){
			//同步代码块
		}
	}

有一个线程执行到上面的同步代码块时:

  • 创建锁记录( Lock record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word。
    在这里插入图片描述
  • 让锁记录中 Object reference指向锁对象,并尝试用cas替换 Object的 Mark Word,将 Mark word的值存入锁记录。
    在这里插入图片描述
  • 如果cas替换成功,对象头中存储了锁记录地址和状态θ0,表示由该线程给对象加锁,这时图示如下:
    在这里插入图片描述
  • 如果cas失败,有两种情况:
    • 如果是已经有其它线程已经持有了该 Object的轻量级锁,这时表明有竟争,进入锁膨胀过程。
    • 如果是同一线程执行了 synchronized锁重入,那么再添加一条 Lock record作为重入的计数,如图示
      在这里插入图片描述
  • 当退出 synchronized代码块(解锁时)如果有取值为null的锁记录,表示有重入,这时重置锁记录重入计数减一
    在这里插入图片描述
  • 当退出 synchronized代码块(解锁时)锁记录的值不为null,这时使用cas将 Mark word的值恢复给对象头。
    • 成功,则解锁成功
    • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进λ重量级锁解锁流程
3.4.2.4 锁膨胀

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竟争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

public static void method1(){
		synchronized (object){
			//同步代码块
			method2();
		}
	}
  • 当 Thread-1进行轻量级加锁时, Thread-0已经对该对象加了轻量级锁
    在这里插入图片描述

  • 这时 Thread-1加轻量级锁失败,进入锁膨胀流程

    • 即为 Object对象申请 Monitor锁,让 Object指向重量级锁地址
    • 然后自己进入 Monitor的 Entry List BLOCKED
      在这里插入图片描述
  • 当 Thread-0退出同步块解锁时,使用cas将 Mark word的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor地址找到 Monitor对象,设置 Owner为null,唤醒 Entry List中 BLOCKED线程

3.4.2.5、 总结

线程A和线程B竞争对象obj的锁(如: synchronized(obj){} ),这时线程A和线程B同时将对象obj的MarkWord复制到自己的锁记录中, 两者竞争去获取锁, 都尝试CAS修改对象obj的Mark Word。

假设线程A进行CAS修改成功,Mark Word替换为轻量级锁(ptr_to_lock_record指向自己的锁记录的指针),线程A获取锁成功。

这时,线程B仍通过CAS尝试修改对象obj的Mark Word,因为对象obj的MarkWord中的内容已经被线程A改了, 所以CAS失败,进行锁膨胀,Mark Word修改为重量级锁,轻量级锁升级为重量级锁,然后进行重量级锁竞争获取锁。

线程A执行完同步代码块后,进行锁释放,通过CAS尝试将栈帧中锁记录空间中的Mark Word替换到锁对象obj的对象头中,如果成功,表示锁释放成功。但这时,锁已经升级为量量级锁,CAS失败,进行锁膨胀,实现重量级锁的释放锁逻辑。

3.4.3、自旋优化

重量级锁竟争的时候,还可以使用自旋来迸行优化,如果当前线程自旋成功(即这时候持锁线程已经退岀了同步块,释放了锁),这时当前线程就可以避免阻塞。

自旋重试成功的情况

在这里插入图片描述

自旋失败的情况
在这里插入图片描述

  • 在Java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。
  • Jawa7之后不能控制是否开启自旋功能
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值