一、概念:
单例模式是一种设计模式,保证一个类只有一个实例,并提供一个全局访问点。
二、实现模式:
1、饿汉模式
含义:顾名思义,当加载到该类的时候,就会去创建这个单例,而这个单例通常是以类级别的成员变量的形式出现。但是如果我不使用这个单例,那么就会造成内存浪费。
2、懒汉模式
含义:当需要真正用到这个单例的时候,才去创建它,一般是以getInstance()方法作为全局入口。
三、代码实现:
3.1、饿汉模式:
3.1.1、 在全局变量中直接new
public class SingletonMode1 {
//私有化构造方法
private SingletonMode1(){
}
//定义成员变量,必须有static关键字
private static SingletonMode1 singleton=new SingletonMode1();
public static SingletonMode1 getInstance(){
return singleton;
}
}
3.1.2、在静态代码块中new对象。
这里是利用当进行类加载时,静态代码块会被JVM执行且只被执行一次的特点。
public class SingletonMode2 {
private SingletonMode2(){
}
private static SingletonMode2 singletonMode2;
//静态代码块对类级别成员变量进行初始化
static {
singletonMode2=new SingletonMode2();
}
public static SingletonMode2 getInstance(){
return singletonMode2;
}
}
3.2、懒汉模式:
3.2.1、当调用该方法时,直接new对象
public class SingletonMode3 {
private SingletonMode3() {
}
private static SingletonMode3 instance;
/**
* 当调用该方法时,直接new并且返回
* @return
*/
public static SingletonMode3 getInstance() {
instance = new SingletonMode3();
return instance;
}
}
但是这样在多线程环境下会出现问题,如果现在有十个线程都调用了这个方法,那么这些方法都创建了不同地址的单例对象,那么肯定是不行的,针对这个问题,我们可以想到上锁。
public class SingletonMode3 {
private SingletonMode3() {
}
private static SingletonMode3 instance;
/**
* 调用该方法时,直接new并且返回
* @return
*/
public static synchronized SingletonMode3 getInstance() {
instance = new SingletonMode3();
return instance;
}
}
但是synchronized加到方法上,肯定是力度太重了。因为当你是第一次访问这个方法,那么无可厚非你得上锁,但是当你第一次创建了之后,后面就不需要在去获取锁了,因为你只是读这个单例,而不是去写和修改这个单例的地址,这样就会导致后面想读这个单例的线程全都阻塞住了。针对这个问题,我们想到缩小synchronized的锁范围。
public class SingletonMode3 {
private SingletonMode3() {
}
private static SingletonMode3 instance;
/**
* 双重检查锁用于实现多线程环境下的安全创建
* @return
*/
public static SingletonMode3 getInstance() {
if (instance == null) {
synchronized (SingletonMode3.class) {
//第二层判断是为了排除当其他线程进入到第一个判断了之后,获取到锁,然后对该单例对象进行修改的情况。
if (instance == null) {
instance = new SingletonMode3();
}
}
}
return instance;
}
}
但是,这么完美的代码依然存在一个问题:指令重排
指令重排序是指:当JVM在保证最终结果正确的情况下,可以不按照程序编写的顺序执行语句,以尽可能提高程序的性能。
创建一个对象,在JVM中会经过三步:
- 为对象分配内存空间
- 初始化对象
- 将对象指向分配好的内存
在这三步中,第2、3步有可能会发生指令重排现象,创建对象的顺序变为1->3->2。这样就会导致多个线程获取到对象时,有可能线程A创建对象的过程中,执行了1、3步骤,线程B判断singleton已经不为空,获取到未初始化的singleton对象,就会报空指针异常。
解决方案就是:加上volatile 关键字
public class SingletonMode3 {
private SingletonMode3() {
}
private static volatile SingletonMode3 instance;
/**
* 双重检查锁用于实现多线程环境下的安全创建
* @return
*/
public static SingletonMode3 getInstance() {
if (instance == null) {
synchronized (SingletonMode3.class) {
//第二层判断是为了排除当其他线程进入到第一个判断了之后,获取到锁,然后对该单例对象进行修改的情况。
if (instance == null) {
instance = new SingletonMode3();
}
}
}
return instance;
}
}
volatile是可以保证有序性和可见性。
有序性:当程序执行的时候,会严格按照代码的先后顺序执行,防止指令重排序。
可见性:当一个线程修改了成员变量的值后,其他线程能够立即看到这个修改。
3.2.2、采用静态内部类
这里是利用当进行类加载时,静态代码块会被JVM执行且只被执行一次的特点。
/**
* 使用静态内部类方法实现懒汉式单例实现
*/
public class SingletonMode4 {
private SingletonMode4(){
}
/**
* 用JVM的特性,加载外部类的时候,不会去加载静态内部类
* 有自己的加载时机,独立于外部类
*/
private static class SingletonMode4Holder {
private static SingletonMode4 instance=new SingletonMode4();
}
public static SingletonMode4 getInstance(){
return SingletonMode4Holder.instance;
}
}
四、破坏单例模式的两种情况
4.1、使用反射来破坏单例
public Class TestSingleton{
public static void main(String[] args) throws Exception {
//用正常入口获取单例
SingletonMode4 newInstance1= SingletonMode4.getInstance();
//使用反射,获取到类的构造方法来破坏单例
Constructor<SingletonMode4> constructor = SingletonMode4.class.getDeclaredConstructor();
constructor.setAccessible(true);
SingletonMode4 newInstance2 = constructor.newInstance();
//输出结果是false
System.out.println("newInstance1==newInstance2:"+(instance==newInstance1));
}
}
解决方法:直接在私有的构造方法里抛出异常,不让你创建新的对象。
public class SingletonMode4 {
private SingletonMode4(){
throw new RuntimeException("单例模式不能使用构造函数创建对象");
}
/**
* 用JVM的特性,加载外部类的时候,不会去加载静态内部类
* 有自己的加载时机,独立于外部类
*/
private static class SingletonMode4Holder {
private static SingletonMode4 instance=new SingletonMode4();
}
public static SingletonMode4 getInstance(){
return SingletonMode4Holder.instance;
}
}
4.2、使用序列化和反序列化来破坏单例
public Class TestSingleton{
public static void main(String[] args)throws Exception {
//正常入口获取单例
SingletonMode4 instance1 = SingletonMode4.getInstance();
//先进行序列化,最后序列化破坏单例
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("E:\\DeskTop\\test.txt"));
out.writeObject(instance1);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("E:\\DeskTop\\test.txt"));
SingletonMode3 instance2 = (SingletonMode4) in.readObject();
in.close();
// 比较两个实例,输出是false
System.out.println("newInstance1==newInstance2:"+(instance==newInstance1));
}
}
解决方法:在单例类中新增 readResolve()方法。
public class SingletonMode4 {
private SingletonMode4(){
throw new RuntimeException("单例模式不能使用构造函数创建对象");
}
/**
* 用JVM的特性,加载外部类的时候,不会去加载静态内部类
* 有自己的加载时机,独立于外部类
*/
private static class SingletonMode4Holder {
private static SingletonMode4 instance=new SingletonMode4();
}
public static SingletonMode4 getInstance(){
return SingletonMode4Holder.instance;
}
/**
* 解决方法就是将再该单例类中新增该方法
* @return
*/
private Object readResolve() {
return SingletonMode4Holder.instance; // 返回当前已存在的单例实例
}
}
简单点来说,就是在反序列化时,如果我们的类中有readResolve()方法,那么就会自动调用这个方法来实现反序列化,此时我们只需要返回一个已有的实例,从而避免生成新的对象。
五、总结
-
单例模式常见的写法有两种:懒汉式、饿汉式
-
在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,不会存在并发安全和性能问题,但是可能会造成内存浪费。
-
懒汉式:在需要用到对象时才实例化对象。最佳实践:使用双重校验锁来实现单例的创建。
-
为了防止多线程环境下,因为指令重排序导致变量报空指针异常,需要在单例对象上添加volatile关键字,防止指令重排序。