零基础也能看懂的java设计模式---单例模式

一、单例模式介绍

1.什么是单例模式

所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。

2.单例模式的使用场景

需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数 据库或文件的对象(比如数据源、session工厂等)

3.评价单例模式方法的指标
  • 是否是单例模式?
  • 是饿汉式还是懒汉式?
  • 是否是线程安全的?
  • 效率如何?
4.实现单例模式的几种方法

具体看下文

二、饿汉式

饿汉式,实例在初始化的时候就已经建好了,不管你有没有用到,都先建好了再说。好处是没有线程安全的问题,坏处是浪费内存空间。

1.使用静态常量的方式实现
/**
 * @author 四五又十
 * @version 1.0
 * @date 2020/6/1 19:50
 */
public class SingleTon01 {

    //1. 构造器私有化, 外部能new
    private SingleTon01() {}

    //2.本类内部创建对象实例
    private static final SingleTon01 instance  = new SingleTon01();

    //3.提供一个公有的静态方法,返回实例对象
    public static SingleTon01 getInstance(){
        return instance;
    }
}


分析:使用静态常量的方式来创建,由于在类加载的时候创建该类的实例,所以不会有线程安全问题,也就是说是线程安全的,但是只要这个类一进行装载,那么该类就完成了实例化,也就是没有达到懒加载的目的。

优点:写法比较简单,就是在类装载的时候就完成实例化。避免了线程同 步问题

缺点:只要这个类一进行装载,那么该类就完成了实例化,这样有可能造成内存浪费

2.静态代码块实现

这种方式与上面就是表现形式不一样,其他都一样

/**
 * @author 四五又十
 * @version 1.0
 * @date 2020/6/1 19:50
 */
public class SingleTon02 {

    //1. 构造器私有化, 外部能new
    private SingleTon02() {}


    //2.本类内部创建对象实例
    private static SingleTon02 instance;

    static {
        instance = new SingleTon02();
    }

    //3.提供一个公有的静态方法,返回实例对象
    public static SingleTon02 getInstance(){
        return instance;
    }
}

三、懒汉式

1.有线程安全问题的实现
/**
 * @author 四五又十
 * @version 1.0
 * @date 2020/6/1 19:50
 */
public class SingleTon03 {

    //1. 构造器私有化, 外部能new
    private SingleTon03() {}

    private static SingleTon03 singleTon03;

    public static SingleTon03 getInstance(){
        if(singleTon03 == null){
            singleTon03 = new SingleTon03();
        }
        return singleTon03;
    }
}

分析:当需要使用该类时,getInstance()方法会判断当前对象类中是否有singleTon03的实例,如果没有那么创建,如果有直接返回,这样似乎做到了单例和懒加载,即需要时才进行加载;但是仔细一想,在多线程的环境下,如果其中A线程执行到了if判断并且通过了判断,然而此时切换到了B线程,那么显然B线程也是可以通过判断的那么会创建singleTon03的实例,如果此时切换回了A线程,那么A也会再次创建该对象的实例,这样就不是单例的效果!

所以这种方法不允许使用

2.加synchronized锁解决线程安全问题

那么很显然,要解决上述的问题,只需要在上述方法上加上synchronized锁即可:

/**
 * @author 四五又十
 * @version 1.0
 * @date 2020/6/1 19:50
 */
public class SingleTon04 {

    //1. 构造器私有化, 外部能new
    private SingleTon04() {}

    private static SingleTon04 singleTon03;

    public static synchronized SingleTon04 getInstance(){
        if(singleTon03 == null){
            singleTon03 = new SingleTon04();
        }
        return singleTon03;
    }
}

分析:当A,B线程想要同时执行getInstance方法时,由于加上了synchronized锁,那么其中一个线程只能等待,等轮到该等待的线程时,此时已经不能通过if判断,那么这样即使在多线程环境下也可以做到单例;synchronized是一个重量级的锁,很显然会带来效率问题

优点:解决了线程不安全问题

缺点: 效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行 同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例, 直接return就行了。方法进行同步效率太低

