【并发编程】并发编程的基础

 

目录

硬件模型

CPU多级缓存

MESI -CPU缓存一致性协议

JAVA内存模型

8种操作

线程的安全性

1)原子性

   1、CAS(compare and swap) 的ABA问题

   2、synchronize

2)可见性

3)有序性


硬件模型

        学习并发之前,我们要先简单了解一下计算的硬件模型。

程序加载到主存中之后,先加载到CPU高速缓存中待命,甚至会加载到CPU寄存器当中,让CPU进行处理。主存的处理速度是远远不如CPU的,所以中间加了一个CPU高速缓存,配合CPU的速度,定时和主存同步数据(因为主存跟不上CPU的频率,经常等待主存),另外为了让处理器内部运算单元尽可能的充分利用,处理器还会对输入的代码进行乱序执行,但是保证结果是一致的。而寄存器是CPU内存的基础,每个CPU都会包含一系列的寄存器。CPU在寄存器中的处理速度要比在主存中效率高。

 

CPU多级缓存

推荐文章

我们把CPU比喻成一个大型加工总部,内存为部件存储大仓库,而缓存就是总部与大仓库之间的小仓库,离CPU较近的小仓库是一级缓存,其次依次为二级缓存和三级缓存,当加工总部需要加工某个成品时候需要很多部件,这个时候缓存就是把所需要的部件提前从内存调出,存储在小仓库内,当总部加工需要某个部件时候就可以直接从最近的小仓库提取,就不必大费周章去内存大仓库调取。

 

既然存在多个缓存区域,那么怎么保证缓存数据的一致性呢?

MESI -CPU缓存一致性协议

MESI(modified Exclusive shared or invalid)是一种广泛使用的支持回写策略的缓存一致性协议。推荐文章

 CPU缓存行使用4种状态进行标记,进而将数据进行同步或失效来保持多级缓存的一致。

状态转化图

        

M:被修改(modified)此数据被修改,但未同步到主存中被写回到主存中,状态变成独享(E)
E:独享(Exclusive)未被修改过,与主存一致的数据被读取时,变成共享(S)
S:共享(shared)被多个CPU缓存过,与主存一致被修改之后,其他CPU中的此缓存失效(I)
I:失效(Invalid)缓存无效 

 

JAVA内存模型

Java Memory Model,JMM是JVM规范定义的一个抽象的概念。他屏蔽了Java程序在不同硬件和OS对内存访问的差异,实现了在不同平台上都能达到内存访问的一致性。主要是围绕如何处理原子性、可见性、顺序性这三个特征来设计的。

 

JVM的内存模型不同于JMM内存模型。JMM主要是为了定义程序中的共享变量的访问规则,即在虚拟机将变量存储(或取出)到主内存的底层细节。注意,像局部变量和方法参数这种线程私有的变量不包含在内。(可以把JMM的范围比作JVM中的堆存放的数据规则)。参考文章

 

那Java内存模型到底是什么呢?

                                           

说明:JVM规定共享变量都在主内存中产生,而JVM的每个线程都有自己的工作内存,是私有的,每个线程对共享变量修改的时候,都是在工作内存中对副本进行修改,然后经过JMM控制命令进而同步到主内存中来保持数据的一致性,而多个线程操作的这些同步变量都是经过主内存来进行传递同步的,有8种操作命令来完成。每种操作必须是原子性的

 

8种操作

lock(锁定)锁定主内存中的变量,只能供一个线程用
unlock(解锁)释放主内存中的变量,其他线程才可以上锁
read(读取)把主内存的变量传输到线程的工作空间
load(载入)把read到工作空间的值,放到变量副本中
use(使用)把工作内存中的变量传递给执行引擎
assign(赋值)把执行引擎执行的结果赋值给变量副本
store(存储)把工作内存中的变量传递给主内存
write(写入)吧store到主内存中的变量值放到变量中

图例表示

                  

 

这几种操作之间也有一定的同步规则(执行顺序)

  1. 如果要把一个变量从主内存传输到工作内存,那就要顺序的执行read和load操作,如果要把一个变量从工作内存回写到主内存,就要顺序的执行store和write操作。对于普通变量,虚拟机只是要求顺序的执行,并没有要求连续的执行
  2. 不允许read和load、store和write操作之一单独出现
  3. 不允许一个线程丢弃最近的assign操作,也就是不允许线程在自己的工作线程中修改了变量的值却不同步/回写到主内存
  4. 不允许一个线程回写没有修改的变量到主内存
  5. 变量只能在主内存中产生,不允许在工作内存中直接使用一个未被初始化的变量,也就是没有执行load或者assign操作。也就是说在执行use、store之前必须对相同的变量执行了load、assign操作
  6. 一个变量在同一时刻只能被一个线程对其进行lock操作
  7. 对变量执行lock操作,就会清空工作空间该变量的值
  8. 不允许对没有lock的变量执行unlock操作
  9. 对一个变量执行unlock之前,必须先把变量同步回主内存中

但是对于volatile修饰的变量有一些特殊规则

