> 文档中心 > java并发编程艺术第二章读书笔记

java并发编程艺术第二章读书笔记


 

2.1volatile的应用

在多线程并发编程中synchronized和volatile都扮演着重要的角色,volatile是轻量级synchronized(执行成本更低),它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程 修改一个共享变量时,另外一个线程能读到这个修改的值。我们也常说synchronized是读写安全的,volatile是读安全的。

        synchronized不能修饰变量,volatile可以!

我简单介绍一下这几个术语:1、内存屏障:简单理解成对变量做一个开关,有线程访问就关上开关,其他线程不可访问。2、缓冲行:缓存最小的存储单位3、原子操作:这个没啥好说的,包括数据库、spring、操作系统说的原子操作都是一个意思,要么全做要么全不做4、尽可能填满缓冲行,让缓冲行属于自己5、缓存命中:直接从缓存中取操作数,这样更快6、写命中:先写回缓存,在写回去内存,以减少占用总线的时间7、缓存刷新回内存,发现内存中这部分没了。

volatile是如何来保证可见性的呢?有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码,通过查IA-32架 构软件开发者手册可知,Lock前缀的指令在多核处理器下会引发了两件事情

1)将当前处理器缓存行的数据写回到系统内存。

2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部 缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的 变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据 写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操 作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一 致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

我举个例子:假设两个线程t1、t2对一个volatile修饰的变量i做出操作,目前i=0;

t1对i赋值10;这个结果暂时存在cpu的缓存中,于此同时t2对i赋值99,结果也保存在t2的cpu的缓存中,此时t1的cpu将i的值10刷新回内存,这时,t2的cpu缓存里的i=99就会失效,想赋值就得重新从内存中读到i=99

2.2 synchronized的实现原理与应用

在多线程并发编程中synchronized一直是元老级角色,很多人都会称呼它为重量级锁。但 是,随着Java SE 1.6对synchronized进行了各种优化之后,有些情况下它就并不那么重了。本文详细介绍Java SE 1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。

先来看下利用synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现 为以下3种形式。

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

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

·对于同步方法块,锁是Synchonized括号里配置的对象。

当一个线程试访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。

JVM基于进入和退出Monitor对 象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter 和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有 详细说明。但是,方法的同步同样可以使用这两个指令来实现。

monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结 束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有 一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter 指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

2.2.1 Java对象头

synchronized用的锁是存在Java对象头里的。如果对象是数组类型,则虚拟机用3个字宽存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽 等于4字节

2.2.2 锁的升级与对比

 2.3 原子操作的实现原理

原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意 为“不可被中断的一个或一系列操作”。首先了解一下简单的cpu术语以便理解。

关于cas问题,这篇文章写的我认为不错,包括cas的aba问题也有介绍强烈建议看这篇(35条消息) 什么是CAS机制?_1*null的博客-CSDN博客_cas机制

关于cpu流水线,假设现在有三个一样的任务,都分为10个步骤完成,流水线的意思可以不用等一个任务的十个步骤都走完在进行下一个任务,而是同时进行三个任务的第一个步骤、同时进行三个任务的第二个步骤···已保证cpu合理的使用效率。

32位IA-32处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操 作。首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存中读取或者写 入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节 的内存地址。处理器提供总线锁定和缓存锁定两个机制来保证复杂 内存操作的原子性

(1)使用总线锁保证原子性

第一个机制是通过总线锁保证原子性。如果多个处理器同时对共享变量进行读改写操作 (i++就是经典的读改写操作),那么共享变量就会被多个处理器同时进行操作,这样读改写操 作就不是原子的,操作完之后共享变量的值会和期望的不一致。

举个例子,如果i=1,我们进行 两次i++操作,我们期望的结果是3,但是有可能结果是2,如图2-3所示。 图2-3 结果对比 原因可能是多个处理器同时从各自的缓存中读取变量i,分别进行加1操作,然后分别写入 系统内存中。

那么,想要保证读改写共享变量的操作是原子的,就必须保证CPU1读改写共享 变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。 处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个 LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该 处理器可以独占共享内存。

我姑且按自己理解解释一下,假设现在有两个cpu1和cpu2,并发对i++进行操作,照理说 i应该等于3,假设此时两个cpu的缓存的i都是i=1,这时cpu1动手快了点,先完成了i++,此时i=2,我们都知道,cpu往内存里刷新数据是走总线的,cpu1完成++操作后,用一个lock指令锁定了总线,这条总线只允许cpu1往里刷新数据,cpu2想继续他的i++操作,就得等总线lock释放,从新读到内存里的i=2,再做i++。

(2)使用缓存锁保证原子性

第二个机制是通过缓存锁定来保证原子性。在同一时刻,我们只需保证对某个内存地址 的操作是原子性即可,但总线锁定把CPU和内存之间的通信锁住了,这使得锁定期间,其他处 理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,目前处理器在某些场合下 使用缓存锁定代替总线锁定来进行优化。

所谓“缓存锁定”是指内存区域如果被缓存在处理器的缓存 行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声 言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子 性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处 理器回写已被锁定的缓存行的数据时

我对这块的理解是,加入cpu1的缓存中i=2的数据王内存刷新,cpu2的缓存中这个i的缓存就失效(缓存一致性)

但是有两种情况下处理器不会使用缓存锁定。 第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行 (cache line)时,则处理器会调用总线锁定。 第二种情况是:有些处理器不支持缓存锁定。对于Intel 486和Pentium处理器,就算锁定的 内存区域在处理器的缓存行中也会调用总线锁定。