单例设计模式1
一、饿汉式
单例设计模式,简单说:一个类只有一个实例对象。
单例设计模式核心:因为内存、所以性能。
饿汉式是指,这个类一旦加载,这个类的实例就被创建。而加载某个类到内存中由调用这个类的静态成员触发。单例模式里面的getInstance()就是静态方法。
饿汉式代码步骤:1.构造函数私有化,2.内部创建本类静态实例,3.对外提供公共的访问实例的方法。
饿汉式是线程安全的,因为类只会加载一次,加载的时候只会创建一份instance对象。
引入一个相当有深度的单例文章:Java设计模式学习笔记,一:单例模式
public class Single {
private static final Single instance = new Single();
private Single(){}
public static Single getInstance(){
return instance;
}
}
二、懒汉式
懒汉式是指:只有调用这个单例类得时候,才去创建这个单例类的实例对象。比如下面的代码,但是下面的代码有线程安全问题。两个线程同时进入if判断,也就创建了两次这个对象,不满足需求。
public class Single {
private static Single instance = null;
private Single(){}
public static Single getInstance(){
if(instance == null){
instance = new Single();
}
return instance;
}
}
解决办法如下代码:把getInstance()方法设为同步。这样做的好处是线程安全了,但是坏处就是每次调用getInstance()方法都要去判断同步锁,这样会浪费性能。实际上只是需要在第一次创建实例的时候,需要同步。
public synchronized static Single getInstance(){
if(instance == null){
instance = new Single();
}
return instance;
}
改进以上的代码的方法就是“双重检验锁”。
//A,B线程同时调用getInstance()方法
public static Single getInstance(){
if(instance == null){
synchronized (Single.class){
if(instance==null){
instance = new Single();
}
}
}
return instance;
}
}
注:在多线程情况下,JVM虚拟机可能会出现指令优化。在单例对象调用构造函数之前,就已经分配内存。这个时候,instance指向的其实是一个未初始化好的单例对象。所以,这个时候,我们需要在instance引用前面加上volatile关键字,禁止指令重排序,保证instance引用初始化的时候,单例对象已经被初始化。
指令重排优化,可能会导致初始化单利对象和将该对象地址赋值给 instance 字段的顺序与上面 Java 代码中书写的顺序不同。
例如:线程 A 在创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象设置为默认值。此时线程 A 就可以将分配的内存地址赋值给 instance 字段了,然而该对象可能还没有完成初始化操作。线程 B 来调用 newInstance() 方法,得到的 就是未初始化完全的单例对象,这就会导致系统出现异常行为。
为了解决上述的问题,可以使用volatile
关键字进行修饰 instance 字段。volatile 关键字在这里的含义就是禁止指令的重排序优化(另一个作用是提供内存可见性),从而保证 instance 字段被初始化时,单例对象已经被完全初始化。
双重检验锁与延迟初始化的单例模式的问题根源
public static Singleton getInstance() {
if (instance == null) {
synchorinzed(lock) {
if (instance == null)
instance = new Singleton();//根源在这一句
}
}
return instance;
}
下面重排序过程务必能够手写出来。
instance = new Singleton(); 这一行代码可以拆分成三句。
memory = allocate(); //1.分配对象的内存空间
initInstance(memory); //2.初始化对象
instance = memroy; //3.设置instance引用指向这块内存
上面的代码中,2和3可能会被重排序。
另外一个并发线程B可能拿到的是没有被A线程初始化的对象。
解决办法:(1)禁止指令重排序。所以使用 volatile 关键字修饰instance对象
(2)让这个可能会重排序的代码对其它线程不可见。
使用嵌套类延时加载,如果多个线程同时调用getInstance()方法,那么会执行类初始化。类初始化的时候,会有一把锁,用于同步多个线程对同一个类的初始化。线程A在执行类初始化的时候,可能会出现了重排序,但是这个过程对其它线程是不可见的,所以可以保证正确的初始一个单例对象。
三、使用嵌套类的懒汉式
public class Single {
private Single(){}
private static class SingleFactory{
private static final Single instance = new Single();
}
public static final Single getInstance(){
return SingleFactory.instance;
}
}
这种方法是使用内部类。原理是:加载某各类由调用这个类的静态成员触发。这个内部类的静态成员就是本类的唯一的实例对象。
当我们调用Single.getInstance()方法的时候,JVM加载类Single类。这个方法又调用嵌套类的静态成员intance,因此嵌套类被JVM加载,这个时候创建 new Single()对象。所以当我们没有调用getInstance()方法的时候,instance对象就没有被初始化。所以,当调用getInstance()方法的时候,才创建instance实例对象。这样,既保证了一个类只有一个实例对象,又是懒汉式的,还没有线程安全问题(原因:一个类只会被JVM加载一次)。
四、优缺点
单例模式的优点:
1.节约内存。一个类只有一个实例,内存开销比较小。
2.提高系统性能。如果一个类对象的创建需要读取许多配置文件,可以在系统创建的时候,创建此对象,让它一直驻留在内存中。
单例模式的缺点:
1.扩展性很差。使用单例模式的类一般没有接口,没有面向接口编程,扩展比较困难,一般扩展需要修改代码。
五、使用场景
1.需要频繁实例化的对象然后销毁的对象。
2.创建对象时耗时过多或者是读取很多配置文件的对象。
六、记忆要点
节约内存。提高性能。扩展性差。
七、使用枚举
实力最简单
public enum Single {
INTANCE;
}
public enum Single {
/**
* 默认实现
*/
INTANCE;
@lombok.Getter
@lombok.Setter
private String content;
/**
* 默认构造函数,省略public,可以在这里读取配置文件。
*/
Single(){
content = "default content";
}
//经测试,可以在包外调用此单例
}
分割线--------------------------------------------------------------------------------------------
刻意练习
(1)单例设计模式高性能的写法
(2)单例设计模式双重锁写法
(3)饿汉式写法
(4)单例设计模式应用场景是什么
(5)优缺点
(6)如何使用枚举来实现单例
下一篇:策略设计模式2