3.解决效率过低:Double-Check

这时,为了解决效率多低的问题,想当然的会想到一种方式:

/**
 * @author 四五又十
 * @version 1.0
 * @date 2020/6/1 19:50
 */
public class SingleTon05 {

    //1. 构造器私有化, 外部能new
    private SingleTon05() {}

    private static SingleTon05 singleTon03;

    public static SingleTon05 getInstance(){
        if(singleTon03 == null){
            synchronized(SingleTon05.class){
                singleTon03 = new SingleTon05();
            }

        }
        return singleTon03;
    }
}

也就是在代码块上加上synchronized锁

那么,这样到底有没有解决问题呢?没有!当线程A执行到if判断并且通过if判断时,此时如果线程B也执行if判断那么显然结果和第一种具有线程安全的实现是一样的,也就是没有解决线程安全问题。

为了要解决这个问题,提出了双重验证的方法,具体如下:

/**
 * @author 四五又十
 * @version 1.0
 * @date 2020/6/1 19:50
 */
public class SingleTon06 {

    //1. 构造器私有化, 外部不能new
    private SingleTon06() {}

    private static volatile SingleTon06 singleTon06;

    public static SingleTon06 getInstance(){
        if(singleTon06 == null){
            synchronized(SingleTon06.class){
                if(singleTon06 == null){
                    singleTon06 = new SingleTon06();
                }
            }
        }
        return singleTon06;
    }
}

分析:还是按照上面分析的思路,如果线程A,B同时进入if判断并且通过,那么先进入到if里面的线程进行里面的方法时,则会被加上一把synchronized锁,当执行完成创建好了该实例后,另外一个线程进入if里面,并且也执行加上了一把synchronized锁的代码,但是此时却是不能通过第二个if判断,因为singleTon06已经被先执行的进程创建好了,那么这样就实现了多线程下的安全问题,也同时大大提高了效率!

优点:线程安全;延迟加载;效率较高

在实际开发中,建议使用

volatile是一个轻量级的锁,主要有两个作用:

  1. 被volatile修饰的变量保证对所有线程可见

    2.禁止指令重排序优化

在这里,关键字volatile的作用主要是禁止指令重排序优化。首先先理解在执行new SingleTon06()中,CPU大概干了些什么事情:

1.看class对象是否加载,如果没有就先加载class对象;

2.分配内存空间,初始化实例;

3.调用构造函数;

4.返回地址给引用

但是在实际的过程中,CPU为了优化程序,可能会进行指令重排序,打乱这3,4这几个步骤,导致实例内存还没分配,就被使用了。

线程A执行到new Singleton06(),开始初始化实例对象,由于存在指令重排序,这次new操作,先把引用赋值了,还没有执行构造函数。这时时间片结束了,切换到线程B执行,线程B调用new Singleton06()方法,发现引用不等于null,就直接返回引用地址了,然后线程B执行了一些操作,就可能导致线程B使用了还没有被初始化的变量,所以要加上volatile禁止CPU进行指令重排

4.使用静态内部类
/**
 * @author 四五又十
 * @version 1.0
 * @date 2020/6/1 19:50
 */
public class SingleTon07 {

    //1. 构造器私有化, 外部能new
    private SingleTon07() {}

    private static class singleTon07Instance{
        private static final SingleTon07 INSTANCE = new SingleTon07();
    }
    
    public static SingleTon07 getInstance(){
        return singleTon07Instance.INSTANCE;
    }

}

要理解这种方式首先需要明确一点:

静态内部类不持有外部类的引用(《java核心技术》)

也就是说当外部类加载时,静态内部类并不会随之加载,那么这样利用类加载机制保证了懒加载!并且这个过程是线程安全的

四、使用枚举

/**
 * @author 四五又十
 * @version 1.0
 * @date 2020/6/1 19:50
 */
public enum  SingleTon08 {
    INSTANCE;
    public void method(){}
}

利用jdk1.5增加的枚举可以很好的实现单例,线程安全,懒汉式

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值