单例模式的六种写法

前言: 被写烂了的单例模式

说到设计模式中的单例模式,估计是大家最熟悉的设计模式了,至少都应该听过,但是你真的了解这个设计模式么?最近我系统的重新学习了一下单例模式,在此整理,以巩固自己的学习成果,方便自己以后查找,也希望能够帮到对单例模式不够清楚的同学们,本文带你详细的了解单例模式的各种写法及对应写法存在的问题并给出解决方案.

一.什么是单例模式

数学与逻辑学中,singleton定义为“有且仅有一个元素的集合。
单例模式最初的定义出现于《设计模式》(艾迪生维斯理, 1994):“保证一个类仅有一个实例,并提供一个访问它的全局访问点。”
Java中单例模式定义:“一个类有且仅有一个实例,并且自行实例化向整个系统提供。” 摘自百度百科

我的理解: 让java中的某个类,无论在什么情况下,只能有且仅有一个实例对象,那么这个类就是单例模式的设计.

补充: 单例模式属于创建型模式

二.单例模式的不同写法

单例模式的常用写法,有饿汉模式单例,懒汉模式单例,注册模式单例,注册模式单例又分枚举模式单例和容器模式单例,ThreadLocal单例,其中ThreadLocal单例属于线程内单例,也可以叫做伪单例.下面说明不同单例的写法及存在的问题,并提出解决方案.

补充: 写单例模式一个是保证私有化构造,不让别创建实例,第二个就是提供全局访问方法,让别人能够获取的你实例.围绕这两个点进行体会,个人感觉能够理解的更深入一些.

1. 饿汉模式单例

package com.fatfat.sington.hungry.sington.hungry;

/**
 * @ClassName HungrySingtom
 * @Auther LangGuofeng
 * @Date: 2019/7/28/028 10:51
 * @Description: 饿汉单例模式,正常new一个实例
 */
public class HungrySingleton {

    private static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return HUNGRY_SINGLETON;
    }

}

另外一种写法,利用静态代码块

package com.fatfat.sington.hungry.sington.hungry;

/**
 * @ClassName HungrySingletonStatic
 * @Auther LangGuofeng
 * @Date: 2019/7/28/028 11:49
 * @Description: 饿汉模式单例,利用静态代码块初始化实例
 */
public class HungrySingletonStatic {
    private static final HungrySingletonStatic HUNGRY_SINGLETON_STATIC;

    static {
        HUNGRY_SINGLETON_STATIC = new HungrySingletonStatic();
    }

    private HungrySingletonStatic() {
    }

    public static final HungrySingletonStatic getInstance(){
        return HUNGRY_SINGLETON_STATIC;
    }
}

分析:

  1. 首先私有构造方法,保证了外部不能调用构造方法创建对象.
  2. 使用静态方法提供全局访问点.获取实例.
  3. 第一个用static final 关键字修饰这个实例属性,实例是属于类的,在类加载的时候就创建了这个实例,所以不存在线程安全问题.
  4. 第二个使用静态代码块创建实例,原理都是一样的,都是在类加载的时候就创建实例,
  5. 饿汉模式的特点就是在类加载的时候,不管你现在用不用这个实例,我都给你创建出来,使用的时候直接去内存中获取就可以,不用现创建,体验比较好,但是如果单例模式的类特别多,相对来说浪费一点资源,性能相对没有那么高.

**补充:**类的加载顺序,

2. 懒汉模式单例

package com.fatfat.sington.hungry.sington.lazy;

/**
 * @ClassName LazySimpleSingleton
 * @Auther LangGuofeng
 * @Date: 2019/7/28/028 12:24
 * @Description: TODO
 */
public class LazySimpleSingleton {

    private static LazySimpleSingleton lazySimpleSingleton = null;

    private LazySimpleSingleton(){
    }

    public static final LazySimpleSingleton getInstance(){

        if (lazySimpleSingleton == null){
            lazySimpleSingleton = new LazySimpleSingleton();
        }
        
    }
}

分析:

  1. 首先私有化构造方法,这个不多说
  2. 然后看全局访问方法,这里通过判断实例是否为null,来确定是否初始化实例 。这种方式在类加载的时候不初始化实例,当外边类调用这个方法的时候,如果当前实例不为null,那么直接返回实例,否则初始化实例。从内存占用方面去考虑,这种懒汉模式要不饿汉模式占用的内存更少,性能更优。但是,这里存在一个问题,当出现并发访问的时候,线程不安全。会重复创建实例,下面我们测试一下。
  3. 首先写一个线程类
package com.fatfat.sington.hungry.sington.lazy;

/**
 * @ClassName ExectorThread
 * @Auther LangGuofeng
 * @Date: 2019/7/28/028 12:55
 * @Description: 获取单例线程
 */
public class ExectorThread implements Runnable{

    @Override
    public void run() {
        LazySimpleSingleton singleton = LazySimpleSingleton.getInstance();
        System.out.println(Thread.currentThread().getName() + ":" + singleton );
    }
}

  1. 然后在写一个测试类
package com.fatfat.sington.hungry.sington.lazy;

/**
 * @ClassName LazyTest
 * @Auther LangGuofeng
 * @Date: 2019/7/28/028 13:07
 * @Description: TODO
 */
