文章目录
单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
介绍
- 意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
- 主要解决:一个全局使用的类频繁地创建与销毁。
- 何时使用:当您想控制实例数目,节省系统资源的时候。
- 如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
- 关键代码:构造函数是私有的。
实现
1. 单例模式之饿汉式单例
不支持多线程,线程不安全,这种方式比较常用,但容易产生垃圾对象。
//饿汉式单例
/**
* 优点,没有synchronized,效率高,不能延迟加载
* 缺点,如果类中有 private byte[] bytes1=new byte[1024];
* 这样占用内存资源的对象,又不会用,会占用内存资源,造成浪费。
*适用于,类中没有开辟空间内存的对象。如:spring
*/
public class SingletonDemo01 {
// private byte[] bytes1=new byte[1024];
// private byte[] bytes2=new byte[1024];
// private byte[] bytes3=new byte[1024];
// private byte[] bytes4=new byte[1024];
// private byte[] bytes5=new byte[1024];
//私有化构造器
private SingletonDemo01(){}
//类初始化时候,立即加载该对象
private static SingletonDemo01 instance=new SingletonDemo01();
//提供获取该对象的方法,没有synchronized,效率高
public static SingletonDemo01 getInstance(){
return instance;
}
}
class SingletonDemo01Test{
public static void main(String[] args) {
SingletonDemo01 instance1=SingletonDemo01.getInstance();
SingletonDemo01 instance2=SingletonDemo01.getInstance();
System.out.println(instance1==instance2);
System.out.println(instance1.hashCode());
System.out.println(instance1.hashCode());
}
}
运行结果:
2. 单例模式之懒汉式单例
支持多线程,线程安全,这种方式比较常用,有延迟加载。
package com.per.singleton;
/**
* 懒汉式单例
* 优点,有synchronized,效率低,但是有延迟加载
* 缺点,极端情况不安全。
*适用于,类中没有开辟空间内存的对象。如:spring
*/
public class SingletonDemo02 {
//私有化构造器
private SingletonDemo02(){}
//类初始化时候,立即加载该对象
private static SingletonDemo02 instance;
//提供获取该对象的方法,有synchronized,效率低
public static synchronized SingletonDemo02 getInstance(){
if(instance==null){
instance=new SingletonDemo02();
}
return instance;
}
}
class SingletonDemo02Test{
public static void main(String[] args) {
SingletonDemo02 instance1=SingletonDemo02.getInstance();
SingletonDemo02 instance2=SingletonDemo02.getInstance();
System.out.println(instance1==instance2);
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}
运行结果:
3.单例模式之 双检锁/双重校验锁(DCL,即 double-checked locking)
在懒汉式基础上进行改进
package com.per.singleton;
//DOL(双锁)懒汉式单例
/**
* 优点,相较于懒汉式单例效率更高了
* * volatile,关键字的作用:保证了变量的可见性(visibility)。
* * 被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象
*
* 缺点,当一个线程还没走完同步锁,另一个线程进来会直接返回return instance;这是出现错误
*/
public class SingletonDemo03 {
private SingletonDemo03() {
}
//类初始化时候,立即加载该对象
//volatile,关键字的作用:保证了变量的可见性(visibility)。
private static SingletonDemo03 instance;
//提供获取该对象的方法,有synchronized,效率低
public static SingletonDemo03 getInstance() {
if (instance == null) {
synchronized (SingletonDemo03.class) {
if (instance == null) {
instance = new SingletonDemo03();
}
}
}
return instance;
}
}
class SingletonDemo03Test{
public static void main(String[] args) {
SingletonDemo03 instance1=SingletonDemo03.getInstance();
SingletonDemo03 instance2=SingletonDemo03.getInstance();
System.out.println(instance1==instance2);
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}
运行结果:
但是这种模式还是不太安全,因为当一个线程还没走完同步锁,另一个线程进来会直接返回 return instance;这时出现错误。
所以这是把private static SingletonDemo03 instance;
改为private volatile static SingletonDemo03 instance;
- volatile,关键字的作用:保证了变量的可见性(visibility)。 被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象
4. 单例模式之 登记式/静态内部类
package com.per.singleton;
import java.lang.reflect.Constructor;
/**
* 单例模式进化到第四个版本是不是感觉已经很完善了,
*
*/
public class SingletonDemo04 {
private SingletonDemo04(){}
private static class Inner{
private static SingletonDemo04 instance=new SingletonDemo04();
}
public static SingletonDemo04 getInstance(){
return Inner.instance;
}
}
class SingletonDemo04Test{
public static void main(String[] args) throws Exception {
SingletonDemo04 instance1=SingletonDemo04.getInstance();
SingletonDemo04 instance2=SingletonDemo04.getInstance();
System.out.println(instance1==instance2);
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}
运行结果:
这时你是否会认为足够安全,答案是否定的。
在Java中但是有一个很霸道的东西,那就是反射
,反射机制会破环这种单例。
例如:我们进行这样的测试:
class SingletonDemo04Test{
public static void main(String[] args) throws Exception {
SingletonDemo04 instance1=SingletonDemo04.getInstance();
Constructor<SingletonDemo04> declaredConstructor = SingletonDemo04.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);//关闭权限检查。private SingletonDemo03(){} 中private失效。
SingletonDemo04 instance2 = declaredConstructor.newInstance();
System.out.println(instance1==instance2);
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}
运行结果:
这是再看结果,为false,说明反射已经破环了我们的单例。
如何解决呢?
第一种解决反射方案:我们已DCL单例为例。在私有构造器里面加上同步代码块。
private SingletonDemo03() {
synchronized (SingletonDemo06.class){
if(instance!=null){
throw new RuntimeException("不要试图用反射破环单例");
}
}
}
这时结果为:
反射不会在破环我们的单例。
但是,这种情况是我们先进行的初始化操作,所以反射就不能使用,但是如果先进行反射呢?
答案肯定就是可以的。如何解决呢?
第二种解决反射方案:在类中加入标志位。
private static boolean falg = false;
private SingletonDemo03() {
if (falg == false) {
falg = true;
} else {
throw new RuntimeException("不要试图用反射破环单例");
}
}
- 测试代码
class SingletonDemo03Test {
public static void main(String[] args) throws Exception {
Constructor<SingletonDemo03> declaredConstructor = SingletonDemo03.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);//关闭权限检查。private SingletonDemo03(){} 中private失效。
SingletonDemo03 instance2 = declaredConstructor.newInstance();
SingletonDemo03 instance1 = declaredConstructor.newInstance();
System.out.println(instance1 == instance2);
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}
这时结果为:
这种解决方案就解决了刚才的问题。但是这种方案还不是最安全的,因为在反射中 declaredConstructor.setAccessible(true);//关闭权限检查。private SingletonDemo03(){} 中private失效。
可以在外部设置你的标志位。
但是在反射中枚举类型就不可以被反射。
5.单例模式之 枚举
从源码中看出枚举类型是不可以反射的。
代码
:
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
是不是感觉很简单,
描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。
总结:一般情况下,不建议使用第 2 种懒汉方式,建议使用第1 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 4 种登记方式。如果涉及到反序列化创建对象时,可以尝试使用第 5 种枚举方式。如果有其他特殊的需求,可以考虑使用第 3 种双检锁方式。
设计模式持续更新中......