1. volatile的使用
在多线程并发编程中synchronized和volatile都扮演着重要的角色,volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的‘可见性’。
2. synchronized 的实现原理与应用。
Java中的每一个对象都可以作为锁。具体表现为以下3种形式:
(1)对于普通同步方法,锁是当前实例对象。
(2)对于静态同步方法,锁是当前类的Class对象。
(3)对于同步方法块,锁是synchronized括号里配置的对象。
从JVM规范中可以看到Synchronized在JVM里面的实现原理,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用 monitorenter 和 monitorexit 指令实现的,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细的说明。但是,方法的同步同样可以使用这两个指令来实现。
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,并且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。
3. Java对象头
synchronized 用的锁是存在Java对象头里的。如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。
Java对象头长度:
Java对象头里的Mark Word里默认存储对象的HashCode/ 分代年龄 / 和锁标记位。
32位JVM的Mark Word 的默认存储结构如表:
Java对象头的存储结构
在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。32位虚拟机 Mark Word可能变化为存储以下4种数据
在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:
4. 锁的升级与对比
Java SE 1.6 为了减少获得锁和释放锁带来的性能损耗,引入了‘偏向锁’ 和 ‘轻量级锁’, 在Java SE 1.6 中,锁一共有4种状态,级别从低到高依次是:无锁状态/ 偏向锁状态/ 轻量级锁状态 /重量级锁状态。
关闭偏向锁:
偏向锁在Java 6 和 Java 7 里是默认启用的,但是它在应用程序启动几秒之后才激活,
如有必要可以使用 JVM 参数来关闭延迟:
-XX:BiasedLockingStartupDelay=0。
如果确定应用程序里的所有锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:
-XX:-UseBiasedLocking=false。
偏向锁的撤销
关闭偏向锁
------------------------------------
轻量级锁: 轻量级锁加锁/ 轻量级锁解锁
线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displayed Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换位指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其它线程竞争锁,当前线程便尝试通过自旋来获取锁。
轻量级锁解锁使,会使用原子的CAS操作将Displayed Mark Word替换回对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
因为自旋会消耗CPU,为了避免无用的自旋(比如获取锁的线程被堵塞住了),一旦锁升级成为重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程视图获取锁时都会被堵塞住,当持有锁的线程释放锁之后就会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。
锁的优缺点对比:
CAS操作
CAS是单词compare and set的缩写,意思是指在set之前先比较该值有没有变化,只有在没变的情况下才对其赋值。
voliatile的应用:
Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获取这个变量。
Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。
CPU的术语定义:
volatile的两条实现原则:
1. Lock前缀的指令会引起处理器缓存回写到内存。
2. 一个处理器的缓存回写到内存会导致其它处理器的缓存无效。
原子操作的实现原理:
CPU术语定义:
处理器如何实现原子操作:
32位IA-32处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。
(1)使用总线锁保证原子性
第一个机制是通过总线锁保证原子性。
所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其它处理器的请求将被阻塞住,那么该处理器可以独占共 享内存。
(2)使用缓存锁保证原子性
第二个机制是通过缓存锁定来保证原子性。
所谓“缓存锁定”是指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言LOCK # 信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已锁定的缓存行的数据时,会使缓存行无效。
但是有两种情况下处理器不会使用缓存锁定:
(1)当操作的数据不能被缓存在处理器内部或操作的数据跨多个缓存行(cache line)时,则处理器会调用总线锁定。
(2)有些处理器不支持缓存锁定。对于Intel 486和Pentium处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。
针对以上两个机制,我们通过Intel处理器提供了很多Lock前缀的指令来实现。
-------------------------------------------------------------------
Java如何实现原子操作:
在Java中可以通过锁和循环CAS的方式来实现原子操作。
(1)使用循环CAS实现原子操作
JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。
package com.newbies.thread;
import org.jcp.xml.dsig.internal.dom.DOMTransform;
import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;
/**
* TODO :
*
* @VERSION 1.0
* @author: 杨改革
* @date: 2021/4/8 11:18
*/
public class Piped {
public static void main(String[] args) throws IOException {
PipedWriter out = new PipedWriter();
PipedReader in = new PipedReader();
// 将输出流和输入流进行连接,否则在使用时会抛出IOException
out.connect(in);
Thread printThread = new Thread(new Print(in),"PrintThread");
printThread.start();
int receive = 0;
try {
while ((receive=System.in.read())!=-1){
out.write(receive);
}
}finally {
out.close();
}
}
static class Print implements Runnable{
private PipedReader in;
public Print(PipedReader in){
this.in=in;
}
@Override
public void run() {
int receive = 0;
try {
while ((receive=in.read())!=-1){
System.out.print((char)receive);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
(2)CAS实现原子操作的三大问题
在Java并发包中有一些并发框架也使用了自旋CAS的方式来实现原子操作,比如LinkedTransferQueue类的Xfer方法。CAS虽然很高效地解决了原子操作,但是CAS仍然存在3大问题:
1)ABA问题。 AtomicStampedReference.compareAndSet方法解决。
2)循环时间长开销大。
3)只能保证一个共享变量地原子操作。
(3)使用锁机制实现原子操作
锁机制保证了只有获得锁地线程才能够操作锁定的内存区域。JVM内部实现了很多锁机制,有偏向锁/ 轻量级锁和互斥锁。有意思的是除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。