- 什么是单例模式:
单例模式是指在内存中只会创建且仅创建一次对象的设计模式 - 单例模式特点:
(1)单例类只能有一个实例。
(2)单例类必须自己创建自己的唯一实例。
(3)单例类必须给所有其他对象提供这一实例。
public class Hungry {
private final static Hungry HUNGRY = new Hungry();
//构造器私有,其他人就无法new一个对象,保证内存中只有一个对象
private Hungry() {
}
//提供实例的方法,static:保证可见性
public static Hungry getHungry() {
return HUNGRY;
}
}
在类创建的时候就已经实例化,不会改变,所以线程安全。但是,如下
public class Hungry {
private final static Hungry HUNGRY = new Hungry();
private byte[] bytes = new byte[1024 * 1024];
private byte[] bytes2 = new byte[1024 * 1024];
private byte[] bytes3 = new byte[1024 * 1024];
private byte[] bytes4 = new byte[1024 * 1024];
private byte[] bytes5 = new byte[1024 * 1024];
private Hungry() {
}
public static Hungry getHungry() {
return HUNGRY;
}
}
可能会浪费空间。
懒汉式单例
public class LazyMan {
private LazyMan() {
}
private static LazyMan LAYMAN = null;
//用的时候创建
public static LazyMan getLazyMan() {
if (LAYMAN == null)
LAYMAN = new LazyMan();
return LAYMAN;
}
}
但是,它是线程不安全的
测试:
public class LazyMan {
private LazyMan() {
System.out.println(Thread.currentThread().getName() + "创建");
}
private static LazyMan LAYMAN = null;
public static LazyMan getLazyMan() {
if (LAYMAN == null)
LAYMAN = new LazyMan();
return LAYMAN;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
LazyMan.getLazyMan();
}).start();
}
}
}
控制台:
Thread-2创建 Thread-1创建 Thread-0创建
解决办法:加锁
双重检查锁模式
public class LazyMan {
private LazyMan() {
System.out.println(Thread.currentThread().getName() + "创建");
}
private static LazyMan LAYMAN = null;
//双重检测锁模式的 懒汉式单例 DCL懒汉式
public static LazyMan getLazyMan() {
if (LAYMAN == null) {
synchronized (LazyMan.class) {
if (LAYMAN == null) {
LAYMAN = new LazyMan();
}
}
}
return LAYMAN;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
LazyMan.getLazyMan();
}).start();
}
}
}
问题解决。
但是,解析LAYMAN = new LazyMan();,它不是一个原子性的操作,包括以下三部分。1:分配内存空间 2:执行构造方法,初始化对象 3:把这个对象指向这个空间 我们期望是1 2 3 顺序执行,但在指令重排后CPU不一定按1 2 3 顺序执行,有可能 1 3 2。当线程A执行 1 3 2 顺序的2时,线程B进入,因为这个对象已经指向这个空间所以线程B判断出LAYMAN非空,执行了返回语句,但是实际上LAYMAN并未初始化,所以会出现错误。
解决办法:
volatile :防止指令重排
public class LazyMan {
private LazyMan() {
System.out.println(Thread.currentThread().getName() + "创建");
}
private volatile static LazyMan LAYMAN = null;
public static LazyMan getLazyMan() {
if (LAYMAN == null) {
synchronized (LazyMan.class) {
if (LAYMAN == null) {
LAYMAN = new LazyMan();
}
}
}
return LAYMAN;
}
}
问题解决。
道高一尺魔高一丈,反射破坏
import java.lang.reflect.Constructor;
public class LazyMan {
private LazyMan() {
//System.out.println(Thread.currentThread().getName() + "创建");
}
private volatile static LazyMan LAYMAN = null;
public static LazyMan getLazyMan() {
if (LAYMAN == null) {
synchronized (LazyMan.class) {
if (LAYMAN == null) {
LAYMAN = new LazyMan();
}
}
}
return LAYMAN;
}
public static void main(String[] args) throws Exception {
LazyMan lazyMan = LazyMan.getLazyMan();
Constructor declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyMan lazyMan1 = declaredConstructor.newInstance();
LazyMan lazyMan2 = declaredConstructor.newInstance();
System.out.println(lazyMan);
System.out.println(lazyMan1);
System.out.println(lazyMan2);
}
}
控制台:
cn.smxy.ln.LazyMan@119d7047 cn.smxy.ln.LazyMan@776ec8df cn.smxy.ln.LazyMan@4eec7777
解决办法:
加锁
import java.lang.reflect.Constructor;
public class LazyMan {
private LazyMan() {
//System.out.println(Thread.currentThread().getName() + "创建");
synchronized (LazyMan.class) {
if (LAYMAN != null) {
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
}
private volatile static LazyMan LAYMAN = null;
public static LazyMan getLazyMan() {
if (LAYMAN == null) {
synchronized (LazyMan.class) {
if (LAYMAN == null) {
LAYMAN = new LazyMan();
}
}
}
return LAYMAN;
}
public static void main(String[] args) throws Exception {
LazyMan lazyMan = LazyMan.getLazyMan();
Constructor declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyMan lazyMan1 = declaredConstructor.newInstance();
LazyMan lazyMan2 = declaredConstructor.newInstance();
System.out.println(lazyMan);
System.out.println(lazyMan1);
System.out.println(lazyMan2);
}
}
问题解决。
但是,如果不创建lazyMan 依然有问题 如下:
import java.lang.reflect.Constructor;
public class LazyMan {
private LazyMan() {
//System.out.println(Thread.currentThread().getName() + "创建");
synchronized (LazyMan.class) {
if (LAYMAN != null) {
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
}
private volatile static LazyMan LAYMAN = null;
public static LazyMan getLazyMan() {
if (LAYMAN == null) {
synchronized (LazyMan.class) {
if (LAYMAN == null) {
LAYMAN = new LazyMan();
}
}
}
return LAYMAN;
}
public static void main(String[] args) throws Exception {
//LazyMan lazyMan = LazyMan.getLazyMan();
Constructor declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyMan lazyMan1 = declaredConstructor.newInstance();
LazyMan lazyMan2 = declaredConstructor.newInstance();
//System.out.println(lazyMan);
System.out.println(lazyMan1);
System.out.println(lazyMan2);
}
}
控制台:
cn.smxy.ln.LazyMan@119d7047 cn.smxy.ln.LazyMan@776ec8df
单例模式又被破坏
解决办法:
添加标志位
import java.lang.reflect.Constructor;
public class LazyMan {
private static boolean xnn = false;
private LazyMan() {
//System.out.println(Thread.currentThread().getName() + "创建");
synchronized (LazyMan.class) {
if (xnn == false) {
xnn = true;
} else {
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
}
private volatile static LazyMan LAYMAN = null;
public static LazyMan getLazyMan() {
if (LAYMAN == null) {
synchronized (LazyMan.class) {
if (LAYMAN == null) {
LAYMAN = new LazyMan();
}
}
}
return LAYMAN;
}
public static void main(String[] args) throws Exception {
//LazyMan lazyMan = LazyMan.getLazyMan();
Constructor declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyMan lazyMan1 = declaredConstructor.newInstance();
LazyMan lazyMan2 = declaredConstructor.newInstance();
//System.out.println(lazyMan);
System.out.println(lazyMan1);
System.out.println(lazyMan2);
}
}
问题解决。
实际上Java反射机制仍能破坏单例模式。
但是枚举Enum可以防止反射和反序列化时破坏单例。
实例:
public enum EnumSingle {
INSTANCE;
public EnumSingle getIntance() {
return INSTANCE;
}
}
总结
(1)单例模式常见的写法有两种:懒汉式,饿汉式
(2)懒汉式:在类初始化时,已经自行实例化
(3)饿汉式:在第一次调用的时候实例化自己
(4)在开发中如果对内存要求非常高,那么使用懒汉式写法,可以在特定时候才创建该对象;
(5)如果对内存要求不高使用饿汉式写法,因为简单不易出错,且没有任何并发安全和性能问题
(6)在单例对象上添加volatile关键字可以防止指令重排序
(7)最优雅的实现方式是使用枚举,其代码精简,没有线程安全问题,且 Enum 类内部防止反射和反序列化时破坏单例。



