欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

synchronized如何保证线程同步?

程序员文章站 2022-07-26 17:23:00
什么是synchronized?synchronized是Java提供的一个并发控制的关键字。可以用来修饰方法和代码块。被synchronized修饰的代码块及方法,在同一时间,只能被单个线程访问。synchronized有什么作用?使用该关键字修饰的方法,在同一时刻最多只有一个线程可以进入。如果第一个线程获取锁进入了synchronized修饰的区域,在其释放锁之前,需要进入该实例中synchronized修饰的方法或者代码块的其他线程就需要等待,直到第一个线程释放锁之后,其他线程中才会有一个线...

什么是synchronized

synchronized是Java提供的一个并发控制的关键字。可以用来修饰方法代码块。被synchronized修饰的代码块及方法,在同一时间,只能被单个线程访问。

synchronized有什么作用?

使用该关键字修饰的方法,在同一时刻最多只有一个线程可以进入。如果第一个线程获取锁进入了synchronized修饰的区域,在其释放锁之前,需要进入该实例中synchronized修饰的方法或者代码块的其他线程就需要等待,直到第一个线程释放锁之后,其他线程中才会有一个线程接着获取锁,进入互斥资源访问区。
通过词典查字面意思,是:adj. 同步的;同步化的。简而言之,参照Java内存模型,synchronized可以保证原子性有序性可见性

synchronized原理

没有什么比源码更有说服力的了,我们在一个类中,分别使用synchronized来修饰方法和代码块,然后编译该类,再使用javap命令,分析汇编指令。

实战源码

SynchronizedForJavap.java

/**
 * <pre>
 * 程序目的:观察使用了synchronized关键字的java class文件,反编码后的字节码信息,
 * 以便观察jvm是如何实现synchronized的
 * </pre>
 * created at 2020-07-15 07:27
 * @author lerry
 */
public class SynchronizedForJavap {
	public void syncBlock() {
		synchronized (this) {
			System.out.println("hello block");
		}
	}

	public synchronized void syncMethod() {
		System.out.println("hello method");
	}
}

先执行:javac SynchronizedForJavap.java,对java文件进行编译,生成SynchronizedForJavap.class文件,然后执行:javap -v SynchronizedForJavap.class。部分输出结果如下:

public void syncBlock();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter // monitorenter指令进入同步块
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #3                  // String hello block
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: monitorexit // monitorexit指令退出同步块
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit // monitorexit指令退出同步块
        20: aload_2
        21: athrow
        22: return

————————————————————————————————————
public synchronized void syncMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED //在同步方法中添加了ACC_SYNCHRONIZED标记
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #5                  // String hello method
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 18: 0
        line 19: 8


从上面的中文注释处可以看到,对于synchronized关键字而言,javac在编译时,会生成对应的monitorentermonitorexit指令分别对应synchronized同步块的进入和退出,有两个monitorexit指令的原因是:为了保证抛异常的情况下也能释放锁,所以javac为同步代码块添加了一个隐式的try-finally,在finally中会调用monitorexit命令释放锁。而对于synchronized方法而言,javac为其生成了一个ACC_SYNCHRONIZED关键字,在JVM进行方法调用时,发现调用的方法被ACC_SYNCHRONIZED修饰,则会先尝试获得锁。

想要深入理解synchronized,还需要了解Java内存布局

Java内存布局

在这里,我们需要借助一个工具:

<!--查看Java 对象布局、大小工具-->
<dependency>
	<groupId>org.openjdk.jol</groupId>
	<artifactId>jol-core</artifactId>
	<version>0.10</version>
</dependency>

JavaObjectLayOutDemo.java

import org.openjdk.jol.info.ClassLayout;

/**
 * <pre>
 * 程序目的:观察Java内存布局
 * </pre>
 * created at 2020-07-15 08:05
 * @author lerry
 */
public class JavaObjectLayOutDemo {
	public static void main(String[] args) {
		Object obj = new Object();
		System.out.println(ClassLayout.parseInstance(obj).toPrintable());
	}
}

输出结果如下:synchronized如何保证线程同步?
图:new Object内存布局
一个Java对象在内存中包括对象头、实例数据和补齐填充3个部分:
synchronized如何保证线程同步?
图:Java对象内存布局
现在,加上一段代码,使用synchronized关键字,对obj对象加锁:

public class JavaObjectLayOutDemo {
	public static void main(String[] args) {
		Object obj = new Object();
		System.out.println(ClassLayout.parseInstance(obj).toPrintable());

		System.out.println("使用了synchronized关键字之后的对象内存布局:");
		synchronized (obj){
			System.out.println(ClassLayout.parseInstance(obj).toPrintable());
		}
	}
}

