《java并发编程的艺术》并发底层实现原理

volatile

作用:当一个线程修改一个共享变量时,另一个线程能读到这个修改的值。
定义:java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独的获得这个变量。

有volatile修饰的共享变量在进行写操作时会多出第二行汇编代码
lock ...
该前缀指令在多核处理器下会引发两件事情:
1. 将当前处理器缓存行的数据写到系统内存。
2. 这个写回内存的操作会使其他CPU里缓存了该内存地址的数据无效。

synchronizd

java中每一个对象都可以作为锁,表现为以下3种形式
1. 对于普通同步方法,锁是当前实例对象
2. 对于静态同步方法,锁是当前类的Class对象
3. 对于同步方法块,锁是synchronized括号里配置的对象。

JVM规范:JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。
代码块使用:monitorentermonitorexit指令实现同步。
方法块同样可以使用上述方法。
monitorenter指令是在编译后插入到同步代码块的开始位置。
monitorexit插入到方法结束处和异常处。
每一个对象有一个monitor相关联,当一个monitor被持有后处于锁定状态,线程执行到monitorenter指令时,会尝试获取对象的锁。另外monitorentermonitorexit相对应。

java对象头

长度 内容 说明
32/64位 Mark Word 存储对象的hashcode或锁信息等
32/64位 Class Metadata Address 存储到对象类型数据的指针
32/64位 Array length 数组的长度(如果当前对象是数组)

32位Mark word:

锁状态 25位 4位 1位(是否是偏向锁) 2位(锁标志位)
无锁状态 对象的hashcode 对象分代年龄 0 01

64位Mark word:

锁状态 25位 31位 1位(cms_free) 4位(分代年龄) 1位(是否是偏向锁) 2位(锁标志位)
无锁 unused hashcode 0 01
偏向锁 threadID(54位)epoch(2位) 1 01

锁的升级和对比

递增分为4种状态:无锁、偏向锁、轻量级锁、重量级锁
只能升级,不能降级(提高获得锁和释放锁的效率)

偏向锁

CAS(比较与交换,Compare and swap) 是一种有名的无锁算法。

1.当一个线程访问同步块并获取锁时,会在对象头和栈帧的锁记录里存储锁偏向的线程ID。
2.在下次线程进入或者退出同步块时,测试对象头的Mark Word里是否存储着指向当前线程的偏向锁。
3.如果测试成功,表示线程已经获得了锁,失败则需要再测试Mark Word中偏向锁的标识是否设置为1(表示当前是偏向锁)如果没有设置则使用CAS竞争锁,否则尝试使用CAS将对象头的偏向锁指向当前线程。

偏向锁的撤销:
偏向锁使用了一种等到竞争出现才释放锁的机制。
1.首先暂停拥有偏向锁的线程
2.然后检查持有偏向锁的线程是否存活,如果不处于活动状态,将对象头设置为无锁状态,如果线程存活,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

轻量级锁

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

解锁:
使用原子的CAS操作将Displaced Mark Word替换回对象头,通过成功表示没有竞争发生,如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

原子操作

处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。
1.使用总线锁保证原子性
使用处理器提供的LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求被阻塞,那么该处理器可以独占共享内存。
2.使用缓存锁定保证原子性
缓存锁定:内存区域如果被缓存在了处理器的缓存行中,并且在Lock操作期间被锁定,那么当他执行锁操作回写到内存时,处理器修改内部的内存地址,用缓存一致性(组织同时修改由两个以上处理器缓存的内存区域数据)保证一致性。

java实现原子操作

通过 锁和循环CAS 的方式实现原子操作。

猜你喜欢

转载自blog.csdn.net/sayWhat_sayHello/article/details/81075111