ToB企服应用市场:ToB评测及商务社交产业平台

标题: 一篇文章讲清楚synchronized关键字的作用及原理 [打印本页]

作者: 三尺非寒    时间: 2024-9-26 21:37
标题: 一篇文章讲清楚synchronized关键字的作用及原理
概述

在应用Sychronized关键字时需要把握如下注意点:
锁的范围

底层实现

加锁释放锁原理

synchronized是 Java内建的同步机制,所以也被称为 Intrinsic Locking,提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取锁的线程时只能等待或者阻塞在那里。
synchronized是基于一对 monitorenter/monitorexit 指令实现的,Monitor对象是同步的基本实现单元,无论是显示同步,照旧隐式同步都是云云。区别是同步代码块是通过明白的 monitorenter 和 monitorexit 指令实现,而同步方法通过ACC_SYNCHRONIZED 标记来隐式实现。
同步代码块
  1. public class Test1 {
  2.     public void fun1(){
  3.         synchronized (this){
  4.             System.out.println("fun111111111111");
  5.         }
  6.     }
  7. }
复制代码
将.java文件使用javac命令编译为.class文件,然后将class文件反编译出来。反编译的字节码文件截取:


通过反编译后的内容查看可以发现,synchronized编译后,同步块的前后有monitorenter/monitorexit两个 字节码指令。在Java假造机规范中有描述两条指令的作用:翻译一下如下:
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程实行monitorenter指令时实验获取monitor的所有权,过程如下:
monitorexit:
Q:synchronized 代码块内出现异常会释放锁吗?
A:会自动释放锁,查看字节码指令可以知道,monitorexit插入在方法竣事处(13行)和异常处(19行)。从Exception table异常表中也可以看出。
同步方法代码
  1. public class Test1 {
  2.     //锁当前对象(this)
  3.     public synchronized void fun2(){
  4.         System.out.println("fun2222222222222222222222");
  5.     }
  6.      //静态synchronized修饰:使用的锁对象是当前类的class对象
  7.     public synchronized static void fun3(){
  8.         System.out.println("fun33333333333333");
  9.     }
  10. }
复制代码
编译之后反编译截图:

从反编译的结果来看,同步方法外貌上不是通过monitorenter/monitorexit指令来完成,但是与平凡方法相比,常量池中多出来了ACC_SYNCHRONIZED标识符。java假造机就是根据ACC_SYNCHRONIZED标识符来实现方法的同步,当调用方法时,调用指令先检查方法是否有 ACC_SYNCHRONIZED访问标记,如果存在,实行线程将先获取monitor,获取乐成之后才实行方法体,实行完后再释放monitor。在方法实行期间,其他线程都无法再获取到同一个monitor对象。 虽然编译后的结果看起来不一样,但现实上没有本质的区别,只是方法的同步是通过隐式的方式来实现,无需通过字节码来完成。
ACC_SYNCHRONIZED的访问标记,其实就是代表:当线程实行到方法后,如果检测到有该访问标记就会隐式的去调用monitorenter/monitorexit两个命令来将方法锁住。
小结

synchronized 同步代码块的实现是通过 monitorenter 和 monitorexit 指令,此中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的竣事位置。当实行 monitorenter 指令时,线程试图获取锁也就是获取 monitor的持有权(monitor对象存在于每个Java对象的对象头中, synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的缘故原由)。
其内部包含一个计数器,当计数器为0则可以乐成获取,获取后将锁计数器设为1也就是加1。相应的在实行 monitorexit 指令后,将锁计数器设为0 ,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标记来辨别一个方法是否声明为同步方法,从而实行相应的同步调用。
可重入锁原理

ReentrantLock和synchronized都是可重入锁
定义
指的是 同一个线程的 可以多次获得 同一把锁(一个线程可以多次实行synchronized,重复获取同一把锁)。
  1. /*  可重入特性    指的是 同一个线程获得锁之后,可以再次获取该锁。*/
  2. public class Demo01 {
  3.     public static void main(String[] args) {
  4.         Runnable sellTicket = new Runnable() {
  5.             @Override
  6.             public void run() {
  7.                 synchronized (Demo01.class) {
  8.                     System.out.println("我是run");
  9.                     test01();
  10.                 }
  11.             }
  12.             public void test01() {
  13.                 synchronized (Demo01.class) {
  14.                     System.out.println("我是test01");
  15.                 }
  16.             }
  17.         };
  18.         new Thread(sellTicket).start();
  19.         new Thread(sellTicket).start();
  20.     }
  21. }
复制代码
原理
synchronized 的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁,每重入一次,计数器就 + 1,在实行完一个同步代码块时,计数器数量就会减1,直到计数器的数量为0才释放这个锁。
优点
可以肯定程度上克制死锁(如果不能重入,那就不能再次进入这个同步代码块,导致死锁);更好地封装代码(可以把同步代码块写入到一个方法中,然后在另一个同步代码块中直接调用该方法实现可重入);
包管可见性原理

