synchronized底层原理详解


synchronized底层原理详解#

一、特性##

  1. 原子性:操作整体要么全部完成,要么全部未完成。就是为了保证数据一致,线程安全。

  2. 有序性:程序的执行顺序按照代码的顺序执行。一般情况下,虚拟机为了提高执行效率,会对代码进行指令重排序,运行的顺序可能和代码的顺序不一致,结果不变。单线程不会出现问题,多线程有可能出现问题。

    深入理解Java虚拟机中有这么一句话:

    Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”( Within-Thread As-If-Serial Semantics),后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象

    理解:

    怎么在一个线程中?

    用户的指令都是在线程中执行的。指令执行在哪个线程里,就是“在”哪一个线程。

    什么叫观察?

    线程的运行通常没有办法直接观察到,一般只能观察到线程执行的后果,比如内存的改变。于是观察,多数情况下是指去读取被修改了内存的值。

    在一个线程中观察另一个

    在一个线程中执行的指令,去读取另一个线程修改的变量的值。
    所谓无序,就是说读取线程中读取到的变量值发生改变的顺序,和修改线程中修改变量的顺序,不一定一致。

  3. 可见性:变量的修改对所有的线程都是可见的。

    理解:

    synchronized对一个对象加锁,这时其他线程都无法操作。当前线程释放锁之后,其他线程才能获取到synchronized里的内容。

二、synchronized和ReentrantLock的区别##

  1. 使用来说,synchronized是Java的关键字,对象只有在同步块或者同步方法中才能调用wait/notify方法。ReentrantLock是JDK1.5之后提供的API层面的锁,需要主动创建,配合condition的await/signal使用
  2. ReentrantLock比较灵活,可以尝试获取锁、可以锁多个条件、可以中断等
  3. ReentrantLock必须手动使用调用获取锁和释放锁的方法,synchronized由系统调用
  4. ReentrantLock只能用于锁代码块,而synchronized可以修饰静态方法、实例方法和代码块
  5. 性能上说,ReentrantLock略高于synchronized.JDK6及之后,synchronized被优化为无锁、偏向锁、轻量级锁、重量级锁和GC标记等状态,在升级为重量级锁之前,性能还是很好地。
  6. synchronized是悲观锁、可重入锁、非公平锁,ReentrantLock是乐观锁、可重入锁,可设置为公平锁或非公平锁。
  7. 锁的对象来说,synchronized锁的是对象,ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。
  8. 锁的实现来说,synchronized是在软件层面依赖JVM实现,而j.u.c.Lock是在硬件层面依赖特殊的CPU指令实现。

三、底层原理

  1. synchronized不论是修饰静态方法、实例方法或者是代码块,最后锁住的要么是实例化后的对象,要么是一个类。对于修饰一个(静态/实例)方法时,JVM会在字节码层面给该方法打上一个ACC_SYNCHRONIZE标识,当有线程访问这个方法时,都会尝试去获取对象的objectMonitor对象锁,得到锁的线程才能继续访问该方法。修饰代码块时,JVM会在字节码层面给方法块入口处加monitorenter,出口处添加monitorexit标识,一般出口有两个,正常出口和异常出口,所以一般1个monitorenter对应2个monitorexit。线程执行到monitorenter处就需要尝试获取objectMonitor对象锁,获取不到就会一直阻塞,获取到了才能继续运行。

     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 ;
     }
    
  2. 在JDK6之后,锁被优化为无锁、偏向锁、轻量级锁和重量级锁。在编译过程中有锁粗化,锁消除,在运行时有锁升级。

    1. 锁粗化:如果虚拟机探测到有一系列的连续操作都对同一个对象加锁,甚至加锁操作出现在循环中,那么将会把加锁同步范围扩展到整个操作的外部,这就是锁粗化。
    2. 锁消除:经过逃逸分析后,发现同步代码块不可能存在共享数据竞争的情况,那么就会将锁消除。逃逸分析,主要是分析对象的动态作用范围,比如在一个方法里一个对象创建后,在调用外部方法时,该对象作为参数传递到其他方法中,成为方法逃逸;当被其他线程访问,如赋值给其他线程中的实例变量,则成为线程逃逸。
    3. 锁升级:JD6之后分为无锁,偏向锁,轻量级锁,重量级锁。其中偏向锁->轻量级锁->重量级锁的升级过程不可逆。

    一句话概括偏向锁、轻量级锁、重量级锁

    偏向锁:当一个线程第一次获取到锁之后,再次申请就可以直接取到锁

    轻量级锁:没有多线程竞争,但有多个线程交替执行

    重量级锁:有多线程竞争,线程获取不到锁进入阻塞状态

    Java对象的内存结构在64位操作系统中,占16个字节:分为对象头、实例数据、对齐填充

    对象头占12个字节,实例数据+对齐填充占4个字节,实例数据如果不足4个字节,才会有对齐填充

    对象头分markword和classAddressMethod,其中markword占8个字节

    avatar

    锁升级过程:

    无锁升级为偏向锁

    1. 线程访问同步代码块,判断锁标识位(01)
    2. 判断是否偏向锁
    3. 否,CAS操作替换线程ID
    4. 成功,获得偏向锁

    偏向锁升级为轻量级锁

    1. 线程访问同步代码块,判断锁标识位(01)
    2. 判断是否偏向锁
    3. 是,检查对象头的markword中记录的是否是当前线程ID
    4. 是,获得偏向锁
    5. 不是,CAS操作替换线程ID
    6. 成功,获取偏向锁
    7. 失败,线程进入阻塞状态,等待原持有线程到达安全点
    8. 原持有线程到达安全点,检查线程状态
    9. 已退出同步代码块,释放偏向锁
    10. 未退出代码块,升级为偏向锁,在原持有线程的栈中分配lock record(锁记录),拷贝对象头中的markword到lock record中,对象头中的markword修改为指向线程中锁记录的指针,升级成功
    11. 唤醒线程继续执行

    轻量级锁升级为重量级锁

    1. 线程访问同步代码块,判断锁标识位(00)
    2. 判断是否轻量级锁
    3. 是,当前线程的栈中分配lock record
    4. 拷贝对象头中的markword到lock record中
    5. CAS操作尝试获取将对象头中的锁记录指针指向当前线程的锁记录
    6. 成功,当前线程得到轻量级锁
    7. 执行代码块
    8. 开始轻量级锁解锁
    9. CAS操作,判断对象头的锁记录指针是否仍指向当前线程锁记录,拷贝在当前线程锁记录的mark word信息与当前线程的锁记录指针是否一致
    10. 两个条件都一致,释放锁
    11. 不一致,释放锁(锁已经升级为重量级锁了),唤醒其他线程
    12. 5失败,自旋尝试5、
    13. 自旋过程中成功了,执行6,7,8,9,10,11
    14. 自旋一定次数仍然失败,升级为重量级锁


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM