JMM是一种Java内存模型,是一种概念性的约定,而不是实际存在的东西。
线程解锁前,必须把变量立即刷回主存;
我们知道,一个线程工作时,会将主存中的变量复制一份给线程自己的内存,作为一个副本,线程对此变量的操作,都是在副本上的操作,所以当线程运行完毕,解锁的时候,必须将副本的值同步回主存。
线程加锁前,必须读取主存中最新的变量值,然后复制到自己的工作内存中;
加锁和解锁是同一把锁。
先上代码:
package com.zhan.juc.volatiletest;
import java.util.concurrent.TimeUnit;
/**
* @Author Zhanzhan
* @Date 2021/1/28 21:07
*/
public class JMMDemo {
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 = 1;
System.out.println(num);
}
}
我们看上面的代码,可以看到,有一个线程,里面while循环,不停的判断num的值,然后在线程的下面将num值赋为1,那么这时我们开出来的这个线程会停掉吗?
看结果:
我们发现,并没有停,这是为什么呢?就像上面说的那样,线程将num的值从主存中复制了一份,然后主存中num值的变动,线程并没有看到。
那么要如何解决?接下来看:
package com.zhan.juc.volatiletest;
import java.util.concurrent.TimeUnit;
/**
* @Author Zhanzhan
* @Date 2021/1/28 21:07
*/
public class JMMDemo {
private volatile 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 = 1;
System.out.println(num);
}
}
我们看到,上面的代码中num用了volatile来修饰,然后看结果:
我们发现,结果符合我们的预期,线程看到了主存中num的值的变动。
什么是原子性:不可分割,线程A在执行任务的时候,不能被打扰,也不能被分割,要么同时成功,要么同时失败。
废话不多说,上代码:
package com.zhan.juc.volatiletest;
/**
* 测试volatile不保证原子性
*
* @Author Zhanzhan
* @Date 2021/1/28 21:19
*/
public class Demo {
private volatile static int num = 0;
public static void add() {
num++;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
// 判断当前的线程是否大于两条
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + " num=" + num);
}
}
如果线程能保证原子性,那么输出的结果应该是num=20000,现在我们看结果:
发现,volatile不能保证原子性。
那这时我们就有一个问题,如果不使用lock和synchronized,怎么保证原子性?
上代码:
package com.zhan.juc.volatiletest;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 测试volatile不保证原子性
*
* @Author Zhanzhan
* @Date 2021/1/28 21:19
*/
public class Demo {
// private volatile static int num = 0;
private volatile static AtomicInteger num = new AtomicInteger();
public static void add() {
// num++; // 不是一个原子性操作
num.getAndIncrement();
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
// 判断当前的线程是否大于两条
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + " num=" + num);
}
}
什么是指令重排:计算机并不是按照我们写的程序的顺序那样去执行的,会进行指令优化。
源代码 =》编译器优化的重排=》指令并行也可能的重排=》内存系统的重排=》执行
处理器在进行指令重排的时候,会考虑数据之间的依赖性。
int x = 1; // 1
int y = 2; // 2
x = x + 5; // 3
y = x * x; // 4
我们期望的顺序:1234
但可能编程的执行顺序:2134 1324
举例说明,指令重排可能引发的问题:
假设有两个线程A和B,然后有四个变量:a b x y
a b x y四个值默认为0
线程A | 线程B |
---|---|
x = a | y = b |
b = 1 | a = 2 |
线程A执行了两个操作:x = a 和 b = 1;
线程B执行了两个操作:y = b 和 a = 2;
在线程A执行 x = a后,线程B执行了 y = b,
这时我们预期的结果是 x = 0 和 y = 0;
但是现在线程A中的变量没有依赖关系,线程B中的变量也没有依赖关系,所以线程A和线程B就有可能发生指令重排,会造成如下操作:
线程A | 线程B |
---|---|
b = 1 | a = 2 |
x = a | y = b |
这时,得到的结果就为 x = 2 和 y = 1;
那么如何解决呢?
我们可以用volatile修饰,来避免指令重排,volatile会加一个内存屏障,来保证特定操作的执行顺序以及内存可见性。
这里我们跳过饿汉模式,直接上懒汉模式:
package com.zhan.juc.volatiletest.single;
/**
* 单例模式--懒汉
*
* @Author Zhanzhan
* @Date 2021/1/30 13:39
*/
public class LazyMan {
private static LazyMan lazyMan;
private LazyMan() {
System.out.println(Thread.currentThread().getName() + "ok");
}
/**
* 无检测的单例模式
*
* @return
*/
public static LazyMan getInstanceByNotSafe() {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
return lazyMan;
}
public static void main(String[] args) {
/**
* 测试多线程下,这种单例模式是否安全
*/
for (int i = 0; i < 10; i++) {
new Thread(() -> {
LazyMan.getInstanceByNotSafe();
}).start();
}
}
}
这样写对吗?看结果:
我们发现不是单例的,那么一般的懒汉模式要怎么写呢?
package com.zhan.juc.volatiletest.single;
/**
* 单例模式--懒汉
*
* @Author Zhanzhan
* @Date 2021/1/30 13:39
*/
public class LazyMan {
private static LazyMan lazyMan;
private LazyMan() {
System.out.println(Thread.currentThread().getName() + "ok");
}
/**
* 双重检测锁 的 懒汉单例模式 DCL懒汉式
* @return
*/
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
LazyMan.getInstance();
}).start();
}
}
}
看结果:
符合预期,是单例的。但是这里会有一个问题,new 单例对象的这个操作,不是一个原子操作。即 lazyMan = new LazyMan(); // 不是一个原子性操作
这一步经过了那几个操作:
我们正常认为的执行顺序是 1=》2=》3
但是有可能会指令重排,执行的顺序是 1=》3=》2
如果此时有A线程执行的顺序是1=》3=》2,那么执行到3这一步时,有个B线程也进来了,就会发现lazyMan 这个对象不为null,于是直接返回,但是此时lazyMan 还没完成构造,就会引发问题,所以,就要使用volatile来修饰 lazyMan ,禁止指令重排。于是代码如下:
package com.zhan.juc.volatiletest.single;
/**
* 单例模式--懒汉
*
* @Author Zhanzhan
* @Date 2021/1/30 13:39
*/
public class LazyMan {
private volatile static LazyMan lazyMan; // 使用volatile修饰,禁止指令重排
private LazyMan() {
System.out.println(Thread.currentThread().getName() + "ok");
}
/**
* 双重检测锁 的 懒汉单例模式 DCL懒汉式
* @return
*/
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
/**
* 1、分配内存空间;
* 2、执行构造方法,初始化对象;
* 3、把这个对象指向这个空间。
*/
lazyMan = new LazyMan(); // 不是一个原子性操作
}
}
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
LazyMan.getInstance();
}).start();
}
}
}
我们一般就认为,上面的那种单例模式,没啥毛病了。
但是!依然可以有方法破解上述的单例模式,怎么破解呢?用反射!
上代码:
package com.zhan.juc.volatiletest.single;
import java.lang.reflect.Constructor;
/**
* 单例模式--懒汉
*
* @Author Zhanzhan
* @Date 2021/1/30 13:39
*/
public class LazyMan {
private volatile static LazyMan lazyMan; // 使用volatile修饰,禁止指令重排
private LazyMan() {
System.out.println(Thread.currentThread().getName() + "ok");
}
/**
* 双重检测锁 的 懒汉单例模式 DCL懒汉式
* @return
*/
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
/**
* 1、分配内存空间;
* 2、执行构造方法,初始化对象;
* 3、把这个对象指向这个空间。
*/
lazyMan = new LazyMan(); // 不是一个原子性操作
}
}
}
return lazyMan;
}
public static void main(String[] args) throws Exception {
// 用反射破解单例模式
LazyMan instance = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null); // 获取构造器
declaredConstructor.setAccessible(true); // 无视私有构造器
LazyMan instance2 = declaredConstructor.newInstance(); // 通过反射创建对象
System.out.println(instance);
System.out.println(instance2);
}
}
看结果:
我们看到,这两个对象实例的hashCode不一样,是两个不同的对象实例,于是,我们得出结论,一般的懒汉模式就这样被反射破解了,那么有应对方法吗?既然是获取私有构造器,来创建实例,那么我们在私有构造器上也加上校验呢?来看:
package com.zhan.juc.volatiletest.single;
import java.lang.reflect.Constructor;
/**
* 单例模式--懒汉
*
* @Author Zhanzhan
* @Date 2021/1/30 13:39
*/
public class LazyMan {
private volatile static LazyMan lazyMan; // 使用volatile修饰,禁止指令重排
private LazyMan() {
synchronized (LazyMan.class){
if (lazyMan != null){
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
System.out.println(Thread.currentThread().getName() + "ok");
}
/**
* 双重检测锁 的 懒汉单例模式 DCL懒汉式
* @return
*/
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
/**
* 1、分配内存空间;
* 2、执行构造方法,初始化对象;
* 3、把这个对象指向这个空间。
*/
lazyMan = new LazyMan(); // 不是一个原子性操作
}
}
}
return lazyMan;
}
public static void main(String[] args) throws Exception {
// 用反射破解单例模式
LazyMan instance = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null); // 获取构造器
declaredConstructor.setAccessible(true); // 无视私有构造器
LazyMan instance2 = declaredConstructor.newInstance(); // 通过反射创建对象
System.out.println(instance);
System.out.println(instance2);
}
}
看结果:
我们发现,这种方法被抵挡了。那么,还有可能破解这种单例模式吗?
我们思考下,刚才那种破解,第一个对象实例是通过单例模式来创建的,第二个对象实例是通过反射来创建的,那么如果两个对象都通过反射来创建呢?上代码:
package com.zhan.juc.volatiletest.single;
import java.lang.reflect.Constructor;
/**
* 单例模式--懒汉
*
* @Author Zhanzhan
* @Date 2021/1/30 13:39
*/
public class LazyMan {
private volatile static LazyMan lazyMan; // 使用volatile修饰,禁止指令重排
private LazyMan() {
synchronized (LazyMan.class){
if (lazyMan != null){
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
System.out.println(Thread.currentThread().getName() + "ok");
}
/**
* 双重检测锁 的 懒汉单例模式 DCL懒汉式
* @return
*/
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
/**
* 1、分配内存空间;
* 2、执行构造方法,初始化对象;
* 3、把这个对象指向这个空间。
*/
lazyMan = new LazyMan(); // 不是一个原子性操作
}
}
}
return lazyMan;
}
public static void main(String[] args) throws Exception {
// 用反射破解单例模式
// LazyMan instance = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null); // 获取构造器
declaredConstructor.setAccessible(true); // 无视私有构造器
LazyMan instance2 = declaredConstructor.newInstance(); // 通过反射创建对象
LazyMan instance = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
}
看结果:
非常的amazing啊,竟然又被破解了,既然这样,我就较上劲儿了,还有什么方法能抵挡吗?有的,我们用下红绿灯模式,就是通过设置一个标识,来再次进行校验,上代码:
package com.zhan.juc.volatiletest.single;
import java.lang.reflect.Constructor;
/**
* 单例模式--懒汉
*
* @Author Zhanzhan
* @Date 2021/1/30 13:39
*/
public class LazyMan {
private static boolean flag = false;
private volatile static LazyMan lazyMan; // 使用volatile修饰,禁止指令重排
private LazyMan() {
synchronized (LazyMan.class){
if (!flag){
flag = true;
} else {
throw new RuntimeException("不要试图使用反射破坏单例");
}
}
System.out.println(Thread.currentThread().getName() + "ok");
}
/**
* 双重检测锁 的 懒汉单例模式 DCL懒汉式
* @return
*/
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
/**
* 1、分配内存空间;
* 2、执行构造方法,初始化对象;
* 3、把这个对象指向这个空间。
*/
lazyMan = new LazyMan(); // 不是一个原子性操作
}
}
}
return lazyMan;
}
public static void main(String[] args) throws Exception {
// 用反射破解单例模式
// LazyMan instance = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null); // 获取构造器
declaredConstructor.setAccessible(true); // 无视私有构造器
LazyMan instance2 = declaredConstructor.newInstance(); // 通过反射创建对象
LazyMan instance = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
}
看结果:
符合预期,抵挡了这种方法,那到现在了,还有可能破解单例吗?有的,就是,如果我知道了你设置的标识符,我直接对标识符进行修改呢?
package com.zhan.juc.volatiletest.single;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
/**
* 单例模式--懒汉
*
* @Author Zhanzhan
* @Date 2021/1/30 13:39
*/
public class LazyMan {
private static boolean flag = false;
private volatile static LazyMan lazyMan; // 使用volatile修饰,禁止指令重排
private LazyMan() {
synchronized (LazyMan.class){
if (!flag){
flag = true;
} else {
throw new RuntimeException("不要试图使用反射破坏单例");
}
}
System.out.println(Thread.currentThread().getName() + "ok");
}
/**
* 双重检测锁 的 懒汉单例模式 DCL懒汉式
* @return
*/
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
/**
* 1、分配内存空间;
* 2、执行构造方法,初始化对象;
* 3、把这个对象指向这个空间。
*/
lazyMan = new LazyMan(); // 不是一个原子性操作
}
}
}
return lazyMan;
}
public static void main(String[] args) throws Exception {
// 用反射破解单例模式
// LazyMan instance = LazyMan.getInstance();
Field flag = LazyMan.class.getDeclaredField("flag");// 假设我们知道了标识符的名称
flag.setAccessible(true); // 无视私有权限
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null); // 获取构造器
declaredConstructor.setAccessible(true); // 无视私有构造器
LazyMan instance2 = declaredConstructor.newInstance(); // 通过反射创建对象
// 更改标识符
flag.set(instance2, false);
LazyMan instance = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
}
看结果:
我们发现,又被破坏了。那么我们如何解决呢?看反射中newInstance()的源码:
/**
* Uses the constructor represented by this {@code Constructor} object to
* create and initialize a new instance of the constructor's
* declaring class, with the specified initialization parameters.
* Individual parameters are automatically unwrapped to match
* primitive formal parameters, and both primitive and reference
* parameters are subject to method invocation conversions as necessary.
*
* If the number of formal parameters required by the underlying constructor
* is 0, the supplied {@code initargs} array may be of length 0 or null.
*
*
If the constructor's declaring class is an inner class in a
* non-static context, the first argument to the constructor needs
* to be the enclosing instance; see section 15.9.3 of
* The Java™ Language Specification.
*
*
If the required access and argument checks succeed and the
* instantiation will proceed, the constructor's declaring class
* is initialized if it has not already been initialized.
*
*
If the constructor completes normally, returns the newly
* created and initialized instance.
*
* @param initargs array of objects to be passed as arguments to
* the constructor call; values of primitive types are wrapped in
* a wrapper object of the appropriate type (e.g. a {@code float}
* in a {@link java.lang.Float Float})
*
* @return a new object created by calling the constructor
* this object represents
*
* @exception IllegalAccessException if this {@code Constructor} object
* is enforcing Java language access control and the underlying
* constructor is inaccessible.
* @exception IllegalArgumentException if the number of actual
* and formal parameters differ; if an unwrapping
* conversion for primitive arguments fails; or if,
* after possible unwrapping, a parameter value
* cannot be converted to the corresponding formal
* parameter type by a method invocation conversion; if
* this constructor pertains to an enum type.
* @exception InstantiationException if the class that declares the
* underlying constructor represents an abstract class.
* @exception InvocationTargetException if the underlying constructor
* throws an exception.
* @exception ExceptionInInitializerError if the initialization provoked
* by this method fails.
*/
@CallerSensitive
@ForceInline // to ensure Reflection.getCallerClass optimization
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, clazz, modifiers);
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
上代码:
package com.zhan.juc.volatiletest.single;
/**
* 枚举实现单例模式
* @Author Zhanzhan
* @Date 2021/1/30 14:34
*/
public class EnumSingle {
enum EnumTest{
INSTANCE;
private EnumSingle enumSingle = null;
private EnumTest(){
enumSingle = new EnumSingle();
}
public EnumSingle getEnumSingle(){
return enumSingle;
}
}
public static void main(String[] args){
EnumSingle instance1 = EnumTest.INSTANCE.getEnumSingle();
EnumSingle instance2 = EnumTest.INSTANCE.getEnumSingle();
System.out.println(instance1);
System.out.println(instance2);
}
}
看结果:
符合预期,是同一个对象。
看反射是否能破坏此单例模式:
package com.zhan.juc.volatiletest.single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* 枚举实现单例模式
* @Author Zhanzhan
* @Date 2021/1/30 14:34
*/
public class EnumSingle {
enum EnumTest{
INSTANCE;
private EnumSingle enumSingle = null;
private EnumTest(){
enumSingle = new EnumSingle();
}
public EnumSingle getEnumSingle(){
return enumSingle;
}
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<EnumTest> declaredConstructor = EnumTest.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumTest instance1 = declaredConstructor.newInstance();
EnumTest instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}