这个紧张在于内存模型和happens-before规则
Synchronized的happens-before规则,即监视器锁规则:对同一个监视器的解锁,happens-before于对该监视器的加锁。
  1. public class MonitorDemo {
  2.     private int a = 0;
  3.     public synchronized void writer() {     // 1
  4.         a++;                                // 2
  5.     }                                       // 3
  6.     public synchronized void reader() {    // 4
  7.         int i = a;                         // 5
  8.     }                                      // 6
  9. }
复制代码
该代码的happens-before关系如图所示:

在图中每一个箭头毗连的两个节点就代表之间的happens-before关系,玄色的是通过步伐序次规则推导出来,红色的为监视器锁规则推导而出:线程A释放锁happens-before线程B加锁,蓝色的则是通过步伐序次规则和监视器锁规则推测出来happens-befor关系,通过传递性规则进一步推导的happens-before关系。
这里是2 happens-before 5,通过这个关系可以得出:根据happens-before的定义中的一条:如果A happens-before B,则A的实行结果对B可见,并且A的实行序次先于B。线程A先对共享变量A进行加一,由2 happens-before 5关系可知线程A的实行结果对线程B可见即线程B所读取到的a的值为1
对象结构

HotSpot假造机中,对象在内存中存储的结构可以分为三块地域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

Java对象头

Hotspot的对象头紧张包括三部门数据:Mark Word(标记字段)、Klass Pointer(范例指针)、数组长度
此中Mark Word是用于存储对象自身运行时数据,如HashCode、GC分代年龄、锁状态标记、线程持有锁、方向线程ID、方向时间戳等信息。
Mark Word

比如 hash码,对象所属的年代,对象锁,锁状态标记,方向锁(线程)ID,方向时间,数组长度(数组对象)等。Java对象头一般占据2个机器码(64位假造机中,1个机器码是8个字节,也就是64bit)。

synchronized锁的优化

JVM中monitorenter和monitorexit字节码依靠于底层的操作体系的Mutex Lock来实现的,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来实行,这种切换的代价是非常昂贵的;然而在现实中的大部门环境下,同步方法是运行在单线程环境(无锁竞争环境)如果每次都调用Mutex Lock那么将严重的影响步伐的性能。所以在jdk1.6中对锁的实现引入了大量的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、方向锁(Biased Locking)、顺应性自旋(Adaptive Spinning)等技术来减少锁操作的开销
Monitor

Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。
Monitor的基本结构是什么?

Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
synchronized通过Monitor来实现线程同步,Monitor是依靠于底层的操作体系的Mutex Lock(互斥锁)来实现的线程同步。这种依靠于操作体系Mutex Lock所实现的锁我们称之为“重量级锁”,因此,为了减少获得锁和释放锁带来的性能消耗,引入了“方向锁”和“轻量级锁”。
锁的范例

