并发编程专题二

转载: https://blog.csdn.net/qq_31989841/article/details/99292859
  • JMM是什么
  • 八大原子操作
  • 可见性/原子性/有序性
  • volatile关键字
  • synchronized关键字
  • 1. JMM是什么

    在这里插入图片描述
    其实就是第一篇文章里分析的那些过程。

    并发编程专题(一)

    Java Memory Model,简称JMM,把它抽象成一种规范,用工作内存主内存这两个概念。在JMM中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,而工作内存数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。
    主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存完成。

    在这里插入图片描述

    2. 八大原子操作

    我们已经清楚,每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,线程与主内存中的变量操作必须通过工作内存间接完成,主要过程是将变量从主内存拷贝到每个线程各自的工作内存空间,然后对变量进行操作,操作完成后在某个不确定时刻再将变量写回主内存。这里再说具体一点,八大原子操作。

    (1)lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态
    (2)unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
    (3)read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
    (4)load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
    (5)use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎 (6)assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
    (7)store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
    (8)write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中在这里插入图片描述
    如果要把一个变量从主内存中复制到工作内存中,就需要按顺序地执行read和load操作, 如果把变量从工作内存中同步到主内存中,就需要按顺序地执行store和write操作。但Java内 存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。

    3. 可见性/原子性/有序性

    可见性/原子性/有序性是并发编程的三大特性。前面1,2点相当于对第一篇文章做了个复习和补充,接下来分析一下实实在在的代码。

    (1)可见性问题

    在这里插入图片描述在这里插入图片描述
    就这么一个类,线程A执行refresh方法,把标志位刷新;线程B执行load方法,用标志位空跑,先运行线程B再运行线程A。根据前面讲的JMM,你觉得结果是什么?

    在这里插入图片描述
    结果就是线程A尽管把标志位刷新了,线程B还是在空跑。这就是缓存不一致带来的可见性问题。
    在这里插入图片描述
    你如果懂了JMM过程,就应该能懂为什么是这个结果。线程A尽管把标志位刷新了,刷新的是自己工作内存中的值,(在某一个时刻会写回主存),但是线程B还是不知道,还用的是自己工作内存中的false值。

    (2)原子性问题

    在这里插入图片描述
    开10个线程,每个线程把counter加1000,问你counter最后是多少?答案是<=10000,这个不画图解释了,跟上面一样的。

    (3)有序性问题

    在这里插入图片描述
    这就是说,为了优化,cpu可能会指令重排,但是重排要求不能够改变单线程程序最终结果。在这里插入图片描述
    看这个例子,是一个指令重排在多线程环境下可能带来的错误。

    Java并发-懒汉式单例设计模式加volatile的原因

    这篇文章谈到了volatile关键字可以禁止指令重排,保证有序性。volatile还可以保证可见性。但是volatile不能够保证原子性。接下来重点谈下volatile关键字。

    4. volatile关键字

    volatile关键字就是第一篇文章说的缓存一致性协议的一种实现方式,缓存一致性协议就是volatile关键字的方法论。

    (1)保证可见性

    volatile关键字保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值立即被其他的线程看到,即修改的值立即更新到主存中,当其他线程需要读取时,它会去内存中读取新值。(synchronized和Lock也可以保证可见性,因为它们可以保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到内存中。)

    那我们再来分析下。在上面的例子中,如果initFlag用volatile修饰,线程A把标志位刷新了,true值立即更新到主存中,线程B工作内存中的值同时失效。那线程B再想读,就只能从主存中去取,于是能正常停止。

    (2)保证有序性

    先说说概念。举个例子,如果变量i用volatile修饰:volatile int i=0;
    i=10; //这叫volatile写
    另一个变量=i; //这叫volatile读
    不用volatile修饰的变量的读写叫普通读写
    在这里插入图片描述
    代码块1
    i=10; //volatile写
    代码块2
    另一个变量=i; //volatile读
    代码块3

    那么规定,代码块1必须在volatile写执行前执行,不能指令重排(看表最后一列三个No);代码块3必须在volatile读执行后执行,不能指令重排(看表第二行三个No);代码块2分情况讨论下,如果代码块2只是普通读写,可以指令重排,可以变成:

    代码块1
    代码块2
    i=10; //volatile写
    另一个变量=i; //volatile读
    代码块3

    或是

    代码块1
    i=10; //volatile写
    另一个变量=i; //volatile读
    代码块2
    代码块3

    但如果代码块2是volatile读/写就不能重排。
    这只是结论,如果你还对原理感兴趣,搜索内存屏障多了解下也是好的。

    5. synchronized关键字

    volatile关键字也没有保证原子性。
    实际上,所有的并发模式在解决线程安全问题时,采用的方案都是序列化(一个一个来)访问临界资源(多个线程同时访问同一个共享、可变资源的情况, 这个资源我们称之其为临界资源)。即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。synchronized关键字是同步互斥访问方法之一(还有Lock,之后章节会手撕源码)。
    怎么使用这些基础就不讲了,直接讲原理。synchronized关键字被编译成字节码后会被翻译成monitorenter和monitorexit两条指令分别在同步块逻辑代码的起始位置与结束位置。

    在这里插入图片描述在这里插入图片描述

    在这里插入图片描述
    那么这个是干嘛的呢?让每个线程在进入同步代码块或是同步方法时,去这个同步对象的对象头里去找monitor指针,目的是找到monitor对象(每个对象在创建时都会有一个与它对应的monitor对象),看里面的信息,判断可不可以申请到对象的锁。

    (1)对象头是什么?——了解对象的内存布局

    HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头 (Header)、实例数据(Instance Data)和对齐填充(Padding)。
    对象头:比如 hash码,对象所属的年龄,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象才有)等
    实例数据:即创建对象时,对象中成员变量,方法等
    对齐填充:对象的大小必须是8字节的整数倍

    在这里插入图片描述
    主要关注一下对象头,上面这个图描述的不详细。
    实际上,对象的锁状态不同,对象头会存放什么东西也会动态变化(这里要刷新你们的认知了,一个对象就算被synchronized括起来,某个线程进入的时候,也不是立马变成重量级锁状态,而是存在一个锁的膨胀升级的过程,之后会说)。在32位的HotSpot虚拟机中对象无锁状态下,对象头的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志位,1Bit固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示。
    在这里插入图片描述
    看重量级锁这一行,也就是说对象如果升级到重量级锁状态,对象头里存放着monitor指针,可以找到monitor对象。
    下面这个就是monitor对象里的内容,_owner表示持有它的线程,count表示重入次数。
    在这里插入图片描述
    如果没有线程在占用,可以申请到锁。如果有线程占用,这个线程进同步队列等。总结一下,就是下面这个过程。
    在这里插入图片描述

    (2)锁的膨胀升级过程

    前面提过一句,“一个对象就算被synchronized括起来,某个线程进入的时候,也不是立马变成重量级锁状态,而是存在一个锁的膨胀升级的过程”。为什么要这个过程?很简单,避免消耗。
    锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。假设现在有一同步代码块:synchronized(object){ //代码块 }
    下面为锁的升级全过程:
    (1)无锁:当还没有任何线程进入的时候object就是无锁状态。
    在这里插入图片描述
    (2)偏向锁:如果线程1申请锁,获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。如果线程2想要访问同步代码块了,会尝试修改一下对象头里面的线程id为自己(想让这个对象偏向自己),可能修改成功(线程1已经释放锁);可能失败(线程1还占有着锁),这时线程2向虚拟机发出申请说,让线程1这次时间片用完了停一停,我要用了。
    在这里插入图片描述这里所谓的安全点就是时间片用完了停留的位置,如果线程1还停留在同步代码块里,升级为轻量级锁。如果线程1在同步代码块外,保持偏向锁级别,使它偏向线程2.
    在这里插入图片描述(3)轻量级锁:这时线程1,线程2都把对象头复制一份到自己的栈空间里面去,这个空间叫做lockrecord,线程1让对象头里的指针指向自己的lockrecord,然后从安全点继续执行。同时线程2自旋尝试把这个指针指向自己栈里面的lockrecord,一定次数内成功了,获得轻量级锁。没成功,升级成重量级锁。

    在这里插入图片描述(4)重量级锁:对象头升级成重量级锁的内容——monitor指针。线程2阻塞,线程1做完有个轻量级锁解锁过程,主要是看对象头的指针指向的还是不是自己的lockrecord,但是现在已经是重量级锁,它就知道有人升级了,在等着申请锁了,于是唤醒线程2

    在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值