JUC并发编程(2)|JMM
目录
JUC (2)
什么是JMM?
JMM只是一个java内存模型,一种概念模型。不是具体存在的。为了保护JAVA内存运行安全
-
线程解锁前,必须把共享变量立刻刷回主存。
- 线程A去拿主存的变量,不会直接引用地址,而是clone一份。
- 线程A把拿来的共享变量改变后,要马上刷新回去
-
线程加锁前,必须读取主存中的最新值到工作内存中
-
加锁和解锁是同一把锁
线程的工作内存(ThreadLocal)
八种操作
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便 随后的load动作使用
- load(载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机 遇到一个需要使用到变量的值,就会使用到这个指令
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变 量副本中
- write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
- store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中, 以便后续的write使用
- lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量 才可以被其他线程锁定
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操作之前,必须把此变量同步回主内存
⚠️B修改了值,但A不能及时可见
Volatile
Volatile 是 Java 虚拟机提供轻量级的同步机制
-
保证可见性
- 内存中的变量改变后,及时刷新到所有工作内存中。
-
不保证原子性
原子性 : 不可分割 线程A在执行任务的时候,不能被打扰的,也不能被分割。要么同时成功,要么同时失败。
- 使用Atom原子类解决原子性问题 CAS
-
禁止指令重排
- 源代码–>编译器优化的重排–> 指令并行也可能会重排–> 内存系统也会重排—> 执行
- 处理器在进行指令重排的时候,考虑:数据之间的依赖性
单例模式
-
饿汉式
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// 饿汉式单例 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 byte[] data4 = new byte[1024*1024]; private Hungry(){ } private final static Hungry HUNGRY = new Hungry(); public static Hungry getInstance(){ return HUNGRY; } }
-
-
普通懒汉式⚠️
-
1 2 3 4 5 6 7 8 9 10 11 12
public class LazyMan { private static LazyMan lazyMan; private LazyMan(){} public static LazyMan getInstance(){ if(lazyMan==null){ lazyMan=new LazyMan(); } return lazyMan; } }
-
单线程下单例可行,但多线程并发有可能会重复创建新实例。原因是
lazyMan=new LazyMan()
这一步时不是原子性操作,其他线程可能会判断到if(lazyMan==null)
还是true,重复new。
-
可以使用double check机制⬇️
-
-
DCL懒汉式
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
public class DCLLazyMan { private volatile static LazyMan lazyMan; private LazyMan(){ System.out.println(Thread.currentThread().getName()+" ok"); } public static LazyMan getInstance(){ if(lazyMan==null){ synchronized(LazyMan.class){ if(lazyMan==null){ lazyMan = new LazyMan();//不是原子性操作 } } } return lazyMan; } }
synchronized(LazyMan.class)
加锁双重验证volatile
修饰类变量,避免指令重排new LazyMan()
的3个步骤- 分配内存空间
- 执行构造方法,初始化对象
- 对象指向该堆地址
- 指令顺序:123————>正常
- 指令顺序:132————>对象指向的堆地址已经分配内存空间,但未初始化对象,且
lazyMan==null
会造成难以预计的后果。
-
-
静态内部类
-
1 2 3 4 5 6 7 8 9 10
//静态内部类 public class Holder { private Holder(){} public static Holder getInstance(){ return InnerClass.HOLDER; } public static class InnerClass{ private static final Holder HOLDER = new Holder(); } }
-
⚠️以上的方式,有效但是不安全!,因为有反射declaredConstructor.setAccessible(true);//无视私有构造器
和declaredConstructor.newInstance()
就可以绕过单例模式直接new。
|
|
但是也有防范方法:
- 构造器判断
|
|
- 在构造器也进行一次判断
但是,如果对方两次构造都用newInstance()方法就可以绕过单例模式
|
|
- 标志位判断:增加一个
bool
变量,构造好后设为true。- 同理,使用
Field
反射也可以更改变量,同样不安全
- 同理,使用
所以只要有反射就无法完成防范,但是反射原理也告诉了我们解决方法:
|
|
newInstance
函数有一句if ((clazz.getModifiers() & Modifier.ENUM) != 0)
,如果是枚举则不会构造。
所以我们可以用枚举类做一个单例模式
|
|
使用反射试一下:
|
|
EnumSingle.class.getDeclaredConstructor(String.class, int.class);
一定要String.class, int.class,这是Enum类的构造方法
会报错:
|
|
枚举类型的特性:
- 类加载机制:
- 枚举类型在被加载时,JVM会确保它们的实例是唯一的。在枚举类被初始化时,所有的枚举实例都会被创建,并且这个过程是线程安全的。
- 由于枚举类型的类加载机制,每个枚举常量都是一个
public static final
实例,并且只能被实例化一次。- 序列化:
- 枚举类型的序列化机制保证了枚举实例在反序列化过程中不会创建新的实例。JVM会确保反序列化的枚举实例与原始实例是同一个对象。
- 枚举的
readResolve
方法会自动处理序列化和反序列化,以保证返回同一个枚举实例。