Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令。
1.volatile的应用
volatile生成汇编指令时会在前面加lock前缀,lock前缀的指令在多核处理器下会引发了两种事情。
1.将当前处理器缓存行的数据写回到系统内存
2.这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
缓存一致性协议:
1.读取操作,不做任何处理,只是将Cache中的数据读取到寄存器。
2.写入操作,发出信号通知其他CPU将该变量的Cache line置为无效状态,其他CPU在进行该变量读取的时候不得不到主内存中再次获取。
2.synchronized的实现原理与应用
Java中的每一个对象都可以看做锁。
1.对于普通同步方法,锁是当前对象
2.对于静态同步方法,锁是当前类的Class对象
3.对于同步代码块,锁是Synchonized括号里配置的对象
同步代码块的底层实现是用两条汇编指令:monitorenter monitorexit
任何对象都有一个monitor与之关联,当且一个monitor被持有之后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。
2.1. Java对象头
synchronized用的锁是存在java对象头里的。
2.2. 锁的升级与对比
在jkd6中,锁有四种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。
1.偏向锁
偏向锁使用了一种等到竞争才释放锁的机制,所以当其他线程阐释竞争偏向锁时,持有偏向锁的线程才会释放锁。
2.轻量级锁
(1)轻量级加锁
线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark word复制到锁记录中,官方成为Displaced Mark Word.然后线程尝试使用CAS将对象头中的Mark Work替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获得锁。
(2)轻量级锁解锁
轻量级解锁,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀为重量级锁。
锁的优缺点比较
偏向锁
优点:加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距
缺点:如果线程间存在锁竞争,会带来额外的锁撤销的消耗
使用场景:适用于只有一个线程访问同步块场景
轻量级锁
优点:竞争的线程不会阻塞,提高了程序的响应速度
缺点:如果始终得不到锁竞争的线程,使用自旋会消耗CPU
运用场景:追求响应时间,同步块执行速度非常快
重量级锁
优点:线程竞争不使用自旋,不会消耗CPU
缺点:线程阻塞,响应时间缓慢
运用场景:追求吞吐量,同步块执行速度较长
3. 原子操作的实现原理
3.1 处理器如何实现原子操作
首先处理器会自动保证基本的内存操作的原子性
处理器保证从系统内存中读取或者写入一个字节是原子性的。
处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。
1.总线锁定:所谓总线锁就是使用处理器提供的一个lock#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
2.缓存锁定
所谓“缓存锁定”是指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行所操作回写到内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。
但是有两种情况下处理器不会使用缓存锁定:
1.当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行
2.有些处理器不支持缓存锁定
3.2 java如何实现原子操作
在Java中可以通过锁和循环CAS的方式来实现原子操作
(1)使用循环CAS实现原子操作
jvm中的CAS操作正是利用了处理器提供了CMPXCHG指令实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。
(2)CAS实现原子操作的三大问题
1.ABA问题。
ABA问题的解决方法就是使用版本号。
JDK的Atomic包下提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标记是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
2.循环时间开销大。
3.只能保证一个共享变量的原子操作。
从jdk5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
(3)使用锁机制实现原子操作
锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM内部实现了很多种锁机制,有偏向锁、轻量级锁、互斥锁。有意思的是除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。