JAVA多线程杂学4-2018年10月28日

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/attack_breast/article/details/83474953

volatile的应用

在多线程并发编程中synchronized和volatile都扮演着重要的角色,volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。

注意点

①确保某一线程修改后所有线程能用修改后的结果去操作

②修饰变量

术语

①内存屏障:是一组CPU处理器指令,用于实现对内存操作顺序的限制。(说白了就是CPU操作内存顺序)

②缓冲行:缓存中可以分配的最小存储单位。CPU处理器填写缓冲线时会加载整个缓冲线,需要使用多个主内存读周期。(缓存最小存储单位)

③原子操作:不可中断的一个或者一系列操作。

④缓存行填充:当CPU处理器识别到从内存中读取操作数是可以缓存时,CPU处理器读取整个缓存行到适当的缓存(L1/2/3或者所有)(当操作数可以缓存到CPU高速缓存时,则一次性读取并缓存整个缓存行)

⑤缓存命中:如果进行高速缓冲行填充操作的内存位置仍然是下次CPU处理器访问的地址时,CPU处理器从CPU高速缓存中读取操作数而不是从内存读取。(CPU从CPU高速缓存读取而不是从内存读取的行为)

⑥写命中:当CPU处理器将操作数写回到一个内存缓冲区域时,CPU首先会检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行则CPU处理器将这个操作数写回到缓存而不是写回到内存(CPU写入CPU高速缓存而不是写入内存的行为)

⑦写缺失:一个有效缓存行被写入到不存在的内存区域(无法从CPU高速缓存回写到内存的行为)

整体流程

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

Synchonized

synchronized用的锁是存在Java对象头里的。32位JVM中JAVA对象头中的Mark Word结构如下图:

64位JVM中JAVA对象头中的Mark Word结构如下图:

32位JVM在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下4种数据:

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率

 

偏向锁

经过研究发现,很多情况下锁不仅不存在多线程竞争关系,即便存在也总是同一线程获此获得,为了减轻这种情况下的获得锁/释放锁的代价而发明了偏向锁。

当一个线程访问同步块并获取到锁后,会在对象头和栈帧中的锁记录里记录当前线程ID,以后该线程再进入和退出同步块时只要测试对象头的Mark Word里是否存储着指向当前线程ID的偏向锁,这样就不需要进行CAS操作来加锁和解锁,

如果记录着当前线程ID则表示线程之前已经获得了锁。如果不是当前线程ID则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置则使用CAS竞争锁;如果设置了则尝试使用CAS将对象头的偏向锁指向当前线程ID。

撤销偏向锁

释放原则就是一旦出现竞争则释放锁,不出现竞争则不释放锁(默认不存在竞争关系,既然存在那就立即释放锁)。偏向锁撤销的流程是这样的:首先暂停持有偏向锁的线程,然后查看其是否活着:如果其不处于活动状态,则将对象头设置成无锁状态;如果其仍然活着,则遍历偏向对象的锁记录,持有偏向锁的栈中锁记录和JAVA对象头的Mark Word要么重新变为其他线程ID、要么恢复到无锁状态或者标记对象不适合作为偏向锁,最后唤醒第一步中被暂停的线程。

关闭偏向锁

偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。

轻量级锁

轻量级锁加锁

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针(存放地址)。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁

轻量级锁解锁

抢夺锁失败的线程会通过自旋来再次获取锁,但这不意味其会无限自旋,一方面自旋会消耗CPU、另一方面已经获得锁的线程可能被阻塞住进而造成无用的自旋,所以,锁会从轻量级升级到重量级锁。一旦升级成重量级锁之后,所有其他线程试图获取锁时都会被阻塞,

当持有锁的线程解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,即把JAVA对象中的Mark Word所存储的指针替换为指针所指向的值,如果成功则表示没有竞争发生;如果失败则表示当前锁存在竞争,锁已经升级为重量级锁。此时此刻持有锁的线程释放锁之后会唤醒所有被阻塞的线程,被唤醒的线程就会进行新一轮的夺锁之争。

锁的优缺点对比

偏向锁

优点:加锁和解锁不需要额外的消耗(以为压根就不需要加解锁,就是替换个数值而已),和执行非同步方法相比仅存在纳秒级别的差距。

缺点:如果线程之间存在锁竞争的话,会带来额外的锁撤销的消耗,毕竟一旦出现竞争关系,偏向锁就得收回,影响已经获取到锁的线程(因为首先会暂停已经获取到锁的线程)。

适用场景:适用于只有一个线程访问同步代码块的场景。

轻量级锁

优点:竞争的线程不会阻塞已经获取到锁的线程(偏向锁会),进而提高了程序的响应速度。

缺点:如果始终得不到锁的竞争线程,使用自旋会消耗CPU。

适用场景:追求响应时间,同步代码块执行速度非常快(通过JVM参数直接关闭偏向锁,直接从轻量级锁开始)

重量级锁

优点:线程竞争不使用自旋,因此不会消耗CPU。

缺点:竞争同一个锁的其他线程都被阻塞,响应时间缓慢。

适用场景:追求吞吐量,同步代码块执行速度较长。

 

 

 

 

 

 

 

猜你喜欢

转载自blog.csdn.net/attack_breast/article/details/83474953
今日推荐