#yyds干货盘点#设计模式之单例模式

Posted 汤圆学Java

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了#yyds干货盘点#设计模式之单例模式相关的知识,希望对你有一定的参考价值。

作者:汤圆

个人博客: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 创建的单例,此时就有了两个单例,就出问题了
 *
 *
 */

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

所以这种方式也不推荐

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);
    }
}

总结

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

以上是关于#yyds干货盘点#设计模式之单例模式的主要内容,如果未能解决你的问题,请参考以下文章

单例模式八个例子#yyds干货盘点#

#yyds干货盘点# 关键字: volatile详解

「干货分享」经典设计模式之单例模式

#yyds干货盘点# 设计模式之代理模式:动态代理

#yyds干货盘点# 设计模式之代理模式:静态代理

设计模式之单例模式