在Java SE 1.6里Synchronied同步锁,一共有四种状态:无锁、方向锁、轻量级锁、重量级锁,它会随着竞争环境逐渐升级。锁可以升级但是不可以降级,目的是为了提供获取锁和释放锁的效率。
锁升级过程: 无锁 → 方向锁 → 轻量级锁 → 重量级锁 (此过程是不可逆的
synchronized是灰心锁,在操作同步资源之前需要给同步资源先加锁,这把锁就是存在Java对象头里的。
四种锁状态对应的的Mark Word内容:

锁粗化

原则上,我们都知道在加同步锁时,尽可能的将同步块的作用范围限定到尽量小的范围(只在共享数据的现实作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小。在存在锁同步竞争中,也可以使得等待锁的线程尽早的拿到锁)。
大部门上述环境是完善准确的,但是如果存在连串的一系列操作都对同一个对象反复加锁息争锁,乃至加锁操作时出现在循环体中的,那纵然没有线程竞争,频繁的进行互斥同步操作也会导致不必要的性能操作。
这里贴上根据上述Javap 编译的环境编写的实例java类
  1. public static String test04(String s1, String s2, String s3) {
  2.     StringBuffer sb = new StringBuffer();
  3.     sb.append(s1);
  4.     sb.append(s2);
  5.     sb.append(s3);
  6.     return sb.toString();
  7. }
复制代码
在上述的一连append()操作中就属于这类环境。JVM会检测到这样一连串的操作都是对同一个对象加锁,那么JVM会将加锁同步的范围扩展(粗化)到整个一系列操作的 外部,使整个一连串的append()操作只需要加锁一次就可以了。
锁消除

锁消除是指假造机即时编译器再运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的紧张判定依据来源于逃逸分析的数据支持。意思就是:JVM会判定再一段步伐中的同步明显不会逃逸出去从而被其他线程访问到,那JVM就把它们当作栈上数据对待,认为这些数据是线程独有的,不需要加同步。此时就会进行锁消除。
当然在现实开辟中,我们很清楚的知道哪些是线程独有的,不需要加同步锁,但是在Java API中有许多方法都是加了同步的,那么此时JVM会判定这段代码是否需要加锁。如果数据并不会逃逸,则会进行锁消除。比如如下操作:在操作String范例数据时,由于String是一个不可变类,对字符串的毗连操作总是通过生成的新的String对象来进行的。因此Javac编译器会对String毗连做自动优化。在JDK 1.5之前会使用StringBuffer对象的一连append()操作,在JDK 1.5及以后的版本中,会转化为StringBuidler对象的一连append()操作。
  1. public static String test03(String s1, String s2, String s3) {
  2.     String s = s1 + s2 + s3;
  3.     return s;
  4. }
复制代码
上述代码使用javap 编译结果

众所周知,StringBuilder不是安全同步的,但是在上述代码中,JVM判定该段代码并不会逃逸,则将该代码带默认为线程独有的资源,并不需要同步,所以实行了锁消除操作。(还有Vector中的各种操作也可实现锁消除。在没有逃逸出数据安全防卫内)
方向锁

通俗的讲,方向锁就是在运行过程中,对象的锁方向某个线程。即在开启方向锁机制的环境下,某个线程获得锁,当该线程下次再想要获得锁时,不需要再获得锁(即忽略synchronized关键词),直接就可以实行同步代码,比较得当竞争较少的环境。
为了解决这一问题,HotSpot的作者在Java SE 1.6 中对Synchronized进行了优化,引入了方向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁方向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁息争锁。只需要简单的测试一下对象头的Mark Word里是否存储着指向当前线程的方向锁。如果乐成,表示线程已经获取到了锁。
方向锁的获取流程

方向锁的释放流程:

方向锁只有遇到其他线程实验竞争方向锁时,持有方向锁状态的线程才会释放锁,线程不会主动去释放方向锁。方向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在实行),它会首先暂停拥有方向锁的线程,判定锁对象是否处于被锁定状态。撤销方向锁后恢复到无锁(标记位为“01”)或升级到轻量级锁(标记位为“00”)的状态
轻量级锁

在JDK 1.6之后引入的轻量级锁,需要注意的是轻量级锁并不是替换重量级锁的,而是对在大多数环境下同步块并不会有竞争出现提出的一种优化。它可以减少重量级锁对线程的阻塞带来的线程开销。从而进步并发性能。
但是当多个线程同时竞争锁时,轻量级锁会膨胀为重量级锁。
轻量级锁加锁

轻量级锁解锁

使用原子的CAS操作将Displaced Mark Word替换回到对象头中,如果乐成,则表示没有发生竞争关系。如果失败,表示当前锁存在竞争关系。锁就会膨胀成重量级锁。
锁的优缺点

锁优点缺点使用场景方向锁加锁息争锁不需要CAS操作,没有额外的性能消耗,和实行非同步方法相比仅存在纳秒级的差距如果线程间存在锁竞争,会带来额外的锁撤销的消耗实用于只有一个线程访问同步块的场景轻量级锁竞争的线程不会阻塞,进步了响应速度如线程始终得不到锁竞争的线程,使用自旋会消耗CPU性能追求响应时间,同步块实行速度非常快重量级锁线程竞争不实用自旋,不会消耗CPU线程阻塞,响应时间缓慢,在多线程下,频繁的获取释放锁,会带来巨大的性能消耗追求吞吐量,同步块实行速度较长锁升级过程

Synchronized与ReentrantLock


synchronized的缺陷

Lock解决相应问题

Lock类这里不做过多解释,紧张看里面的4个方法:
Synchronized加锁只与一个条件(是否获取锁)相关联,不灵活,厥后Condition与Lock的结合解决了这个问题。
多线程竞争一个锁时,别的未得到锁的线程只能不绝的实验获得锁,而不能中断。高并发的环境下会导致性能降落。ReentrantLock的lockInterruptibly()方法可以优先考虑响应中断。 一个线程等待时间过长,它可以中断自己,然后ReentrantLock响应这个中断,不再让这个线程继续等待。有了这个机制,使用ReentrantLock时就不会像synchronized那样产生死锁了。
ReentrantLock为常用类,它是一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
synchronized是通过软件(JVM)实现的,简单易用,纵然在JDK5之后有了Lock,仍然被广泛的使用。
Synchronized使用注意点

关于作者

来自一线步伐员Seven的探索与实践,持续学习迭代中~
本文已收录于我的个人博客:https://www.seven97.top
公众号:seven97,欢迎关注~

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4