单例模式中最重要的思想是:构造器私有,因此能保证我们的内存中只有一个对象。
单例模式分为懒汉式和饿汉式
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@1b6d3586
2.懒汉式单例
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());
}
}
执行结果,问题已解决。 我想说,,,其实这种方法仍然是不安全的,后面讲如何通过枚举类解决此问题

本文深入解析单例模式的实现方式,包括懒汉式和饿汉式的优缺点,并探讨了线程安全问题及解决方案,最后讨论了反射机制对单例模式的影响。

1636

被折叠的 条评论
为什么被折叠?



