学习
实践
活动
专区
工具
TVP
写文章
专栏首页北风IT之路深入理解synchronized底层原理,一篇文章就够了!

深入理解synchronized底层原理,一篇文章就够了!

  • 前言
  • 一、synchronized的特性
    • 1.1 原子性
    • 1.2 可见性
    • 1.3 有序性
    • 1.4 可重入性
  • 二、synchronized的用法
  • 三、synchronized锁的实现
    • 3.1 同步方法
    • 3.2 同步代码块
  • 四、synchronized锁的底层实现
  • 五、JVM对synchronized的优化
    • 5.1 锁膨胀
      • 5.1.1 偏向锁
      • 5.1.2 轻量级锁
      • 5.1.3 重量级锁
    • 5.2 锁消除
    • 5.3 锁粗化
    • 5.4 自旋锁与自适应自旋锁
  • 结语
  • 前言

如果某一个资源被多个线程共享,为了避免因为资源抢占导致资源数据错乱,我们需要对线程进行同步,那么synchronized就是实现线程同步的关键字,可以说在并发控制中是必不可少的部分,今天就来看一下synchronized的使用和底层原理。

一、synchronized的特性

1.1 原子性

所谓原子性就是指一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。但是像i++、i+=1等操作字符就不是原子性的,它们是分成读取、计算、赋值几步操作,原值在这些步骤还没完成时就可能已经被赋值了,那么最后赋值写入的数据就是脏数据,无法保证原子性。

被synchronized修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放,这中间的过程无法被中断(除了已经废弃的stop()方法),即保证了原子性。

注意!面试时经常会问比较synchronized和volatile,它们俩特性上最大的区别就在于原子性,volatile不具备原子性。

1.2 可见性

可见性是指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的。

synchronized和volatile都具有可见性,其中synchronized对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到主存当中,保证资源变量的可见性,如果某个线程占用了该锁,其他线程就必须在锁池中等待锁的释放。

而volatile的实现类似,被volatile修饰的变量,每当值需要修改时都会立即更新主存,主存是共享的,所有线程可见,所以确保了其他线程读取到的变量永远是最新值,保证可见性。

1.3 有序性

有序性值程序执行的顺序按照代码先后执行。

synchronized和volatile都具有有序性,Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。

1.4 可重入性

synchronized和ReentrantLock都是可重入锁。当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。通俗一点讲就是说一个线程拥有了锁仍然还可以重复申请锁。

二、synchronized的用法

synchronized可以修饰静态方法、成员函数,同时还可以直接定义代码块,但是归根结底它上锁的资源只有两类:一个是对象,一个是

先看看下面的代码(初学者看到先不要晕,后面慢慢讲解):

首先我们知道被static修饰的静态方法、静态属性都是归类所有,同时该类的所有实例对象都可以访问。但是普通成员属性、成员方法是归实例化的对象所有,必须实例化之后才能访问,这也是为什么静态方法不能访问非静态属性的原因。我们明确了这些属性、方法归哪些所有之后就可以理解上面几个synchronized的锁到底是加给谁的了。

首先看第一个synchronized所加的方法是add1(),该方法没有被static修饰,也就是说该方法是归实例化的对象所有,那么这个锁就是加给Test1类所实例化的对象。

然后是add2()方法,该方法是静态方法,归Test1类所有,所以这个锁是加给Test1类的。

最后是method()方法中两个同步代码块,第一个代码块所锁定的是Test1.class,通过字面意思便知道该锁是加给Test1类的,而下面那个锁定的是instance,这个instance是Test1类的一个实例化对象,自然它所上的锁是给instance实例化对象的。

弄清楚这些锁是上给谁的就应该很容易懂synchronized的使用啦,只要记住要进入同步方法或同步块必须先获得相应的锁才行。那么我下面再列举出一个非常容易进入误区的代码,看看你是否真的理解了上面的解释。

上面的简单意思就是用两个线程分别对i加100万次,理论结果应该是200万,而且我还加了synchronized锁住了add方法,保证了其线程安全性。可是!!!我无论运行多少次都是小于200万的,为什么呢?

原因就在于synchronized加锁的函数,这个方法是普通成员方法,那么锁就是加给对象的,但是在创建线程时却new了两个Test2实例,也就是说这个锁是给这两个实例加的锁,并没有达到同步的效果,所以才会出现错误。至于为什么小于200万,要理解i++的过程就明白了,我之前写了一篇文章讲解过这个过程,请阅读:详谈Java中的CAS操作

三、synchronized锁的实现

synchronized有两种形式上锁,一个是对方法上锁,一个是构造同步代码块。他们的底层实现其实都一样,在进入同步代码之前先获取锁,获取到锁之后锁的计数器+1,同步代码执行完锁的计数器-1,如果获取失败就阻塞式等待锁的释放。只是他们在同步块识别方式上有所不一样,从class字节码文件可以表现出来,一个是通过方法flags标志,一个是monitorenter和monitorexit指令操作。

