public class Hungry {
private Hungry(){
}
private final static Hungry hungry=new Hungry();
public static Hungry getInstance(){
return hungry;
}
}
缺点:浪费存储空间
public class Holder {
private Holder(){
}
public static Holder getInstance(){
return InnerClass.holder;
}
public static class InnerClass{
private static Holder holder=new Holder();
}
}
此方法了解一下,即可,也是会浪费空间的
懒汉模式的代码我是按照逐步升级维护的方式来演示的
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName());
}
private static LazyMan lazyMan;
public static LazyMan getInstance(){
if(lazyMan==null){
lazyMan=new LazyMan();
}
return lazyMan;
}
}
这种方式在单线程模式下可以凑合着用,但是到了多线程的环境下,就会被破坏了
以下代码是我简单的复现多线程来测试top1模式下的懒汉模式的缺陷
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan instance = LazyMan.getInstance();
}).start();
}
}
既然多线程会破坏第一步写的懒汉下的单例模式,我们可以进行加锁操作
private LazyMan(){
System.out.println(Thread.currentThread().getName());
}
private static LazyMan lazyMan;
public static LazyMan getInstance(){
if(lazyMan==null){
synchronized (LazyMan.class){
if(lazyMan==null){
lazyMan=new LazyMan();
}
}
}
return lazyMan;
}
这次你可以自己复现以下,不论运行多少次都不会出现上面那种情况了。因为我们加了锁,并且做了两次判断lazyMan是否为空的操作
那么新的问题又来了,虽然在这种情况下看似趋于完美了,但是你有没有考虑过
反射会破环这个安全性呢?
除了加锁,其实还个小细节,我们想一下,哪还有问题呢?
lazyMan=new LazyMan();
- 1
这一步存在安全隐患!
我们知道new相对内存级别来说不是原子操作new 的完整构建过程可以分为三步
- 分配内存空间
- 执行构造器,初始化对象
- 将初始化的对象指向我们分配的内存空间
这三部在多线程模式下,可不一定是按照 1,2,3,的顺序执行的,有可能线程1执行的步骤是1 3 2 那么线程1执行到3步骤的时候,(不加锁情况下)这时候线程2进来, 发现初始化对象已经指向了内存空间于是就返回了,但其实还没有初始化对象!
为了解决
原子性问题带来的安全隐患,我们可以使用private volatile static LazyMan lazyMan;
- 1
.
volatile1.被设计用来修饰被不同的线程访问和修改的变量 2、被volatile修饰的变量,系统每次用到它的时候,都是直接从对应的内存中取的,而不会利用缓存。这样就解决了多线程访问同一个变量的时候,所产生的不一致性 3、被volatile修饰的变量,所有线程在任何时候所看到的变量的值都是相同的
- 1
- 2
- 3
public static void main(String[] args) throws Exception {
LazyMan lazyMan1 = LazyMan.getInstance();
Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor();
constructor.setAccessible(true);
LazyMan lazyMan2 = constructor.newInstance();
System.out.println(lazyMan1);
System.out.println(lazyMan2);
}
看一下上面我写的测试代码,这次我通过反射的方式来获取这个实例,打印的结果表名两者不是同一对象
那么有没有解决办法呢?
道高一尺,魔高一丈
我们思考一下反射是如何创建实例的?
反射的核心是先获取构造器,然后再通过构造器来获取实例。
对应我们这个代码就是 反射获取我们的无参构造器,然后通过这个无参构造器来创建实例
以子之矛攻子之盾我们可以在无参构造器上加锁,如果创建LazyMan已经存在,抛异常
private LazyMan(){
synchronized (LazyMan.class){
if(lazyMan!=null) {
synchronized (LazyMan.class){
throw new RuntimeException("不要试图用反射来破环单例模式");
}
}
}
}
但是此场景下,还是会出问题,就是我们用反射来获取两个实例对象
public static void main(String[] args) throws Exception {
// LazyMan lazyMan1 = LazyMan.getInstance();
Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor();
constructor.setAccessible(true);
LazyMan lazyMan1 = constructor.newInstance();
LazyMan lazyMan2 = constructor.newInstance();
System.out.println(lazyMan1);
System.out.println(lazyMan2);
}
此时连用两次反射创建实例还是会破环我们写的单例模式
有一个方法可以解决此问题,前提是黑客或者客户不会通过反编译来获取我们定义的变量
private static boolean flag=false;
private LazyMan(){
if (!flag){
flag=true;
synchronized (LazyMan.class){
if(lazyMan!=null) {
synchronized (LazyMan.class){
throw new RuntimeException("不要试图用反射来破环单例模式");
}
}
}
}else {
throw new RuntimeException("不要试图用反射来破环单例模式");
}
}
我说过,前期是客户无法得知我们设置的 flag 变量(比如我们可以通过加密处理)
但是还是有可能被黑客通过反射获取的这个变量的,那么我们的单例花式还是会被破坏
public static void main(String[] args) throws Exception {
// LazyMan lazyMan1 = LazyMan.getInstance();
Class<LazyMan> lazyManClass = LazyMan.class;
Field flag = lazyManClass.getDeclaredField("flag");
flag.setAccessible(true);
Constructor<LazyMan> constructor = lazyManClass.getDeclaredConstructor(null);
constructor.setAccessible(true);
LazyMan lazyMan1 = constructor.newInstance();
flag.set(lazyManClass,false);
LazyMan lazyMan2 = constructor.newInstance();
System.out.println(lazyMan1);
System.out.println(lazyMan2);
}
对此:我们就必须换一个方法了,枚举单例
枚举单例我们查看反射当中 newInstance源码,可以看到
Cannot reflectively create enum objects
- 1
源码当中就说了:不能试图通过反射来获取枚举对象
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance() {
return INSTANCE;
}
public static void main(String[] args) throws Exception {
Constructor<EnumSingle> con = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
con.setAccessible(true);
EnumSingle enumSingle1 = con.newInstance();
EnumSingle enumSingle2 = con.newInstance();
System.out.println(enumSingle1);
System.out.println(enumSingle2);
//报错
}
}