600字范文,内容丰富有趣,生活中的好帮手!
600字范文 > Java 并发编程_详解 synchronized 和 volatile

Java 并发编程_详解 synchronized 和 volatile

时间:2018-11-01 03:08:57

相关推荐

Java 并发编程_详解 synchronized 和 volatile

文章目录

1. synchronized 的应用1.1 基础知识1.2 synchronized 语法2. Monitor概念3. Synchronized原理进阶3.1 对象头格式3.2 轻量级锁(用于优化Monitor这类的重量级锁)3.3 锁膨胀3.4 自旋优化3.4 偏向锁(用于优化轻量级锁重入)3.4.1 撤销偏向3.4.2 批量重偏向3.4.3 批量撤销3.4.4 锁消除4. volatile4.1 CPU 术语4.2 实现可见性

1. synchronized 的应用

1.1 基础知识

临界区 Critical Section

一个程序运行多个线程本身是没有问题的问题出在多个线程访问共享资源 多个线程读共享资源其实也没有问题在多个线程对共享资源读写操作时发生指令交错,就会出现问题 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

例如,下面代码中的临界区

static int counter = 0;static void increment() // 临界区 {counter++; }static void decrement() // 临界区 {counter--; }

竞态条件 Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

1.2 synchronized 语法

synchronized,即俗称的【对象锁】,它采用互斥的方式让同一 时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住(blocked)。这样就能保证拥有锁 的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

(1) 锁对象