3.1 同步方法

首先来看在方法上上锁,我们就新定义一个同步方法然后进行反编译,查看其字节码:

可以看到在add方法的flags里面多了一个ACC_SYNCHRONIZED标志,这标志用来告诉JVM这是一个同步方法,在进入该方法之前先获取相应的锁,锁的计数器加1,方法结束后计数器-1,如果获取失败就阻塞住,知道该锁被释放。

如果看不懂字节码指令的朋友可以先阅读我之前写的两篇文章,了解一下class的结构:

3.2 同步代码块

我们新定义一个同步代码块,编译出class字节码,然后找到method方法所在的指令块,可以清楚的看到其实现上锁和释放锁的过程,截图如下:

从反编译的同步代码块可以看到同步块是由monitorenter指令进入,然后monitorexit释放锁,在执行monitorenter之前需要尝试获取锁,如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,那么就把锁的计数器加1。当执行monitorexit指令时,锁的计数器也会减1。当获取锁失败时会被阻塞,一直等待锁被释放。

但是为什么会有两个monitorexit呢?其实第二个monitorexit是来处理异常的,仔细看反编译的字节码,正常情况下第一个monitorexit之后会执行goto指令,而该指令转向的就是23行的return,也就是说正常情况下只会执行第一个monitorexit释放锁,然后返回。而如果在执行中发生了异常,第二个monitorexit就起作用了,它是由编译器自动生成的,在发生异常时处理异常然后释放掉锁。

四、synchronized锁的底层实现

在理解锁实现原理之前先了解一下Java的对象头和Monitor,在JVM中,对象是分成三部分存在的:对象头、实例数据、对其填充。

实例数据和对其填充与synchronized无关,这里简单说一下(我也是阅读《深入理解Java虚拟机》学到的,读者可仔细阅读该书相关章节学习)。实例数据存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐;对其填充不是必须部分,由于虚拟机要求对象起始地址必须是8字节的整数倍,对齐填充仅仅是为了使字节对齐。

对象头是我们需要关注的重点,它是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。对象头主要结构是由Mark WordClass Metadata Address组成,其中Mark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息Class Metadata Address是类型指针指向对象的类元数据,JVM通过该指针确定该对象是哪个类的实例

锁也分不同状态,JDK6之前只有两个状态:无锁、有锁(重量级锁),而在JDK6之后对synchronized进行了优化,新增了两种状态,总共就是四个状态:无锁状态、偏向锁、轻量级锁、重量级锁,其中无锁就是一种状态了。锁的类型和状态在对象头Mark Word中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word数据。

每一个锁都对应一个monitor对象,在HotSpot虚拟机中它是由ObjectMonitor实现的(C++实现)。每个对象都存在着一个monitor与之关联,对象与其monitor之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个monitor被某个线程持有后,它便处于锁定状态。

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;  //锁计数器
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

该段摘自:https://blog.csdn.net/javazejian/article/details/72828483   ObjectMonitor中有两个队列_WaitSet和_EntryList,用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入_EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。   monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因(关于这点稍后还会进行分析)

五、JVM对synchronized的优化

从最近几个jdk版本中可以看出,Java的开发团队一直在对synchronized优化,其中最大的一次优化就是在jdk6的时候,新增了两个锁状态,通过锁消除、锁粗化、自旋锁等方法使用各种场景,给synchronized性能带来了很大的提升。

5.1 锁膨胀

上面讲到锁有四种状态,并且会因实际情况进行膨胀升级,其膨胀方向是:无锁——>偏向锁——>轻量级锁——>重量级锁,并且膨胀方向不可逆。

5.1.1 偏向锁

一句话总结它的作用:减少统一线程获取锁的代价。在大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得,那么此时就是偏向锁。

核心思想:

如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也就变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程ID等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。

5.1.2 轻量级锁

轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块。

5.1.3 重量级锁

重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。

重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景。

5.2 锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。比如下面代码的method1和method2的执行效率是一样的,因为object锁是私有变量,不存在所得竞争关系。

5.3 锁粗化

锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁。比如下面method3经过锁粗化优化之后就和method4执行效率一样了。

5.4 自旋锁与自适应自旋锁

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。

自旋锁:许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得,通过让线程执行循环等待锁的释放,不让出CPU。如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。但是它也存在缺点:如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销。

自适应自旋锁:这种相当于是对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。

结语

synchronized关键字是并发编程不可或缺的部分,个人认为能真实理解其内部运作原理能对平时的开发带来很大意义上的帮助,希望这篇文章能帮助你!

文章分享自微信公众号:
北风IT之路

本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!

