并发编程----JMM模型,volatile,sychronized

JMM(java 内存 模型)

      JMM(java 内存模型)是一种规范,解决线程中数据不一致问题(可见性,原子性,一致性),主要解决缓存一致性问题。

     JMM模型图示:

     

JMM抽象模型

多线程情况JMM模型中会产生 原子性问题,可见性问题。

有序性问题是由 1、编译器指令重拍,2、处理器指令重排 3内存系统的重排序产生的。

JMM如何解决原子性,可见性,有序性问题

采用限制处理器的优化和使用内存屏障

原子性解决方案:sychronized(monitorenter/monitorexit)

可见性解决方案:volatile、sychronized、final

有序性解决方案:volatile、sychronized

volatile关键字

        轻量级的锁,通过汇编指令#lock解决可见性问题

        内存屏障解决重排序的问题

从cpu层面了解内存屏障

内存屏障的作用:防止指令重排,保证数据的可见性

cpu屏障类型:store barrier(写屏障)、load barrier(读屏障)、full barrier(全屏障)

store barrier (写屏障 storestore barrier)

强制在storestore内存屏障之前的所有指令先执行,发送缓存失效的信号。

所有在storestore内存屏障的指令之后的store指令,必须在storestore内存屏障之前的指令执行完之后再执行

load barrier 读屏障 loadloadbarrier 

 强制在loadload内存屏障之前的所有指令先执行,

所有在loadload内存屏障的指令之后的load指令,必须在loadload内存屏障之前的指令执行完之后再执行

full barrier  全屏障   store和load两者的结合就是fullbarrier

内存屏障解决的是顺序一致性问题,并不能解决缓存一致性问题,缓存一致性问题是由缓存锁即MESI完成的。每个cpu在读写数据时都会有自己的loadbuffer和storebuffer,在内存屏障之前会将该cpu自己的laodbuffer或storebuffer中的数据同步到主内存

编译器(JMM)层面如何解决指令重排问题? 

        通过volatile 关键字可以去取消编译器上的缓存和重排序。防止重排序导致可见性问题,保证编译器的优化不会影响到代码实际执行顺序。 源码中有ACC_VOLATITLE

      编译器层面将内存屏障分为四类

       loadloadbarrier      (例如  load1 loadload load2  表示load1的装载一定在load2之前  其他的内存屏障同理 )

      storestorebarrier 

      loadstorebarrier

      storeloadbarrier

内存屏障的功能:

1、确保指令重排序不会把内存屏障后面的指令排到内存屏障前面去,也不会把内存屏障前面的指令排到内存屏障后面去

2、强制对缓存的修改,会立即写到主内存里。

3、写操作会导致其他cpu的缓存无效

volatile的原子性

    volatile没有办法保证符合(如 i++)操作的原子性,可以保证单操作的原子性(如 stop = flase)

     i++ 实际上是三个操作

     多个线程同时执行

     首先getFiled   load   然后i.add  asgin   然后再去 putField store

     storeA storeB storeload loadA loadB  能保证 storeA  与 loadA的顺序 但是无法保证 storeB 插入执行的顺序

     在store 之后才会让其他可见,在store之前 没有办法改变

volatile的使用场景

使用场景:->线程的关闭

不加volatile 子线程无法结束,主线程改了stop的值,但是子线程没法得到stop为true,导致子线程无法结束

加了volatile,子线程可以结束

sychronized 

       解决 可见性,原子性,有序性

       sychronized  代码块锁   对象锁    类锁 

        sychronized,支持重入,非公平锁

      运行含有sychronized的代码时,jvm 实现的东西,只能从指令上去定位样实现的, javap -v  xxx.class   命令可以看到jvm 的指令

    1、为什么任何一个对象都可以成为锁

       任何一个对象都会在jvm层面上有一个OOP/oopDesc的对应,oopDesc中会存在一个对象头。对象头(Object Header)包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64个Bits,官方称它为“Mark Word”。

对象头的另外一部分是类型指针,即是对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

    2、sychronized 是如何实现锁的

    sychronized 锁的获取过程: 锁的获取过程是从  偏向锁---> 轻量级锁 ---->  重量级锁

     自旋锁 :

              设计初衷:1、线程的阻塞和唤醒对cpu开销大

                                2、对象锁的锁状态只会持续很短的时间,很短的时间去频繁阻塞和唤醒线程是非常不值得的

              实现:  在指定时间内,通过一个没有意义的for(;;)循环去获取锁。  for(;;){获取锁}

     偏向锁:

       大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁,如果测试失败,则需要再测试下 Mark Word中偏向锁的标识是否设置成 1(表示当前是偏向锁),如果没有设置,则使用 CAS 竞争锁,如果设置了,则尝试使用 CAS 将对象头的偏向锁指向当前线程。

       偏向锁的撤销:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态,如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
 

     轻量级锁  :

        轻量级锁加锁:线程在执行同步块之前, JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用 CAS 将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,锁标志的状态值变为”10”,Mark Word中存储的就是指向重量级(互斥量)的指针。

        轻量级锁解锁:轻量级解锁时,会使用原子的 CAS 操作来将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

      重量级锁:

    (性能比较低)要从操作系统层面进行线程切换,成本比较高。当运行的是一个重量级锁意味着当前的锁竞争是非常激烈的。

      重量级锁的运行过程:

      

重量级锁的实现原理:

      类对象有一个ObjectMonitor,每一个对象都有一个ObjectMonitor,类对象的ObjectMonitor每个线程获取的都一样。对象的ObjectMonitor是不同的,所以能够实现锁的作用域不同。所以类锁与对象锁是不会竞争的,因为ObjectMonitor不同。

        锁膨胀的过程:

        偏向锁,轻量级锁都是乐观锁,重量级锁是悲观锁。

        一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。

        轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

      wait 和notify:

      wait与notify的实现

         为什么 wait 和notify必须在sychronized中调用?

        wait功能:1、释放当前线程对象锁,2、使得当前线程进行阻塞。为了实现这两个功能必须获取当前对象的锁

        notify功能:唤醒谁?必须获取对象的锁   在哪里?  到对象的WaitSet里面取出一个线程加入EnterList中。notify不会释放锁,真正释放锁的是monitorexit

wait 与sleep的区别

       区别一:

       sleep是Thread类的方法,是线程用来 控制自身流程的,比如有一个要报时的线程,每一秒中打印出一个时间,那么我就需要在print方法前面加上一个sleep让自己每隔一秒执行一次。就像个闹钟一样。

       wait是Object类的方法,用来线程间的通信,主要是用走不同线程之间的调度的。

      区别二:

     关于锁的释放 ,调用sleep方法不会释放锁(sleep方法是线程的方法,线程是系统中最小的调度单位,不持有资源,不会释放锁)。

      调用wait方法会释放当前线程的锁。wait方法应该放在同步代码块中才有意义。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值