作者:汤圆

个人博客:javalover.cc

前言

有时候我们的类并不需要很多个实例,在程序运行期间,可能只需要一个实例就够了,多了反而会出现数据不一致的问题;

这时候我们就可以用单例模式来实现,然后程序中所有的操作都基于这个实例;

目录

单例模式有很多种,这里我们先列举下:

  • 饿汉模式
  • 懒汉模式-线程不安全
  • 懒汉模式-线程安全
  • 懒汉模式-线程不是很安全
  • 懒汉模式-双重检查
  • 静态内部类
  • 枚举

正文

1. 饿汉模式(不推荐)

饿汉模式的核心就是第一次加载类的时候,进行数据的初始化;

而且这个数据不可被修改(final);

后续只能读,不能写。

这样一来,就保证了数据的准确性;

下面我们看下示例

package pattern.singleton;// 饿汉模式(不推荐),因为占内存public class HungryDemo {    private static final HungryDemo hungryDemo = new HungryDemo();    private HungryDemo() {    }    public static HungryDemo getInstance(){        return hungryDemo;    }    public static void main(String[] args) {        HungryDemo hungryDemo1 = HungryDemo.getInstance();        HungryDemo hungryDemo2 = HungryDemo.getInstance();        System.out.println(hungryDemo1);        System.out.println(hungryDemo2);        System.out.println(hungryDemo1 == hungryDemo2);    }}

从主程序中可以看到,不管获取多少次实例,都是同一个。

优点:在类第一次加载时就创建单例,线程安全

缺点:占内存,不管用不用,都要先加载

2. 懒汉模式-线程不安全(不推荐)

懒汉模式,就是类初始化时不加载数据,等到需要的时候才加载;

下面看示例:

package pattern.singleton;// 懒汉模式-线程不安全(不推荐)public class LazyDemo1 {    private static LazyDemo1 lazyDemo;    private LazyDemo1(){    }    public static LazyDemo1 getInstance(){        if(lazyDemo == null)            lazyDemo = new LazyDemo1();        return lazyDemo;    }    public static void main(String[] args) {        LazyDemo1 l1 = LazyDemo1.getInstance();        LazyDemo1 l2 = LazyDemo1.getInstance();        System.out.println(l1);        System.out.println(l2);        System.out.println(l1 == l2);    }}

这样做的好处就是节省资源,只在需要的时候才加载;

但是会导致一个问题,就是线程的安全性;

比如两个线程同时获取,有可能获取到不同的实例;

优点:懒加载,使用时才加载,节省资源

缺点:线程不安全,多线程有可能创建多个单例

3. 懒汉模式-线程安全(不推荐)

上面的懒汉模式,最大的缺点就是线程不安全;

所以我们可以升级一下,通过加锁来解决,如下所示

package pattern.singleton;// 懒汉模式-线程安全(不推荐)public class LazyDemo2 {    private static LazyDemo2 lazyDemo;    private LazyDemo2(){    }    // 给方法加锁,线程安全了,但是效率低    public static synchronized LazyDemo2 getInstance(){        if(lazyDemo == null)            lazyDemo = new LazyDemo2();        return lazyDemo;    }    public static void main(String[] args) {        LazyDemo2 l1 = LazyDemo2.getInstance();        LazyDemo2 l2 = LazyDemo2.getInstance();        System.out.println(l1);        System.out.println(l2);        System.out.println(l1 == l2);    }}

这样一来,不管多少个线程去获取实例,都只会获取到同一个;

但是缺点也很明显,就是效率低;

比如现在已经遗弃的vector类,就是通过给方法上锁,来解决安全问题

优点:线程安全,懒加载

缺点:效率低,每次获取单例都要操作锁

4. 懒汉模式-线程不是很安全(不推荐)

这一次,我们又升级了上面的懒汉模式,把方法锁改为代码块锁,减小了锁的范围;

