一   volatile 

volatile 变量修饰的共享变量进行写操作时转换成汇编代码时会添加LOCK前缀、 Lock前缀指令再多核处理器下会引发两件事情

 1、将当前处理器缓存行的数据协会到系统内存

 2、这个写回内存的操作会使其他cpu里面缓存了该内存地址的数据无效

 

二 synchronized 原理与应用

对于普通同步方法 锁是当前实例对象

对于静态同步方法 锁是当前类的Class对象

对于同步方法块 锁是synchronized括号内的对象

 

注意:

使用synchronized修饰非静态方法或者使用synchronized修饰代码块时指定的为实例对象时,同一个类的不同对象拥有自己的锁,因此不会相互阻塞。

使用synchronized修饰类和对象时,由于类对象和实例对象分别拥有自己的监视器锁,因此不会相互阻塞。

使用使用synchronized修饰实例对象时,如果一个线程正在访问实例对象的一个synchronized方法时,其它线程不仅不能访问该synchronized方法,该对象的其它synchronized方法也不能访问,因为一个对象只有一个监视器锁对象,但是其它线程可以访问该对象的非synchronized方法。

线程A访问实例对象的非static synchronized方法时,线程B也可以同时访问实例对象的static synchronized方法,因为前者获取的是实例对象的监视器锁,而后者获取的是类对象的监视器锁,两者不存在互斥关系。

 

2.对象头

    对象存放在堆内存中、对象大致分为三个部分 对象头、实例变量、填充字节。

  •  对象头的zhuyao是由MarkWord和Class Point(类型指针)组成,其中Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据。如果对象是数组对象,那么对象头占用3个字宽(Word),如果对象是非数组对象,那么对象头占用2个字宽。(1word = 2 Byte = 16 bit)

  • 实例变量存储的是对象的属性信息,包括父类的属性信息,按照4字节对齐

  • 填充字符,因为虚拟机要求对象字节必须是8字节的整数倍,填充字符就是用于凑齐这个整数倍的

在32位的虚拟机中:java并发机制底层实现原理

在64位的虚拟机中:

java并发机制底层实现原理

    

 

synchronized在JVM里面的实现原理

  同步代码块: 使用Monitorenter 和Monitorexit指令实现 

        Monitorenter插入到同步代码开始的位置 Monitorexit 插入到方法结束处或者异常处,jvm要保证每个Monitorenter必须有对应的Monitorexit与之配对。

    修饰方法: Synchronized方法同步不再是通过插入monitorentry和monitorexit指令实现,而是由方法调用指令来读取运行时常量池中的ACC_SYNCHRONIZED标志隐式实现的,如果方法表结构(method_info Structure)中的ACC_SYNCHRONIZED标志被设置,那么线程在执行方法前会先去获取对象的monitor对象,如果获取成功则执行方法代码,执行完毕后释放monitor对象,如果monitor对象已经被其它线程获取,那么当前线程被阻塞

 

 

三 锁的升级优化

 

锁的4中状态 级别从低到高无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。锁可以升级但是不能降级、意味着偏向锁升级成轻量级锁后不能降级成偏向锁。

        1 偏向锁:  HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁

        当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

     偏向锁的撤销

      需要等待全局安全点(在这个时间点上没有在执行的字节码),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否有活着,如果线程不处于活动状态、则将对象头设置成无锁装填;如果线程仍然活着、拥有偏向锁的栈会被执行、遍历偏向对象的锁记录、栈中的锁记录和对象头的MarkWord要么重新偏向于其他线程,要么恢复到无锁活着标记对象不适合作为偏向锁、最后唤醒暂停的线程;

     关闭偏向锁

   偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,如果不想有这个延迟,那么可以使用                   -XX:BiasedLockingStartUpDelay=0;如果不想要偏向锁,那么可以通过-XX:-UseBiasedLocking = false来设置;

       2 轻量级锁

            为什么要引入轻量级锁?

            轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。

         升级为重量级锁

 线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),然后使用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址;

        如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。

        但是如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

 

java并发机制底层实现原理

 

 

 

四  原子操作的实现原理

    原子本意是:不能被进一步分割的最小粒子 、原子操作意为:不可被中断的一个或一系列操作。

1.术语定义

java并发机制底层实现原理

 

     2.处理器实现原子操作

       处理器使用总线锁和缓存锁定两个机制来保证复杂内存操作的原子性

      1.总线锁

 

      2.缓存锁

      有两种情况下处理器不会使用缓存锁定

         1.当操作的数据不能被缓存在处理器内部或操作的数据跨多个缓存行

         2.有些处理器不支持缓存锁定

 

        3 .java实现原子操作

          1.使用循环cas实现原子操作

          2.使用锁机制实现原子操作

 

参考 

《java并发编程的艺术》

博客  https://blog.csdn.net/tongdanping/article/details/79647337

 

相关文章: