Java 内存模型
问题引入 —> 很多人不清楚 java内存模型 和 jvm运行时数据区,其实它俩是完全不同的俩个概念。
引入概念:jvm 虚拟机规范
当我们编写的java文件,经过javac编译器,编译为字节码文件,可以在不同的虚拟机却执行出基本相同的效果,原因是各大开发虚拟机的厂商都遵循了一个协议,受到了约束,这就是 Jvm虚拟机规范。
每一种语言都会有规范,比如java有java语言规范 、 scala 有scala语言规范。
java语言规范:用来描述Java语言应该有什么样的语法,代码应该如何执行等。
java运行时数据区:是由《java虚拟机规范》提出来的,用来描述一个虚拟机在内存这一块需要遵循什么样的特点,比如有方法区,堆内存,以及线程独占的内存区域。
java内存模型:是由《java语言规范》提出来的,用来描述java语言的特性,实际上就java内存模型描述的是java多线程程序执行的一些特性(规则)。
结论:二者描述的对象完全不同,java运行时数据区用来描述虚拟机在内存中的一些规范,而java内存模型是描述java语言多线程程序执行时的的规则。
java内存模型描述的多线程中有很多的问题
1、所见非所得(查看代码逻辑,并不是按照自己所看到的逻辑运行)
2、无法用肉眼去检测程序的准确性
3、不同的运行平台有不同的表现(代码在32位jdk 和 64 位jdk有不同的表现,不同的JDK对JIT即时编译器优化处理的思路不同)
4、错误很难重现
java内存模型提出规则,具体干活的是java虚拟机规范(jvm);
Shared Variables 定义:
可以在线程之间共享的内存称为共享内存或者堆内存。
所有的对象字段,静态字段,数组元素都存储在堆内存(广义的堆内存,包括堆内存和方法区)中,这些字段都是共享变量。
冲突:如果至少有一个对一个变量的访问是 写 操作 ,那么对这个变量的俩次访问是冲突的。
线程间操作:
1、线程间的操作:指一个程序执行的操作可以被其他线程感知到或者被其他线程直接影响。
2、java内存模型只描述线程间的操作,不描述线程内部的操作,线程内部的操作按照线程内部语义
所有线程间的操作都存在可见性问题,java内存模型需要对其进行规范。
常见的线程间操作:
-
read
-
write
-
Lock UnLock
-
线程的第一个和最后一个操作
-
外部操作(俩个线程对外部一个文件进行操作)
java内存模型对于同步(可见)的规则定义
- 对于使用volatile关键字修饰的变量的修改,后续所有线程都能感知到它的修改,进而读取到正确的值。
- 对于一个监视器锁M 的解锁与后续对M的加锁同步
- 对于每个属性写入的默认值(0,false,null)与每个线程对他的进行的操作同步
- 启动线程的操作与线程中的第一个操作同步(new thread().start( )状态就由 new变为runnable )
- 线程T2的最后操作与线程T1发现线程T2以及结束同步。
- 如果线程T1中断了线程T2,那么线程T1的中断操作与其他所有线程发现T2被中断了同步。
可见性
CPU指令重排
java编程语言语义允许java编译器和微处理器进行执行优化,这些导致了与其交互的代码不再同步。
as - if - serial 语义 : 保证单个线程的执行顺序改变,执行结果不改变。
脚本语言和编译的区别?
宗旨:只要是给机器执行的语言,最终都要被编译为机器码让机器去执行。
脚本语言:解释执行,在执行时,由语言的解释器将其一条条翻译成机器可识别的指令。
编译语言:将我们编写的程序,批量直接编译成机器可以识别的指令码。
java 既是脚本语言也是编译语言
执行前编译器(javac)与执行时编译器(JIT);
代码示例:
值修改成功,但是JIT过度优化,把isRunning缓存起来,导致isRunning修改后也看不到。
解决办法:
volatile关键字
可见性问题:让一个线程对共享变量的修改,可以及时的被其他线程看到。(先写后读)
使用volatile修饰变量在反编译后class文件中会发现有一个ACC_VOLATILE修饰符
java内存模型提出了一个规范,那就是对于使用volatile关键字修饰的变量的修改,后续所有线程都能感知到它的修改,进而读取到正确的值。
volatile关键字的功能:
1、禁止缓存(不允许JIT 编译器的过度优化);
2、对于volatile关键字修饰的变量 不允许进行指令重排序(只会阻止一部分的指令重排,对于影响volatile修饰变量的结果的指令不允许重排);
Interrupt() 详解
isInterrupt ( ) 是一个标记位,也是一个中断状态,不属于线程的六个状态之一。
当正在运行的线程调用interrupt() 方法中断状态会由false ----->true .并不会改变线程的状态,只是一个标记位。
当正在睡眠的线程调用interrupt(),会抛出异常,会中断线程,并不是interrupt中断的,是抛出异常后没有处理中断的 ,并且 方法的中断状态会被擦除 ,由 false ----> true ----> false。
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) { // !true
try {
System.out.println("我正在运行!");
// Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
Thread.currentThread().interrupt(); //当线程正在睡眠时,会抛出异常,
// 擦除中断状态,此时再加中断状态用来中断线程 false -->true
}
}
}
};
thread.start();
Thread.sleep(3000);
thread.interrupt(); //false --> true
}
}
原子性
原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分(不可中断性)。
将整个操作视作一个整体,资源在该次操作中保持一致,这个是原子性的核心特征。
原子问题出现的俩个场景:
1、当一个线程判断了某种状态后,这个状态失效了。
2、当一个线程加载了一个值,这个值失效了。
解决办法:
1、加锁 (synchronize、lock) 一个一个执行。
2、CAS( oldField , newField )。
CAS (Compare and swap)
Compare and swap 比较和交换。属于硬件级别的语言,处理器提供了基本内存操作的原子性特征。
CAS操作需要俩个参数,一个是旧值,一个是新值,在操作期间先对旧值进行比较,若没有发生变化,才交换新值,发生了变化则不交换。
示例:
public class CounterUnsafe {
volatile int i = 0;
private static Unsafe unsafe = null;
//i字段的偏移量
private static long valueOffset;
static {
//unsafe = Unsafe.getUnsafe(); 不可以这么获取
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
Field fieldi = CounterUnsafe.class.getDeclaredField("i");
valueOffset = unsafe.objectFieldOffset(fieldi);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
public void add() {
//i++; 自旋的问题
for (;;){
int current = unsafe.getIntVolatile(this, valueOffset);
if (unsafe.compareAndSwapInt(this, valueOffset, current, current+1))
break;
}
}
}
Atomic 比 锁 的性能好多了。