java并发系列二(深入!!!理解synchronized,volatile)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/lc138544/article/details/84447592

一,synchronized详解

这个关键字大家想必是相当熟悉了,它是一个比较重量级的锁,主要有两层含义,一个是互斥性,一个是可见性。三种用法:1,修饰普通方法2,修饰静态方法3,修饰代码块
这里有一点需要注意,普通方法要拿到当前实例的锁,静态方法要拿到当前class对象的锁。
重点来了!!!
synchronized实现原理!!!
(这块内容晦涩难懂,主要是参考的这篇博文https://blog.csdn.net/javazejian/article/details/72828483)

java虚拟机中的同步是基于Monitor对象实现的。其中同步代码块通过指令monitorenter和monitorexit来实现。同步方法是由方法调用指令读取ACC_SYNCHRONIZED 标志来实现的。
jvm中,对象在内存中的布局如下
在这里插入图片描述

这里只讲一下对象头:它是实现synchronized对象锁的基础。jvm中采用两个字来存储对象头,MarkWord和Class Metadata Address。
MarkWord:存储对象的hashCode和锁信息等
Class Metadata Address:类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。
其中Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等。
重点!!重量级锁的锁标记位为10,指针指向的是monitor对象的起始地址。每个对象都有一个monitor与之关联,当一个monitor被一个线程持有后,它就处于锁定状态。在java虚拟机中,monitor是由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 ;
  }
--------------------- 

ObjectMonitor有两个队列_WaitSet 和 _EntryList。当多个线程同时访问一段代码时,首先会进入_EntryList。当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。
在这里插入图片描述
接下来再简单说一下同步代码块的原理!!!(重要)
monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor重入时计数器的值也会加 1。倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。

 monitorenter  //进入同步方法
//..........省略其他  
 monitorexit   //退出同步方法

同步方法的原理
方法级的同步,无需通过字节码指令来控制。jvm可以从方法常量池的方法表结构中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。

偏向锁
如果一个线程线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构。当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。

轻量级锁
倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”。,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

自旋锁
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失。
自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

二,volatile关键字详解

这个关键字大家肯定也非常熟悉了,就是一种比synchronized关键字更轻量级的同步机制。读取volatile类型的变量总会返回最新写入的值。有一点需要注意,它不具有互斥性。
现在开始重点讲原理
上图
在这里插入图片描述
这个图应该很好理解啦
这里只讲一下缓存一致性的核心思想:当cpu写数据时,如果发现该变量是共享变量,就会通知其他cpu将该变量的缓存行设为无效。因此当其他cpu重新读取这个变量时,就会发现自己的这个变量无效,就会从主内存中重新读取。
下面这段话摘自《深入理解Java虚拟机》:

“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

2)它会强制将对缓存的修改操作立即写入主存;

3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

展开阅读全文

没有更多推荐了,返回首页