Java并发笔记1-底层实现volatile、synchronized、原子操作

volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性:当一个线程修改一个变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,他比synchronized的使用和执行成本更低,因为他不会引起线程上下文的切换和调度。

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

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

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

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

volatile:如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。 

volatile的两条实现原则:

1)Lock前缀指令会引起处理器缓存写回到内存。

2)一个处理器的缓存写到内存会导致其他处理器的缓存无效。

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

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

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

对于同步代码块,锁是synchronized括号里配置的对象

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

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是另外一种方式实现的。

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

JavaSE1.6中锁一共有4种状态:无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态。

  • 偏向锁:1.当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下MarkWord中偏向锁的标识是否设置成为1(表示当前是偏向锁);如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。2.偏向锁的撤销:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的MarkWord要么偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。3.关闭偏向锁:在Java6和7是默认启动,但是在程序启动几秒后在激活。可以使用JVM参数关闭延迟。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁。
  • 轻量级锁:1.轻量级锁加锁:线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,。然后线程尝试使用CAS将对象头中的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获得锁。2.解锁:轻量级解锁时,会使用原子的CAS操作将DIsplaced MarkWord替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。3.因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级为重量级锁,就不会再恢复到轻量级锁状态,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程被释放之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。
  • 重量级锁
  • 优点 缺点 适用场景
    偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 如果线程间存在竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块场景
    轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到锁竞争的线程,使用自旋会消耗CPU。 追求响应时间同步块执行速度非常快。
    重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢 追求吞吐量同步块执行速度较长。
    3原子操作的实现
  • 原子操作:不可中断的一个或一系列操作
  • 1)使用总线锁保证原子性:第一个机制是通过总线锁保证原子性。所谓总线锁就是使用处理器提供的一个Lock信号,当一个处理器在总线上输出此信号时,其他处理器将被阻塞住,那么该处理器可以独占共享内存。
  • 2)使用缓存锁保证原子性:第二个机制是通过缓存来保证原子性。所谓“缓存锁定”是指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当他执行锁操作回写到内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许他的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效。但是有两种情况下处理器不会使用缓存锁定:1挡操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line),则处理器回调用总线锁定。2有些处理器不支持缓存锁定
  • 3)Java实现原子操作
  • 在Java中可以通过锁和循环CAS实现原子操作。使用循环CAS实现原子操作
  • JDK的并发包提供了一些类支持原子操作如:AtomicBoolean(用原子方式更新的boolean值)、AtomicInteger(用原子方式更新int值)、AtomicLong(用原子方式更新long值)。这些原子包装类还提供了有用的工具方法,比如以原子的方式将当前值自增1或自减1.
  • 实现原子操作的三大问题:1)ABA问题:解决思路-加上版本号:Atomic包里compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。2)循环时间长开销大:JVM能支持处理器提供的pause指令,那么效率会有一定提升。3)只能保证一个共享变量的原子操作: 对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。把多个共享变量合并成一个共享变量来操作。
  • 使用锁机制实现原子操作:
  • 锁机制保证了只有获得锁的线程才能操作锁的内存区域。JVM内部实现了很多种锁机制,有偏向锁,轻量级锁和互斥锁。除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。

猜你喜欢

转载自blog.csdn.net/cclliii/article/details/82805823