public class LazyTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(new ExectorThread());
        Thread t2 = new Thread(new ExectorThread());

        t1.start();
        t2.start();

        System.out.println("End");
    }
}

  1. 运行测试类,查看结果,从结果中我们可以看到两个对象的地址是不一样的。
    单例模式的六种写法_第1张图片
    补充: 这种测试是带有随机性的,有的时候打印出来的是相同的对象,我们可以通过断点调试来看具体的执行过程。

  2. 解决方案,在方法上加上 synchronized 关键字

    public synchronized static final LazySimpleSingleton getInstance(){

        if (lazySimpleSingleton == null){
            lazySimpleSingleton = new LazySimpleSingleton();
        }
        return lazySimpleSingleton;
    }

补充: 由于synchronize修饰的是一个静态方法,静态方法归类所有,所以synchronize的对象是class,在线程数量比较多情况下,如果 CPU 分配压力上升,会导致大批量线程出现阻塞,从而导致程序运行性能大幅下降。

  1. synchronized作用于代码块的解决方案
 public synchronized static final LazySimpleSingleton getInstance(){
        //判断实例是否为null
        if (lazySimpleSingleton == null){
            //给当前代码块上锁
            synchronized (LazySimpleSingleton.class){
                if(lazySimpleSingleton == null){
                    lazySimpleSingleton = new LazySimpleSingleton();
                }
            }
        }
        return lazySimpleSingleton;
    }

补充: 使用synchronized作用于代码块,此时所有的线程都可以访问getInstance()方法,与直接修饰静态方法的最大的区别就是,直接修饰静态方法线程阻塞是基于整个类的阻塞,而作用于代码块上,阻塞发生在方法内部。相比之下是有一定的优化,但是只要使用synchronized,势必会要上锁出现阻塞。下面我们在来看基于静态内部类的方法实现单例模式

3.静态内部类单例模式

package com.fatfat.sington.hungry.sington.InnerClass;

/**
 * @ClassName InnerClassSingleton
 * @Auther LangGuofeng
 * @Date: 2019/7/28/028 16:45
 * @Description: TODO
 */
public class InnerClassSingleton {

    private InnerClassSingleton(){

    }

    public static final InnerClassSingleton getInstance(){
        //在返回结果之前去加载静态内部类
        return InnerHolder.singleton;
    }

    //静态内部类默认不加载,只用当外部调用时才加载
    private static class InnerHolder{
        private static final InnerClassSingleton singleton = new InnerClassSingleton();
    }
}

补充: 从jvm类初始化的角度考虑如何实现单例模式,在加载外部类的时候,JVM并不会加载静态内部类,所以在类初始化的时候,内存中是不会存在实例对象的,避免了内存浪费。然后在外部类调用getInstance()方法返回结果之前,jvm加载内部类,不存在线程安全问题。

4.注册式单例——枚举登记单例

package com.fatfat.sington.hungry.sington.reg;

/**
 * @ClassName EnumSingleton
 * @Auther LangGuofeng
 * @Date: 2019/7/28/028 17:01
 * @Description: TODO
 */
public enum  EnumSingleton {
    INSTANCE;
    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

补充: 感觉完全看不出来这是怎么实现的单例模式,有兴趣的同学可以下载一个反编译工具,将这个枚举类的.class文件反编译一下看看,你会发现反编译之后的代码中有静态代码块,在静态代码块中,给INSTANCE赋值的,属于饿汉单例。最强大的一点是,无论通过反射机制还是序列化反序列化都不能破话枚举式单例的实例的唯一性(应该是JDK1.6之后)。

5.注册式单例——容器缓存单例

package com.fatfat.sington.hungry.sington.reg;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @ClassName ContainerSingleton
 * @Auther LangGuofeng
 * @Date: 2019/7/28/028 17:19
 * @Description: TODO
 */
public class ContainerSingleton {

    private static Map ioc = new ConcurrentHashMap<>();

    private ContainerSingleton(){

    }

    public static final Object getBean(String className){
        synchronized (ioc){
            if(!ioc.containsKey(className)){
                Object obj = null;
                try {
                    obj = Class.forName(className).newInstance();
                    ioc.put(className, obj);
                }catch (Exception e){
                    e.printStackTrace();
                }
                return obj;
            }else {
                return ioc.get(className);
            }
        }
    }
}

补充: 容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的。

6.ThreadLocal单例(伪单例)

package com.fatfat.sington.hungry.sington.threadlocal;

/**
 * @ClassName ThreadLocalSingleton
 * @Auther LangGuofeng
 * @Date: 2019/7/28/028 17:34
 * @Description: TODO
 */
public class ThreadLocalSingleton {
    private static final ThreadLocal THREAD_LOCAL_SINGLETON_THREAD_LOCAL =
            new ThreadLocal() {
                @Override
                protected ThreadLocalSingleton initialValue() {
                    return new ThreadLocalSingleton();
                }
            };

    private ThreadLocalSingleton(){

    }

    public static final ThreadLocalSingleton getInstance(){
        return THREAD_LOCAL_SINGLETON_THREAD_LOCAL.get();
    }

}

补充: ThreadLocal 不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,天生的线程安全。前两天有个项目需要配置多数据源,在配置多数据源动态切换的时候就用到了这里,过些天有时间在分享。

三. 关于各种单例模式存在的问题

前面有一些小的问题,在介绍单例模式的写法的时候就已经解决了,比如说线程安全问题,内存占用问题等,下面主要说的是反射机制和序列化反序列化破坏单例模式的问题。
如何防止反射机制和序列化反序列化破坏单例模式

你可能感兴趣的:(设计模式)