synchronized(对象) {//临界区}

(2)synchronized加在方法上

加在成员方法上

public class Demo {//在方法上加上synchronized关键字public synchronized void test() {}//等价于public void test() {synchronized(this) {}}}

加在静态方法上

public class Demo {//在静态方法上加上synchronized关键字public synchronized static void test() {}//等价于public void test() {synchronized(Demo.class) {}}}

2. Monitor概念

Monitor 被翻译为监视器或者管程

每个 Java对象都可以关联一个Monitor 对象,如果使用synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向Monitor 对象的指针

当线程执行到临界区代码时,如果使用了synchronized,会先查询synchronized中所指定的对象(obj)是否绑定了Monitor。 如果没有绑定,则会先去去与Monitor绑定,并且将Owner设为当前线程。如果已经绑定,则会去查询该Monitor是否已经有了Owner 如果没有,则Owner与将当前线程绑定如果有,则放入EntryList,进入阻塞状态(blocked) 当Monitor的Owner将临界区中代码执行完毕后,Owner便会被清空,此时EntryList中处于阻塞状态的线程会被叫醒并竞争,此时的竞争是非公平的注意: 对象在使用了synchronized后与Monitor绑定时,会将对象头中的Mark Word置为Monitor指针。每个对象都会绑定一个唯一的Monitor,如果synchronized中所指定的对象(obj)不同,则会绑定不同的Monitor如果对于对象结构不熟悉可以看 这篇博客

举例:

public class MonitorTest {static final Object lock = new Object();static int counter = 0;public static void main(String[] args) {synchronized (lock) {counter++;}}}

3. Synchronized原理进阶

3.1 对象头格式

3.2 轻量级锁(用于优化Monitor这类的重量级锁)

轻量级锁使用场景:当一个对象被多个线程所访问,但访问的时间是错开的(不存在竞争),此时就可以使用轻量级锁来优化。

创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录对象,内部可以存储锁定对象的mark word对象头(不再一开始就使用Monitor)

让锁记录中的Object reference指向锁对象(Object),并尝试用cas去替换Object中的mark word,将此mark word放入lock record中保存

如果cas替换成功,则将Object的对象头替换为锁记录的地址和状态 00(轻量级锁状态),并由该线程给对象加锁

如果cas失败,有两种情况

如果是其他线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程如果是自己执行了synchronized锁重入,那么再添加一条Lock Record 作为重入的计数 重入:同一个线程给同一个对象加锁

当退出synchronized 代码块(解锁时)如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入技术减一

当退出synchronized代码块(解锁时)锁记录的值不为null,这时使用cas将Mark Word的值恢复给对象头

成功则解锁成功失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

3.3 锁膨胀

如果一个线程在给一个对象加轻量级锁时,cas替换操作失败(因为此时其他线程已经给对象加了轻量级锁),此时该线程就会进入锁膨胀过程

此时便会给对象加上重量级锁(使用Monitor) 将对象头的Mark Word改为Monitor的地址,并且状态改为01(重量级锁)并且该线程放入入EntryList中,并进入阻塞状态(blocked)

3.4 自旋优化

重量级锁竞争时,还可以使用自旋来优化,如果当前线程在自旋成功(使用锁的线程退出了同步块,释放了锁),这时就可以避免线程进入阻塞状态。

第一种情况 成功

第二种情况 失败

3.4 偏向锁(用于优化轻量级锁重入)

轻量级锁在没有竞争时,每次重入(该线程执行的方法中再次锁住该对象)操作仍需要cas替换操作,这样是会使性能降低的。

所以引入了偏向锁对性能进行优化:在第一次cas时会将线程的ID写入对象的Mark Word中。此后发现这个线程ID就是自己的,就表示没有竞争,就不需要再次cas,以后只要不发生竞争,这个对象就归该线程所有。

对象头的状态

Normal:一般状态,没有加任何锁,前面62位保存的是对象的信息,最后2位为状态(01),倒数第三位表示是否使用偏向锁(未使用:0)Biased:偏向状态,使用偏向锁,前面54位保存的当前线程的ID,最后2位为状态(01),倒数第三位表示是否使用偏向锁(使用:1)Lightweight:使用轻量级锁,前62位保存的是锁记录的指针,最后两位为状态(00)Heavyweight:使用重量级锁,前62位保存的是Monitor的地址指针,后两位为状态(10)如果开启了偏向锁(默认开启),在创建对象时,对象的Mark Word后三位应该是101但是偏向锁默认是有延迟的,不会再程序一启动就生效,而是会在程序运行一段时间(几秒之后),才会对创建的对象设置为偏向状态如果没有开启偏向锁,对象的Mark Word后三位应该是001

3.4.1 撤销偏向

以下几种情况会使对象的偏向锁失效

调用对象的hashCode方法多个线程使用该对象(无竞争状态会升级为轻量级锁)调用了wait/notify方法(调用wait方法会导致锁膨胀而使用重量级锁)

3.4.2 批量重偏向

如果对象虽然被多个线程访问,但是线程间不存在竞争,这时偏向T1的对象仍有机会重新偏向T2 重偏向会重置Thread ID 当撤销超过20次后(超过阈值),JVM会觉得是不是偏向错了,这时会在给对象加锁时,重新偏向至加锁线程。

3.4.3 批量撤销

当撤销偏向锁的阈值超过40以后,就会将整个类的对象都改为不可偏向的,新建的对象也是不可偏向的

3.4.4 锁消除

举例:

public void test() {Object o = new Object();synchronized (o) {//doSomeThing}}

在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除

4. volatile

在多线程并发编程中 synchronized 和 volatile 都扮演着重要的角色,volatile 是轻量级的 synchronized ,它在多处理器开发中保证了共享变量的“可见性”,可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile 变量修饰符使用恰当的话,他比synchronized的使用和执行成本更低

4.1 CPU 术语

内存屏障:是一组处理器指令,用于实现对内存操作的顺序限制缓冲行:缓存中可以分配的最小存储单位。处理器填写缓存线时会加载整个缓存线,需要使用多个主内存读周期原子操作:不可终端的一个或一系列操作缓存行填充:当处理器识别到从内存中读取操作数是可缓存的,处理器读取整个缓存行到适当的缓存缓存命中:如果进行高速缓存行填充操作的内存位置仍然是下次处理器访问的地址时,处理器从缓存行中读取操作数,而不是从内存中读取写命中:当处理器将操作数写回到一个内存缓存的区域时,它首先会检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不是写回内存,这个操作被称为写命中写缺失:一个有效的缓存行被写入到不存在的内存区域

4.2 实现可见性

java 代码 :

instance = new Singleton(); // instance 是 volatile 变量

转变成汇编代码:

0x01a3de: move $0 x 0, 0 x 1104800(%esi);0x01a3de24: lock add1 $0 x 0,(%esp)

有volatile修饰的共享变量进行写操作的时候会多出第二行代码,主要在于Lock前缀

Lock 前缀的指令在多核处理器会引发两件事

Lock 前缀指令会引起处理器缓存回写到内存一个处理器的缓存回写到内存会导致其他处理器的缓存无效

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。