1、定义
指一个类只有一个实例,且该类能自行创建这个实例的一种模式。
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。例如任务管理器任何时候都只能打开一个。就是运用了单例模式。
1.1 特点
- 单例类只有一个实例对象。
- 单例类只能自己创建自己的唯一实例对象。
-
1.2 结构
1.3 实现的关键点
将类的构造方法设置为私有方法。这样其他类就不能通过调用该类的构造方法来实例化对象。
- 定义一个私有的类的静态实例。
- 提供一个公有的获得静态实例的方法。
2、单例模式的实现
2.1 饿汉式单例
饿汉法就是在第一次引用该类的时候就创建对象实例,而不管实际是否需要创建。就跟名字一样,像一个饿汉一样,饥不择食,在类装载的时候就创建,不管用不用,先创建了再说。
优缺点:
优点:**如果一直没有被使用,便浪费了空间,典型的空间换时间,如果初始化很耗费时间,同时也会 推迟系统的启动时间。
缺点:每次调用的时候,就不需要再判断,节省了运行时间。**
public class Singleton {
//定义一个私有静态实例instance
private static Singleton instance = new Singleton();
//定义私有的构造方法
private Singleton(){}
//只能通过该方法获取唯一的instance实例
public static Singleton getInstance(){
return instance;
}
}
2.2 懒汉式单例
非线程安全:
public class Singleton {
//先定义一个私有静态变量,不生成实例
private static Singleton instance;
//定义私有的构造方法
private Singleton(){}
//只能通过该方法获取唯一的instance实例
public static Singleton getInstance(){
//懒汉式单例在用户第一次调用时再进行初始化
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
从代码中可以看出,懒汉式单例只有在第一次调用才会初始化。
优缺点:
优点:使用时才创建,节省了资源。
缺点**:**第一次加载时需要实例化,反映稍慢一些,而且在多线程不能正常工作,很可能会造成多次实 例化,就不再是单例了。
线程安全:**
public class Singleton {
//先定义一个私有的静态变量
private static Singleton instance;
//定义私有的构造方法
private Singleton(){}
//为该方法加上synchronized关键字,可以实现线程安全
public static synchronized Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
优缺点:
优点:**可以在多线程环境下确保只初始化一个单例。
缺点:其实只有第一次调用getInstance时才真正需要同步,一旦设置好instance实例后,之后每次同步都 是累赘,平白增添性能消耗。
2.3 双重校验锁
对懒汉式单例的改进。
如果程序可以接受synchronized带来的性能负担,“懒汉式(线程安全)”可以使用,但如果很关心性能,“双重校验锁(DCL)”将会带来很大帮助。(double checked locking)
public class Singleton {
//定义一个私有的静态变量,注意这里的volatile关键字,这是必须的
private static volatile Singleton instance;
//定义私有的构造方法
private Singleton(){}
public static Singleton getInstance(){
//第一个校验是是为了提高代码效率
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
在getInstance()方法中对instance进行了两次判空。
- 第一次是为了提高代码效率,由于单例模式只需要创建一次实例即可。所以当创建了一个实例之后,再次调用该方法就不需要进入同步代码块去竞争锁了。直接返回前面创建的实例即可。
- 第二次是为了防止二次创建实例。
举一个例子:
当单例还没有被创建时,线程t1调用getInstance( )方法。第一次判断Singleton==null时,t1准备继续执行,而这时资源被线程t2抢占了。t2同样调用该方法,这时单例还没有被创建,顺利通过第一个判断if。遇到第二个判断if后,单例没有被创建,顺利通过,创建实例,任务完成。资源这时重新回到线程t1,这时遇到第二个判断,由于实例已创建,不能通过。避免了多线程创建多个实例的错误。
volatile关键字的作用
在创建私有静态变量时,注意此处使用的关键字volatile。它的作用是可以防止JVM指令重排优化。
在 instance = new Singleton()
执行时,JVM指令的执行顺序如下:
- 为变量instance分配内存空间。
- 初始化instance。
- 将变量instance指向分配的内存空间。
但是JVM具有指令重排的特性,有可能执行顺序会变成1-3-2。指令重排在单线程的情况下不会出现问题,但是在多线程的情况下,会导致一个线程获得一个未初始化的实例。使用 volatile 会禁止JVM指令重排,从而保证在多线程下也能正常执行。
举一个例子:
线程T1执行了1和3,此时T2调用 getInstance() 后发现 singleton 不为空,因此返回 singleton, 但是此时的 singleton 还没有被初始化。
volatile还有第二个作用:
保证变量在多线程运行时的可见性。
在JDK1.2以前,Java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的Java内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就 可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。 要解决这个问题,就需要把变量声明为 volatile,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。
2.4 静态内部类
public class Singleton{
//用一个静态内部类来初始化实例
private static class Holder{
private static final Singleton instance = new Singleton();
}
//定义私有的构造方法
private Singleton(){};
//提供一个获取实例的方法
public static final Singleton getInstance(){
return Holder.instance;
}
}
3、单例模式的应用场景
- windows系统的任务管理器,你永远只能打开一个任务管理器。
- windows系统的回收站。