输出结果如下:
synchronized如何保证线程同步?
图:控制台输出-加上synchronized关键字后
synchronized如何保证线程同步?
图:控制台输出结果前后对比-MarkWord部分发生了改变
由此得知:synchronized的锁,记录在对象的对象头中的Mark Word部分。

接下来,我们来看一下各种锁状态。

锁状态 new、偏向锁、轻量级锁、重量级锁

synchronized如何保证线程同步?
图:HotSpot实现的锁状态对比表-synchronized
对应到控制台输出,对应位置如下:
synchronized如何保证线程同步?
图:控制台输出-对象内存布局-锁的位置

锁状态说明及状态转换

  1. Object obj = new Object();
    锁 0 01 -> 无锁状态
  2. 默认synchronized(obj)
    00 -> 轻量级锁
    默认情况偏向锁有个时延,默认是4秒
    why?因为JVM虚拟机自己有一些默认启动的线程,里面有好多sync代码,这些sync代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。
  3. 如果设定-XX:BiasedLockingStartupDelay=0
    new Object() -> 1 01
    偏向锁 -> 线程ID为0 -> Anonymous BiasedLock
    打开偏向锁,new出来的对象,默认就是一个可偏向匿名对象1 01
  4. 如果有线程上锁
    上偏向锁,指的就是,把markword的线程ID改为自己线程ID的过程
    偏向锁不可重偏向、批量偏向、批量撤销
  5. 如果有线程竞争
    撤销偏向锁,升级轻量级锁
    线程在自己的线程栈生成LockRecord,用CAS操作将markword设置为指向自己这个线程的LockRecord的指针,设置成功者得到锁
  6. 如果竞争加剧
    竞争加剧: 有线程超过10次自旋,-XX:PreBlockSpin(在JDK7u40的时候这个指令消失了), 或者自旋线程数超过CPU核数的一半,1.6之后, 加入
    自适应自旋Adapative Self Spinning,JVM自己控制
    升级重量级锁: -> 向操作系统申请资源,linux mutex , CPU3级-0级系统调用,线程挂起,进入等待队列,
    等待操作系统的调度,然后再映射回用户空间
    (以上实验环境是JDK11,打开就是偏向锁,而JDK8默认对象头是无锁)
    下图是JDK1.6引入偏向锁之后的状态转换示意图:
    synchronized如何保证线程同步?
    图:偏向锁、轻量级锁的状态转化及对象MarkWord的关系-《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》

这样的状态转换,虽然脉络清晰了不少,但是是面向机器的。接下来,用一个实际生活场景,来阐述一下锁的各种状态转换。

有一天,蜘蛛侠吃坏了肚子,跑来卫生间蹲坑,这时只有他一个人,没人和他抢,于是管理员在厕所门上贴了个“蜘蛛侠专用”(线程ID),既然很着急、门就不锁了,这样效率也高。  
蜘蛛侠跑了两次,每次门都没锁,可以直接进去,很高兴。科室过了会儿、蝙蝠侠也跑过来了,原来中午他们一起在一家使用地沟油的餐厅吃饭,都中招了。这时,管理员把门上的标签撕掉,让两个人竞争。最后蝙蝠侠更快一些,门上的便签换成了“蝙蝠侠专用”,蜘蛛侠在外面急的转圈(有线程竞争、升级轻量级锁、CAS自旋)。蝙蝠侠出来后,蜘蛛侠赶紧冲进去干活儿。

再后来、发现钢铁侠、雷神、美队都跑过来了,蜘蛛侠占着位置,其他人都在外面急的转圈儿,管理员说这样转太浪费功夫了,于是,给门上了锁(竞争加剧、升级重量级锁),其他人都别动(不耗费CPU资源),都排队(进入等待队列)。蜘蛛侠出来后,管理员把锁交给美队(CPU指定线程去执行)。依次类推。

思维导图

synchronized如何保证线程同步?
图:blog思维导图

参考资料

马士兵-《多线程与高并发》,帮助你·理解多线程在CPU层级的实现,以及这些实现如何一层一层的映射到那些上亿用户,千万QPS,百万TPS的系统。
三大性质总结:原子性,有序性,可见性 - 简书
通过javap命令分析java汇编指令 - 简书
死磕Synchronized底层实现–概论 - 掘金
JOL:查看Java 对象布局、大小工具_禅鸣之时-CSDN博客_jol
Java对象内存布局 - 简书

环境说明

  • java -version
java version "1.8.0_251"
Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)

  • OS:macOS High Sierra 10.13.4

本文地址:https://blog.csdn.net/limenghua9112/article/details/107374854