package pattern.singleton;import java.lang.management.ThreadInfo;// 懒汉模式-线程不安全(不推荐)// 解释:虽然加了代码同步块,但是还是存在线程不安全的情况public class LazyDemo3 {    private static LazyDemo3 lazyDemo;    private static int count = 0;    private LazyDemo3(){    }    public static LazyDemo3 getInstance(){        if(lazyDemo == null){            // 1. 所有的线程会先执行下面的打印,然后第一个线程先获得锁,其他线程依次排队等待解锁            System.out.println(Thread.currentThread().getName());            synchronized (LazyDemo3.class){                try {                    System.out.println(Thread.currentThread().getName()+"等待中");                    // 2. 当第一个进来的线程在这里休眠时,其他外面的线程是获取不到锁的,就会一直等待                    Thread.sleep(1000);                    lazyDemo = new LazyDemo3();                    System.out.println(Thread.currentThread().getName()+"等待结束");                    // 3. 此时第一个线程释放锁,第二个线程因为已经通过了if(lazyDemo == null)的判断                    // 所以会直接获取锁,然后重复刚才的步骤2,这样就会导致实例 lazyDemo 被创建多次                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }        return lazyDemo;    }    public static void main(String[] args) {        for (int i = 0; i < 2; i++) {            new Thread(new Runnable() {                @Override                public void run() {                    LazyDemo3 l1 = LazyDemo3.getInstance();                    System.out.println(l1);                }            }).start();        }    }}/** * 下面是输出 * * Thread-0 * Thread-0等待中 * Thread-1 // 此时Thread-1已经通过了if()校验 * Thread-0等待结束 // Thread-0 释放锁 * Thread-1等待中 // Thread-1 获取锁 * pattern.singleton.LazyDemo3@568acee4 // 这是 Thread-0 创建的单例 * Thread-1等待结束 // Thread-1 释放锁 * pattern.singleton.LazyDemo3@3f580216 // 这是 Thread-1 创建的单例,此时就有了两个单例,就出问题了 * * */

通过例子可以看到,这两个线程交替执行去获取实例,虽然效率有所提高,但是结果却创建了两个实例,因小失大

所以这种方式也不推荐

优点:懒加载,比上面的 LazyDemo2 效率高

缺点:有可能导致线程不安全,详情见代码(需亲测看到效果,才好理解)

5. 懒汉模式-双重检查(推荐)

前面的几种懒汉模式,都是各有各的不足;

所以这里来个大招,将上面的不足都解决掉;

也就是双重检查模式。

package pattern.singleton;// 懒汉模式-双重检查(推荐)public class LazyDemo4 {    // 保证可见性,即在多线程时,一个线程修改了这个变量,则其他线程立马就可以看到变化    private static volatile LazyDemo4 lazyDemo;    private LazyDemo4(){    }    public static LazyDemo4 getInstance(){        if(lazyDemo == null)            // 加同步代码块,保证当前只有一个线程在修改 lazyDemo            synchronized (LazyDemo4.class){                // 加双重检查,其他后面进来的线程,如果看到 lazyDemo 已经创建了,则不再创建,直接返回                if(lazyDemo == null)                    lazyDemo = new LazyDemo4();            }        return lazyDemo;    }    public static void main(String[] args) {        LazyDemo4 l1 = LazyDemo4.getInstance();        LazyDemo4 l2 = LazyDemo4.getInstance();        System.out.println(l1);        System.out.println(l2);        System.out.println(l1 == l2);    }}

可以看到,这里在获取到锁之后,又加了一个null判断,这样就可以保证在创建实例之前,确保实例真的是null

优点:懒加载、线程安全、效率高

6. 静态内部类(推荐)

这个就比较简单了,不需要加锁,也不需要考虑null判断,直接将实例封装到内部类中,再用final修饰为不可变;

从而保证了这个实例的唯一性;

这个其实就是结合了前面的 饿汉模式 和 懒汉模式-双重检查。

package pattern.singleton;// 静态内部类(推荐)public class StaticInnerDemo {    private StaticInnerDemo(){    };    // 静态内部类    // 1. 当 StaticInnerDemo 加载时,下面的 InnerInstace 并没有加载    // 2. 当 调用getInstance()时,下面的静态内部类才会加载,且只会加载一次(因为final常量)    private static class InnerInstance{        private static final StaticInnerDemo staticInnerDemo = new StaticInnerDemo();    }    public static StaticInnerDemo getInstance(){        return InnerInstance.staticInnerDemo;    }    public static void main(String[] args) {        StaticInnerDemo staticInnerDemo1 = getInstance();        StaticInnerDemo staticInnerDemo2 = getInstance();        System.out.println(staticInnerDemo1);        System.out.println(staticInnerDemo2);        System.out.println(staticInnerDemo1 == staticInnerDemo2);    }}

优点:懒加载,线程安全(详情见代码)

7. 枚举(推荐)

最后来个压轴的,通过枚举来实现单例模式;

这个可以说是极简主义风格,自带单例效果;

因为不需要过多的修饰,只是单纯的定义一个枚举,然后创建一个实例,后面程序直接用这个实例就可以了。

package pattern.singleton;// 枚举(推荐)public enum  EnumDemo {    INSTANCE;    public static void main(String[] args) {        EnumDemo instance1 = EnumDemo.INSTANCE;        EnumDemo instance2 = EnumDemo.INSTANCE;        System.out.println(instance1);        System.out.println(instance2);        System.out.println(instance1 == instance2);    }}

优点:懒加载,线程安全,效率高,大牛推荐(Effective Java作者推荐)

总结

关于单例模式的实现方式,首推的就是枚举,其次是懒汉模式-双重检查,最后是静态内部类