JMM
Volatile是java虚拟机提供的轻量级的同步机制,
Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
-
- 保证可见性
-
- 不保证原子性
-
- 禁止指令重排
可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉。通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。
**指令重排**:jvm会再不影响正确性的前提下,可以调整语句的执行顺序,指令重排序优化,
事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的CPU指令
例如,每条指令都可以分为:取指令-指令译码-执行指令-内存访问-数据写回这五个阶段
现代CPU支持多级指令流水线,例如同时执行取指令-指令译码-执行指令-内存访问-数据写回的处理器,可以称之为五级指令流水线,这时CPU可以在一个时钟周期内,同时运行五条指令的不同阶段,本质上流水行技术并不能缩短单挑指令的执行时间,但其变向地提高了指令吞吐量。
在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行
在 Java 中 volatile、synchronized 和 final 实现可见性。
什么是JMM:java内存模型,不存在的东西,是一个概念,约定!
关于JMM的一些同步约定:
-
- 线程解锁前,必须把共享变量立刻刷会主存
-
- 线程加锁前,必须读取主存中的最新值到工作内存中
-
- 加锁和解锁是同一把锁
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
- lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
- store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
- write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
JMM对这八种指令的使用,制定了如下规则:
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存
问题:程序并不知道主内存的值已经被修改过了,如果没加锁的情况下,如线程中,如下,线程中并不知道主线程已经修改了num的值,一直循环,线程无法结束
public class SemaphoreDemo {
private static int num = 0;
public static void main(String[] args) {
new Thread(() -> {
while(num == 0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
}
}
Volatile
1.保证可见性
在num的定义前加上volatile即可保证num的可见性,程序运行则会在num修改之后自动停止
底层实现原理为内存屏障
- 对volatile变量的写指令后会加入写屏障(会对该屏障之前对共享变量的变动,都同步到主存中)
- 对volatile变量的读指令前会加入读屏障(保证在该屏障之后,对共享变量的读取,加载的都是主存中最新数据)
2.不保证原子性
原子性:不可分割
即程序A在执行任务的时候,不能被打扰的,也不能被分割,要么同时成功要么同时失败
如下面java代码执行后得到的结果也不是我们预期的即是在num前已经加了volatile
public class Demo {
private volatile static int num = 0;
public static void main(String[] args) {
for (int i = 0; i < 40; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
while(Thread.activeCount() > 2){ //主线程,gc线程,当除这两个线程外还有线程时建议下
Thread.yield();
}
System.out.println(num);
}
public static void add(){
num++;
}
}
输出结果
那么如何在不加synchronized和lock的情况下保证原子性呢,使用util.concurrent.atomic下的原子类
public class Demo {
private volatile static AtomicInteger num = new AtomicInteger();
public static void main(String[] args) {
for (int i = 0; i < 40; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
while(Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(num);
}
public static void add(){
num.getAndIncrement(); //AtomicInteger + 1方法,CAS
}
}
**util.concurrent.atomic这个包下的类的底层都是和操作系统挂钩的,在内存中修改值!Unsafe!**是一个很特殊的存在
3.指令重排
Volatile是保证可见性的,不能保证原子性,由于内存屏障,可以避免指令重排的现象产生
单例模式
饿汉模式和懒汉模式
饿汉模式
//饿汉模式
public class Hungry {
private byte[] data1 = new byte[1024 * 1024];
private byte[] data2 = new byte[1024 * 1024];
private byte[] data3 = new byte[1024 * 1024];
private Hungry(){
}
private static final Hungry hungry = new Hungry();
public static Hungry newInstance(){
return hungry;
}
}
这种模式下未使用时浪费内存,如data1等数据内存被分配空间,消耗性能,我们应该想办法在使用单例对象时才分配内存,下面饿汉模式
public class LazyMan {
private LazyMan(){
}
private static LazyMan lazyMan;
public static LazyMan newInstance(){
if(lazyMan == null){
lazyMan = new LazyMan();
}
return lazyMan;
}
}
但是这种情况下单线程是安全的即只可能产生一个实例,但多线程下可能会产生多个实例,我们使用双重检测锁模式,下面为DCL懒汉式
public class LazyMan {
private LazyMan(){
}
private static LazyMan lazyMan;
public static LazyMan newInstance(){
if(lazyMan == null){
synchronized (LazyMan.class){
if(lazyMan == null){
lazyMan = new LazyMan(); //不是一个原子性操作
/*
* 1.分配内存空间
* 2.执行构造方法,初始化对象
* 3. 把这个对象指向这个空间
*
* 多线程下可能会造成指令重排
* 123
* 132 B此时lazyman还没完成构造
*/
}
}
}
return lazyMan;
}
}
此时需要在lazyMan前加上volatile避免指令重排(就是如果发生了指令重排,可能在A线程中先给对象分配了内存,然后先进行了第三步将这个对象指向了该内存空间,此时第二步构造方法还未执行完毕,B线程来了,B线程还没执行到同步代码块时在第一个if那里判断时会认为对象不为null,之间执行最后一句return,返回了一个未完成构造方法的对象,对应内存为一篇虚无,所以要加上volatile防止指令重排)
private volatile static LazyMan lazyMan;
但是我们明显可以想到如果我们直接使用暴力反射来使用调用懒汉的构造方法创建对象,这时创建出来的对象肯定不是单例的
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
LazyMan instance = LazyMan.newInstance();
Constructor<LazyMan> declareConstructor = LazyMan.class.getDeclaredConstructor();
declareConstructor.setAccessible(true);
LazyMan instance2 = declareConstructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
查看结果发现果然是两个实例,我们该如何解决这个问题呢?
道高一尺魔高一丈,我们可以在类的构造器中进行加锁
private LazyMan(){
synchronized (LazyMan.class){
if(lazyMan != null){
throw new RuntimeException("不要试图使用反射获取实例");
}
}
}
破环单例模式的三种方式:反射,序列化,克隆
如何防止反射、克隆、序列化对单例模式的破坏
- 防止反射破环(虽然构造方法已私有化,但通过反射机制使用newInstance()方法构造方法也是可以被调用):首先定义一个全局变量开关isFristCreate默认为开启状态
当第一次加载时将其状态更改为关闭状态 - 防止克隆破环:重写clone(),直接返回单例对象
- 防止序列化破环:添加readResolve(),返回Object对象
第一条中加变量仍有通过反编译或者反射来破坏的危险,可以尝试使用枚举