大纲
-
饿汉式单例
-
懒汉式单例
-
懒汉式(双重检测锁模式)
-
破环单例模式
-
防止破环单例模式
单例模式属于创建型的设计模式,无需实例化对象,同时可以确保只有单个对象被创建。
特点如下:
-
单例类只能有一个实例
-
单例类必须自己创建自己的唯一实例(构造器私有)
-
单例类必须给所有其他对象提供这一实例
优点:
-
内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例
-
避免对资源的多重占用(比如写文件操作)
缺点:
- 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化
饿汉式
饿汉式,在类加载时完成初始化。代码示例如下:
public class Singleton {
// 直接初始化对象
private final static Singleton single = new Singleton();
// 构造器私有化,无法现示的创建对象。
private Singleton() {}
public static Singleton getInstance() {
return single;
}
}
饿汉式避免了多线程的同步问题,但是不管后面用不用得上,在类加载时直接初始化,
没有达到Lazy Loading (懒加载) 的效果,极有可能造成内存浪费
懒汉式
懒汉式就是在类加载时先不初始化,等到第一次被使用时才初始化。
- 线程不安全
// 线程不安全
public class Singleton {
private static Singleton single;
private Singleton() {}
public static Singleton getInstance() {
if(single==null) { //不为null说明已经初始化过了,不需要再进行初始化
single = new Singleton();
}
return single;
}
}
懒汉式不会造成内存浪费。代码在单线程下没有问题,但是在多线程下极有可能出现问题。
类加载是有顺序的,主要分为 加载 —> 连接 —> 初始化。假设对象还没被实例化,然后有两个线程同时访问,那么就可能出现多次实例化的结果。
- 线程安全
public class Singleton {
private static Singleton single;
private Singleton() {}
// 加锁,双重判空
public static Singleton getInstance() {
if(single==null) {
synchronized (Singleton.class) {
if(single == null)
single = new Singleton();
}
}
return single;
}
}
在实例化前加锁,防止并发情况下出现多次实例化,进行if(single== null)两次判空检查,可以保证线程安全,实例化代码一次,虽然保证了线程安全,但是又会出现另一个问题:不能保证new过程的原子性操作。
new过程主要有三步:
-
1、分配内存空间
-
2、执行构造方法、初始化对象
-
3、把对象指向空间
正常的顺序是123,但是也有可能出现132的情况,就是还没初始化先指向内存空间。这时候如果有两个线程同时访问,第一个线程不会有问题,会造成第二个线程查询到 single 不为空,直接返回 single,但是此时 single 还没有完成初始化,是个空对象,会出现问题。这个解决办法就是加上 volatile 关键字。
public class Singleton {
// 加上volatile关键字,保证按照123顺序执行,使其成为原子性操作
private volatile static Singleton single;
private Singleton() {}
public static Singleton getInstance() {
if(single==null) {
synchronized (Singleton.class) {
if(single == null)
single = new Singleton();
}
}
return single;
}
}
破环单例模式
public class Singleton {
// 加上volatile关键字,保证按照123顺序执行,使其成为原子性操作
private volatile static Singleton single;
private Singleton() {}
public static Singleton getInstance() {
if(single==null) {
synchronized (Singleton.class) {
if(single == null)
single = new Singleton();
}
}
return
}
}
public void main (String args[]) {
Singleton instance1 = Singleton.getInstance();
// 获取空参构造器,调用无参构造方法
Constructor<singleton> deConstructor = Singleton.class.getDeclaredConstructor(null);
// 破坏私有构造器,无视私有化
deConstructor.setAccessible(true);
// 通过反射创建对象
Singleton instance2 = deConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
结果会发现两个示例的地址不一样。
防止破坏单例模式
- 在构造函数中加锁
```java
public class Singleton {
// 加上volatile关键字,保证按照123顺序执行,使其成为原子性操作
private volatile static Singleton single;
private Singleton() {
synchronized (Singleton.class) {
if(single!=null) {
throw new RuntimeException("不要试图使用反射破坏单例模式");
}
}
}
public static Singleton getInstance() {
if(single==null) {
synchronized (Singleton.class) {
if(single == null)
single = new Singleton();
}
}
return
}
}
public void main (String args[]) {
Singleton instance1 = Singleton.getInstance();
// 获取空参构造器,调用无参构造方法
Constructor<singleton> deConstructor = Singleton.class.getDeclaredConstructor(null);
// 破坏私有构造器,无视私有化
deConstructor.setAccessible(true);
// 通过反射创建对象
Singleton instance2 = deConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
结果会发现两个示例的地址不一样。
- 枚举创建单例
public class Singleton {
}
public enum SingletonEnum {
INSTANCE;
private Singleton single = null;
private SingletonEnum () {
single = new Singleton ();
}
public Singleton getSingleton() {
return single;
}
}
枚举实现是在实例化时是线程安全, Java规范中规定,每一个枚举类型及其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf() 方法来根据名字查找枚举对象。
评论