作者:beifengtz
原始发表时间:2019-07-15
如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • 深入理解synchronized底层原理,一篇文章就够了!

    如果某一个资源被多个线程共享,为了避免因为资源抢占导致资源数据错乱,我们需要对线程进行同步,那么synchronized就是实现线程同步的关键字,可以说在并发控...

    挨踢小子部落阁
  • ButterKnife原理解析看这篇文章就够了

    ButterKnife 算是一款知名老牌 Android 开发框架了,通过注解绑定视图,避免了 findViewById() 的操作,广受好评!由于它是在编译时...

    用户1269200
  • Activity、View、Window的理解一篇文章就够了

    要了解这三者之间的关系,我们带着问题通过分析源码一步一步来揭开它们的神秘面纱! 文章有点长,首先要理解Activity、View、Window,我提出了一些问题...

    用户2802329
  • ESP定律原理详解,只看这一篇文章就够了

    闲着也是闲着,在逆向某软件时深入了解了下ESP定律,然后就想写个文章记录并分享下。

    Java程序猿
  • 了解redis的单线程模型工作原理?一篇文章就够了

    1、首先redis是单线程的,为什么redis会是单线程的呢? 从redis的性能上进行考虑,单线程避免了上下文频繁切换问题,效率高; 从redis的内部结构设...

    HUC思梦
  • 你真的知道Java内存模型是什么吗

    前几天,发了一篇文章,介绍了一下JVM内存结构、Java内存模型以及Java对象模型之间的区别。有很多小伙伴反馈希望可以深入的讲解下每个知识点。Java内存模型...

    格姗知识圈
  • Java内存模型详解

    前几天,发了一篇文章,介绍了一下JVM内存结构、Java内存模型以及Java对象模型之间的区别。有很多小伙伴反馈希望可以深入的讲解下每个知识点。Java内存模型...

    全栈程序员站长
  • Java工程师核心书单推荐

    书籍是获取知识的最好来源之一。技术书籍浩如烟海,市场上有不少《XXX入门到精通》,《XXX王者归来》,《21天学会XXX》这样的书,当然也有不少经典,个人的精力...

    三分恶
  • 读者上岸阿里的硬核经验分享

    大家好,我是石头哥,最近正值校招季,这里分享一篇硬核校招面经,希望对大家有帮助。

    程序猿石头
  • synchronized的实现原理——对象头解密

    并发编程式Java基础,同时也是Java最难的一部分,因为与底层操作系统和硬件息息相关,并且程序难以调试。本系列就从synchronized原理开始,逐步深入,...

    夜勿语
  • 全网首发|如何不费吹灰之力就搞懂大脑的运行原理?这是有史以来最深入浅出的一篇科普文章了(《Neuralink》编译系列二)

    昨天,我们全网首发了Tim Urban所撰写的,关于马斯克新公司Neuralink的科普文章的第一部分。 不过,那仅仅是个开胃汤,真正的热菜还没端上桌。 今...

    AI科技大本营
  • 轻松理解计算机的内存模型及Java内存模型

    本文转载自:https://www.hollischuang.com/archives/2550

    aoho求索
  • BATJ等公司必问的8道Java经典面试题,你都会了吗?

    我在 Oracle 已经工作了近 7 年,面试过从初级到非常资深的 Java 工程师,且由于 Java 组工作任务的特点,我非常注重面试者的计算机科学基础和编程...

    Java团长
  • 跳槽时,这些Java面试题99%会被问到

    工作多年以及在面试中,我经常能体会到,有些面试者确实是认真努力工作,但坦白说表现出的能力水平却不足以通过面试,通常是两方面原因:

    java思维导图
  • 【原创】为了你,我又又又把Java学习路线和方法更新了一遍

    到目前为止,我觉得不管是在公众号后台、知乎还是微信上面我被问的做多的就是:“大佬,有没有 Java 学习路线和方法”(大佬属现代流行的客气称呼,本人非大佬哈)。...

    Guide哥
  • JMM到底如何理解?JMM与MESI到底有没有关系?

    哈喽,我是子牙。十余年技术生涯,一路披荆斩棘从技术小白到技术总监到JVM专家到创业。技术栈如汇编、C语言、C++、Windows内核、Linux内核。特别喜欢研...

    子牙老师
  • 值得保存的 synchronized 关键字总结

    本文是对 synchronized 关键字使用、底层原理、JDK1.6之后的底层优化以及和ReenTrantLock对比做的总结。如果没有学过 synchron...

    用户2164320
  • Java内存模型是什么,为什么要有Java内存模型,Java内存模型解决了什么问题等。。。

    本文中,有很多定义和说法,都是笔者自己理解后定义出来的。希望能够让读者可以对Java内存模型有更加清晰的认识。当然,如有偏颇,欢迎指正。

    全栈程序员站长

扫码关注腾讯云开发者

领取腾讯云代金券