1.volatile
volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。
volatile两条实现原则:
- Lock前缀指令会引起处理器缓存回写到内存。
- 一个处理器的缓存回写到内存会导致其他处理器的缓存无效。
在JDK 7的并发包里新增一个队列集合类LinkedTransferQueue,它在使用volatile变量时,用一种追加字节的方式来优化队列出队和入队的性能。(补足64个字节,使得缓存中的头尾节点不会被读到一个缓存行中,也就不能相互锁定)
不应该追加字节的场景:
- 缓存行非64字节宽的处理器。
- 共享变量不会被频繁地写。
1.synchronized
用synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为以下3种形式。
- ·对于普通同步方法,锁是当前实例对象。
- ·对于静态同步方法,锁是当前类的Class对象。
- ·对于同步方法块,锁是Synchonized括号里配置的对象。
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
(synchronized本质上是去获取对象对应的monitor的所有权)
synchronized用的锁是存在Java对象头里的。如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽等于4字节,即32bit,
在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。
》偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
简单说就算获得它的线程退出后先不释放锁,下一次如果还是这个线程进入的话就不用再获取锁了, 只是去比较一下是否是偏向锁且锁的markword中的线程ID是否是当前ID,
如果是的话,都不用CAS,就直接进入同步块了。 如果有别的线程来竞争的话,就撤销偏向锁标志了。
》轻量级锁
如果当前不是偏向锁,且线程通过CAS修改锁的markword成功了,则就将其标志为轻量级锁
如果CAS没成功,则一直自旋CAS,如果失败到一定程度,则认为膨胀为重量级锁,修改其锁标志,这个自旋的线程再进入阻塞状态。
任何去竞争对象锁的线程都有权利按照规则修改其对象头的锁标志(不管是否获取到锁),但对象头的markword中的指向的锁记录的栈帧所属的线程才是该线程获得锁的标志
如果有线程去尝试获得锁,发现是重量级锁,就不会自旋CAS了,而是直接进入阻塞
在Java中可以通过锁和循环CAS的方式来实现原子操作。
(只有AtomicXXX才能使用CAS)
CAS实现原子操作的三大问题
- ABA问题,java中使用AtomicStampedReference解决,这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
- 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
- 只能保证一个共享变量的原子操作。这个时候就可以用锁
》使用锁机制实现原子操作
锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁。有意思的是除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。