单例模式中最重要的思想是:构造器私有,因此能保证我们的内存中只有一个对象。
单例模式分为懒汉式和饿汉式
1.饿汉式:顾名思义,很饿,上来就吃。
package cn.com; //饿汉式单例,在程序运行时,无论是否需要 都会创建对象,因此 可能会浪费空间 public class HungryMan { //构造器私有,无法new 这个对象 private HungryMan() { } private static HungryMan HUNGRY = new HungryMan(); private static HungryMan getInstance() { return HUNGRY; } //测试后 得到的 对象的hashcode 一致,说明是同一个对象 public static void main(String[] args) { HungryMan instance1 = HungryMan.getInstance(); HungryMan instance2 = HungryMan.getInstance(); HungryMan instance3 = HungryMan.getInstance(); // 打印hashcode System.out.println(instance1); System.out.println(instance1); System.out.println(instance1); } } 运行结果: 三个对象的hashcode 值一样,说明是指向了同一个对象 cn.com.Hungry@1b6d3586 cn.com.Hungry@1b6d3586 cn.com.Hungry@1b6d35862.懒汉式单例
package cn.com; public class LazyMan1 { //构造器私有 private LazyMan1() { System.out.println(Thread.currentThread().getName() + " 正在运行!"); } //先不创建 ,等使用的时候再去创建 private static LazyMan1 man; public static LazyMan1 getInstance() { if (man == null) { man = new LazyMan1(); } return man; } /** * 此代码是线程不安全的,只在单线程下可以 * 例如 */ public static void main(String[] args) { for (int i = 0; i < 5; i++) { new Thread(() -> { LazyMan1.getInstance(); }).start(); } } } 运行结果Thread-0 正在运行! Thread-3 正在运行! Thread-2 正在运行! Thread-1 正在运行!
单例模式下,存在多个线程,明显不符合单例模式要求。下面对代码进行改进
3.懒汉式单例 改进
注: new LazyMan();不是原子性操作,实际底层有三步,
1.分配内存空间
2.执行构造方法,初始化对象
3.把对象指向这个空间
当A线程执行方法时,可能在执行 1---3---2 ;如果此时B线程进入,由于A线程已经将空对象指向内存空间,因此B将会得到一个null的对象。所以,必须使用volatile 修饰符 避免 被指令重排
package cn.com; //懒汉式单例 public class LazyMan { //构造器私有 private LazyMan() { System.out.println(Thread.currentThread().getName() + "is ok "); } //volatile 避免被指令重排 private volatile static LazyMan lanzMan; //双重检测锁 模式的懒汉式单例 ----DCL 懒汉式 public static LazyMan getInstance() { if (lanzMan == null) { synchronized (LazyMan.class) { if (lanzMan == null) { lanzMan = new LazyMan(); } } } return lanzMan; } //多线程并发 public static void main(String[] args) { for (int i = 0; i < 5; i++) { new Thread(() -> { LazyMan.getInstance(); }).start(); } } } 多次运行结果均为Thread-0is ok ;说明在多线程下是安全的。4.尽管我们使用了双重检测锁,但是由于反射的存在,上述代码仍然是不安全的。例如,我们通过反射机制来破坏单例模式。
package cn.com; import java.lang.reflect.Constructor; //懒汉式单例 public class LazyMan { //构造器私有 private LazyMan() { System.out.println(Thread.currentThread().getName() + "is ok "); } //volatile 避免被指令重排 private volatile static LazyMan lanzMan; //双重检测锁 模式的懒汉式单例 ----DCL 懒汉式 public static LazyMan getInstance() { if (lanzMan == null) { synchronized (LazyMan.class) { if (lanzMan == null) { lanzMan = new LazyMan(); /** * new LazyMan();不是原子性操作; * 1.分配内存空间 * 2.执行构造方法,初始化对象 * 3.把对象指向这个空间 */ } } } return lanzMan; } //由于反射的存在,即使 使用双重检测锁 ,在多线程下也是不安全的 public static void main(String[] args) throws Exception { //实例1 LazyMan lazyMan1 = LazyMan.getInstance(); /** * 通过反射破坏单例模式 */ //获取空参构造器 Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true);// 可以无视私有构造器 LazyMan lazyMan2 = declaredConstructor.newInstance(); System.out.println(lazyMan1.hashCode()); System.out.println(lazyMan2.hashCode()); } } 运行结果460141958 1163157884
由此可见,单例模式下 我们获取的是两个不同的对象。
解决办法:在构造器加synchronized,又双重检测锁升级为三重检测锁
//构造器私有 private LazyMan() { synchronized (LazyMan.class) { if (lanzMan != null) { throw new RuntimeException("不要来搞破坏"); } } }再执行代码,结果:
上述解决办法可行吗?再来。。。
我们将获取对象的方式全改为 反射获取
//由于反射的存在,即使 使用双重检测锁 ,在多线程下也是不安全的 public static void main(String[] args) throws Exception { Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true);// 可以无视私有构造器 LazyMan lazyMan1 = declaredConstructor.newInstance(); LazyMan lazyMan2 = declaredConstructor.newInstance(); System.out.println(lazyMan1.hashCode()); System.out.println(lazyMan2.hashCode()); }执行结果: 发现 又获取到了不同对象。。。。。
解决办法:通过在构造器增加标志位
package cn.com; import sun.nio.cs.FastCharsetProvider; import java.lang.reflect.Constructor; //懒汉式单例 public class LazyMan { private static boolean str = false; //构造器私有 private LazyMan() { synchronized (LazyMan.class) { if (str == false) { str = true; } else { throw new RuntimeException("不要来搞破坏"); } } } private volatile static LazyMan lanzMan; public static LazyMan getInstance() { if (lanzMan == null) { synchronized (LazyMan.class) { if (lanzMan == null) { lanzMan = new LazyMan(); } } } return lanzMan; } public static void main(String[] args) throws Exception { //获取空参构造器 Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true);// 可以无视私有构造器 LazyMan lazyMan1 = declaredConstructor.newInstance(); LazyMan lazyMan2 = declaredConstructor.newInstance(); System.out.println(lazyMan1.hashCode()); System.out.println(lazyMan2.hashCode()); } } 执行结果,问题已解决。 我想说,,,其实这种方法仍然是不安全的,后面讲如何通过枚举类解决此问题