饿汉单例模式
package com.std.www.singletonmode;
import java.util.UUID;
public class ScpD {
private final static ScpD scpD=new ScpD();
public static ScpD getScpD() {
return scpD;
}
}
类一经创建就会给对象分配内存,这种方式会造成不必要的内存浪费
懒汉单例模式
package com.std.www.singletonmode;
import java.util.UUID;
public class ScpD {
private static ScpD scpD;
private ScpD(){
}
public static ScpD getScpD() {
if(scpD==null) scpD=new ScpD();
return scpD;
}
}
这里构造方法被定义为私有,只有在调用的时候才会给对象分配内存,但是该方式在多线程下有问题
package com.std.www.singletonmode;
import java.util.UUID;
public class ScpD {
private static ScpD scpD;
private final String DId;
private ScpD(){
this.DId = "D级人员:编号" + UUID.randomUUID().toString().substring(0, 4);
}
public static ScpD getScpD() {
if(scpD==null) scpD=new ScpD();
return scpD;
}
public void show(){
System.out.println(this.DId);
}
}
package com.std.www;
import com.std.www.singletonmode.ScpD;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
public class Main {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
for(int i=0;i<10;i++){
new Thread(()->{ScpD scpD=ScpD.getScpD();scpD.show();}).start();
}
}
}
可以发现创建了10个对象,并不是单例模式,对此我们需要给该类上一把锁
package com.std.www.singletonmode;
import java.util.UUID;
public class ScpD {
private volatile static ScpD scpD;//volatile关键字防止指令重排
private final String DId;
private ScpD(){
this.DId = "D级人员:编号" + UUID.randomUUID().toString().substring(0, 4);
}
public static ScpD getScpD() {
if(scpD==null)//双重检测锁模式DCL懒汉式单例,synchronized用于同步多线程,表示该代码块在该类中一次只能由一个线程执行,volatile防止指令重排
synchronized (ScpD.class) {
//正常指令顺序:1.分配内存空间2.初始化对象3.对象引用该内存
//多线程可能导致出现空指针异常即改变了原有的执行顺序,比如先引用该内存在对象初始化之前
if (scpD == null) {
scpD = new ScpD();
}
}
return scpD;
}
public void show(){
System.out.println(this.DId);
}
}
package com.std.www;
import com.std.www.singletonmode.ScpD;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
public class Main {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
for(int i=0;i<10;i++){
new Thread(()->{ScpD scpD=ScpD.getScpD();scpD.show();}).start();
}
}
}
上面虽然解决的多线程问题, 但是依然不安全,看下面的例子
package com.std.www;
import com.std.www.singletonmode.ScpD;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
public class Main {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
ScpD scpD=ScpD.getScpD();
//反射获取构造器
Constructor<? extends ScpD> scpDecCon = scpD.getClass().getDeclaredConstructor();
//设置私有构造器可访问
scpDecCon.setAccessible(true);
//通过构造器新建对象
ScpD scpD1=scpDecCon.newInstance();
scpD.show();
scpD1.show();
}
}
这里发现通过反射依然可以破坏掉单例模式的安全性,下面的是解决办法,修改构造器再次判断
synchronized (ScpD.class) {//三重检测
if (scpD != null) {
throw new RuntimeException("检测到有反射企图破坏单例模式");
}
this.DId = "D级人员:编号" + UUID.randomUUID().toString().substring(0, 4);
}
不过这里依然有一个小问题,那就是如果一开始就没有使用单例创建对象的话
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
//反射获取构造器
Constructor<? extends ScpD> scpDecCon = ScpD.class.getDeclaredConstructor();
//设置私有构造器可访问
scpDecCon.setAccessible(true);
//通过构造器新建对象
ScpD scpD1=scpDecCon.newInstance();
ScpD scpD2=scpDecCon.newInstance();
scpD1.show();
scpD2.show();
}
对此我们可以设立一个判断依据
private ScpD() {
private static boolean flag=false;
synchronized (ScpD.class) {//三重检测
if (flag) {
throw new RuntimeException("检测到有反射企图破坏单例模式");
}
else flag=true;
this.DId = "D级人员:编号" + UUID.randomUUID().toString().substring(0, 4);
}
}
不过这里依然有破解办法
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
//反射获取私有属性字段
Field flag=ScpD.class.getDeclaredField("flag");
//设置字段可访问
flag.setAccessible(true);
//反射获取构造器
Constructor<? extends ScpD> scpDecCon = ScpD.class.getDeclaredConstructor();
//设置私有构造器可访问
scpDecCon.setAccessible(true);
//通过构造器新建对象
//flag:false
ScpD scpD1=scpDecCon.newInstance();
//flag:true
//设置字段属性
flag.set(flag,false);
//flag:false
ScpD scpD2=scpDecCon.newInstance();
//flag:true
scpD1.show();
scpD2.show();
}
这里我们可以搞一个小小的优化
private static enum Flag{
ISNULL,
NOTNULL
}
private static Flag flag=Flag.ISNULL;
private ScpD() {
synchronized (ScpD.class) {//三重检测
if (flag==Flag.ISNULL) {
throw new RuntimeException("检测到有反射企图破坏单例模式");
}
else flag=Flag.NOTNULL;
this.DId = "D级人员:编号" + UUID.randomUUID().toString().substring(0, 4);
}
}
这样要想改变字段的值必须先获取得到该私有的枚举类,然后通过分析得到枚举类中的值,虽然也能通过反射改变但是要比前者复杂不少
通过源码我们可以发现
反射是无法构造一个枚举类型的对象,因此如果可以选择枚举类型作为单例模式是最为安全的
由此可得单例模式的安全性受诸多因数影响
标签:scpD,模式,flag,详解,static,单例,import,ScpD,public From: https://www.cnblogs.com/liyiyang/p/17713176.html