线程的安全性

               首先,多线程的意义是什么呢?目的在于最大限度的利用CPU资源。因为CPU速度太快了,但是我们IO速度,网络速度,数据库连接等跟CPU的速度相比较来说差太远,于是用多线程来减少CPU的空闲时间。但是在某个时刻内,CPU实际上只能执行一个线程,外部看起来是用了多线程,其实是操作系统对进程线程进行了管理,分配每个进程的时间,而每个进程内,程序自己处理线程的时间分配。就这样,多个线程来回进行切换调度。

               凡事有利有弊,那么多线程自然也带来了一些缺憾需要弥补,那就是线程的安全性。经常谈的是原子性,可见性和有序性。

      那么如何保证线程的安全性呢?

1)原子性

    

    涉及问题

   1、CAS(compare and swap) 的ABA问题

         ABA问题:如果线程A对变量a初次读取的时候是1,并且在准备赋值的时候检查到它仍然是1,那这中间很可能出现一种情况是线程B刚好把变量a从1设置成2,又再次设置成1,所以线程A看到的初始值1很可能已经被修改过,会有问题。

       如何解决?可以加版本号,推荐文章

 

   2、synchronize

         是通过底层系统指令来实现的,JDK1.6对其进行了优化,在其之前synchronize的性能很差,是重量级的锁,不如ReentranLock。但是1.6之后,引入了偏向锁等,提高了性能,在一般情况下还是建议使用synchronize。如果需要利用到Lock特性的时候,再使用Lock。

         synchronize的作用有:①线程互斥访问同步代码  ②保证共享保证变量可见  ③有效解决重排序问题

         synchronize主要解决的是执行控制的问题,实现线程间的互斥。另外他和volatile类似,也可以保证变量可见性。

        下面我们详细看下synchronize的原理:

         它是基于Monitor对象来实现的,Monitor是一种同步工具,可以把对的的方法互斥执行(只能有一个允许执行),Java中,每个对象都带了一把Monitor的监视器,当对象加了synchronize关键词之后,会调用Monitorenter和monitorexit两个字节码指令,表示这个线程对此对象上锁,监视器的值+1,线程使用完成之后,释放锁,-1。如果是同样的线程再次访问此对象,则可重入。

推荐博客:点我   点我

每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor record关联(对象头的MarkWord中的LockWord指向monitor record的起始地址),同时monitor record中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

对象成为锁(被锁住)后,对象头里的mark word字段指向线程栈的Monitor Record对象(实现锁与线程的关联)
通过锁对象的mark word字段可以找到持有锁的线程(线程栈保存锁对象的对象头信息的Monitor Record数据结构)

   JDK1.6之后的优化部分:

   锁的状态有4种:无状态锁,偏向锁,轻量级锁,重量级锁,依次单向升级,不会降级。

   1、引入偏向锁

        目的:为了减少同一个线程获取锁(CAS操作,耗时)的代价而引入偏向锁。如果一个线程拿到锁,可重入(无需任何操作)省去了大量有关锁申请的操作,提高性能。

         适用于长时间单一线程的访问,或者线程交替同步执行的情况。

         如果多个线程同时来抢,就会变成轻量级锁。

    2、轻量级锁

          目的:减少锁切换状态、减少阻塞线程的概率。

          多个线程同时访问,但竞争不多的情况。

          多次CAS,如果没有得到锁,就自旋等待,会消耗CPU,继续拿锁。如果自旋超过一定次数,就变成重量级锁。

    3、重量级锁

          拿不到锁,不会自旋,不会消耗CPU。

    synchronize等待环境机制

            A线程上锁a对象时,会上锁,此时B线程来的话,会排队等待A线程,A线程需要通知B线程去再次拿锁。主要通过notify()、notifyAll()、wait()方法来实现。注意,这些一定是在同步代码块中才能执行这些操作,因为这些方法的前提是此对象上了锁,才需要被释放,等待,通知。否则会爆出IllegalMonitorStateException异常。

是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,在前面的分析中,我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字可以获取 monitor ,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因

2)可见性

导致共享变量不可见的原因:

①线程交叉执行

②重排序

③共享变量更新之后的值没有在及时在主存和工作空间中及时更新

关键词:synchronize、volatile

1. sychronize是如何保证可见的呢?

     JVM规定,线程解锁前,必须把共享变量的最新值刷新到主内存

     线程加锁时,将情况工作内存中共享变量的值,从而使用共享变量时需要中主存中重新读取最新的值。

2.volatile如何保证可见?

     通过内存屏障和禁止重排序优化来实现

     内存屏障:对volatile变量写操作时,会在写操作后加一条store屏障指令,将本地内存中的共享变量值刷新到主内存中。

                       读操作时,会在之前加入一条load屏障指令,从主内存中读取共享变量。(类似synchronize)

     重排序:

                禁止写重排序

                                         

 

           禁止读重排序

                       

3)有序性

        关于重排序问题:编译器在将Java代码编译成字节码的时候可能会对代码进行重排序,而CPU在执行机器指令的时候也可能会对其指令进行重排序。虽然单一线程中重排序不会影响到程序的执行结果,但是可能会在多线程执行的情况下,因为重排序出现问题,所以,我们需要用一些关键词来控制线程的有序性。

关键词:synchronize、volatile、Lock

volatile是通过禁止指令重排来实现有序性。

synchronize不能禁止,他是通过控制线程的执行顺序来达到有序性。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值