一、概述
单例模式又名单子模式。是一种非常常见的设计模式。我们在运用这模式的时候,基本是为了确保整个系统中只有一个实例。
二、单例的好处
我们从单例模式的定义和实现,可以知道单例模式具有以下几个优点:
在内存中只有一个对象,节省内存空间;
避免频繁的创建销毁对象,可以提高性能;
避免对共享资源的多重占用,简化访问;
为整个系统提供一个全局访问点。
三、单线程下单例(Singleton)两种经典实现
1、饿汗式
/**
* 饿汉式单例
*/
public class Singleton1 {
//私有的指向自己的静态引用---主动创建
private static Singleton1 singleton1 = new Singleton1();
//私有的构造方法
private Singleton1() {
}
//获取单例
public static Singleton1 getInstance(){
return singleton1;
}
}
2、懒汉式
public class Singleton2 {
//私有的指向自己的静态引用
private static Singleton2 singleton2 ;
//私有的构造方法
private Singleton2() {
}
//获取单例
public static Singleton2 getInstance(){
//被动创建,需要的时候创建
if(singleton2==null){
singleton2=new Singleton2();
}
return singleton2;
}
}
总结:
1、饿汗式单例在这个类被加载的时候就会去new 一个静态实例。因为我们知道static修饰的变量 整个生命周期只创建一次 。所以也就确保了整个系统中只有它一个实例。
2、懒汉式单例是当我们需要的时候去创建实例。
从速度和反应时间上来说,饿汗式更好。但从资源利用效率上来说,懒汉式更好。
四、多线程下的单例
我们先看如下代码:
public class Test {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
MyThread thread = new MyThread();
thread.start();
}
}
static class MyThread extends Thread {
@Override
public void run() {
try{
sleep(3000);
Singleton1 singleton1 = Singleton1.getInstance();
System.out.println("hashcode-----" + singleton1.hashCode());
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
输出结果:
hashcode-----2136701979
hashcode-----2136701979
hashcode-----2136701979
hashcode-----2136701979
hashcode-----2136701979
hashcode-----2136701979
hashcode-----2136701979
hashcode-----2136701979
hashcode-----2136701979
hashcode-----2136701979
证明:饿汗式单例 在多线程下也是安全的
我们再看看懒汉式单例在多线程下的表现
hashcode-----100758745
hashcode-----2075012821
hashcode-----2075012821
hashcode-----2075012821
hashcode-----2075012821
hashcode-----2075012821
hashcode-----2075012821
hashcode-----2075012821
hashcode-----2075012821
hashcode-----2075012821
证明: 懒汉式单例在多线程下是不安全的:
总结:其实道理很简单。饿汗式单例 因为在类加载的时候,就已经创建了为一得实例,所以多线程下肯定是线程安全的。
而懒汉式是在调用的时候去创建的。当我们代码走到if(singleton2 == null)判断语句的时候,多线程的环境下。就有可能同时走到这块,并且同时创建多个实例。
五、懒汉式单例在多线程环境下正确写法
1、第一种写法:
public class Singleton2 {
//私有的指向自己的静态引用
private static Singleton2 singleton2 ;
//私有的构造方法
private Singleton2() {
}
//获取单例
public static synchronized Singleton2 getInstance(){
//被动创建,需要的时候创建
if(singleton2==null){
singleton2=new Singleton2();
}
return singleton2;
}
}
这种写法和上面的懒汉式写法的唯一区别 使用了synchronzied修饰了整个方法,这样写保证了临界资源的互斥访问,从而保证了线程安全。
从执行结果上来看,问题已经解决了,但是这种实现方式的运行效率会很低,因为同步块的作用域有点大,而且锁的粒度有点粗。同步方法效率低,那我们考虑使用同步代码块来实现。
2、第二种写法:
public class Singleton2 {
//私有的指向自己的静态引用
private static Singleton2 singleton2 ;
//私有的构造方法
private Singleton2() {
}
//获取单例
public static Singleton2 getInstance(){
//被动创建,需要的时候创建
synchronized (Singleton2.class){
if(singleton2==null){
singleton2=new Singleton2();
}
return singleton2;
}
}
}
这种写法相对于上面写法,效率仍很差。
3、第三种写法
public class Singleton2 {
//私有的构造方法
private Singleton2() {
}
private static class Singleton2Holder{
//私有的指向自己的静态引用
private static Singleton2 singleton2 = new Singleton2();
}
//获取单例
public static Singleton2 getInstance(){
return Singleton2Holder.singleton2;
}
}
如上述代码所示,我们可以使用内部类实现线程安全的懒汉式单例,这种方式也是一种效率比较高的做法,它与饿汉式单例的区别就是:这种方式不但是线程安全的,还是延迟加载的,真正做到了用时才初始化。
当客户端调用getInstance()方法时,会触发Singleton2Holder类的初始化。由于Singleton2是Singleton2Holder类成员变量,因此在JVM调用Singleton2Holder类的类构造器对其进行初始化时,虚拟机会保证一个类的类构造器在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器,其他线程都需要阻塞等待,直到活动线程执行方法完毕。在这种情形下,其他线程虽然会被阻塞,但如果执行类构造器方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行类构造器,因为 在同一个类加载器下,一个类型只会被初始化一次,因此就保证了单例。
五、单例模式的双重检查写法
public class Singleton2 {
//这里使用volatile原因是,new Singleton2()不是一个原子操作,会出现指令重排的问题
private static volatile Singleton2 singleton2 ;
//私有的构造方法
private Singleton2() {
}
//获取单例
public static Singleton2 getInstance(){
if(singleton2==null) {
//被动创建,需要的时候创建
synchronized (Singleton2.class) {
if (singleton2 == null) {
singleton2 = new Singleton2();
}
}
}
return singleton2;
}
}
如上述代码所示,为了在保证单例的前提下提高运行效率,我们需要对 singleton2 进行第二次检查,目的是避开过多的同步(因为这里的同步只需在第一次创建实例时才同步,一旦创建成功,以后获取实例时就不需要同步获取锁了)。这种做法无疑是优秀的,但是我们必须注意一点:必须使用volatile关键字修饰单例引用(原因已经在代码注释了)。
需要注意的是为什么我们再这需要使用volatile去修饰Singleton2呢?
首先我们需要弄清楚 new Singleton2()这个操作是一个非原子操作,其次我们要知道JVM到底做了哪些操作
1、 在内存中分配一块地址
2、初始化对象
3、使sinleton2 指向内存中的地址
编译器有可能会出现指令重排的问题,有可能先后顺序变成了1,3,2。当线程1去调getInstance方法获取单例的时候,走到3时,这是线程2也来获取单例,这时候进入判断singleton2==null 判断的时候,其实singleton2只是不为null ,但并未初始化完成。
那线程2获取的实例是一个有问题的实例,会是应用程序奔溃。所以造成这种现象的原因是指令重排。我们又知道volatile是能完美解决这一方案的。所以需要添加修饰。