专业的编程技术博客社区

网站首页 > 博客文章 正文

设计模式(一)单例模式(单例设计模式的作用)

baijin 2024-10-01 07:32:37 博客文章 7 ℃ 0 评论

设计模式

设计模式种类

设及模式主要分为三种,共计23种。

  • 创建型模式:单例模式、工厂模式、抽象工厂模式、原型模式、建造者模式。
  • 结构型模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
  • 行为型模式:模板方法模式、命令模式、访问者模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式(Interceptor模式)、状态模式、策略模式、职责链模式(责任链模式)。

单例模式

说明

单例模式是说一个类在系统中之后一个实例对象,该类会提供一个供调用者获取实例对象的静态方法。

使用场景

比如网站用户统计,每个用户统计都要增加计数,典型的单例模式场景。

饿汉式

就是说一个饿汉,就怕自己饿了没吃的,所以他总是提前准备好事物。 饿汉式有多种实现方式,接下来一一来尝试。 实现方式:

  • 私有(private修饰)默认构造函数,禁止使用new关键字创建类的实例对象
  • 创建类的成员常量,并初始化实例(使用new), 并使用private static final修饰该常量。
  • 暴漏一个静态函数(函数名通常使用getInstance),供调用者使用。

静态常量

package com.itlab1024.singleton;

/**
 * 静态常量方式实现单例模式
 * @author itlab1024
 */
public class Singleton {
    // 初始化类的实例对象,比如使用private static final 修饰。
    private static final Singleton singleton = new Singleton();

    /**
     * 比如使用private修改默认构造方法,禁止使用new关键字创建类的实例对象
     */
    private Singleton() {
    }

    /**
     * 通过获取唯一的类实例对象的方法
     * @return {@link Singleton}
     */
    private static Singleton getInstance() {
        return singleton;
    }
}

优缺点:实现简单,类初始化的时候就完成实例的初始化,无线程安全问题。但是因为是预创建,如果不使用的时候,类的实例对象也存在(没有延迟加载的效果),会造成内存浪费。

静态代码块

package com.itlab1024.singleton;

/**
 * 静态代码块方式实现单例模式
 * @author itlab1024
 */
public class Singleton2 {
    // 初始化类的实例对象,比如使用private static final 修饰。
    private static final Singleton2 singleton;
    // 在静态代码块中进行初始化
    static {
        singleton = new Singleton2();
    }

    /**
     * 比如使用private修改默认构造方法,禁止使用new关键字创建类的实例对象
     */
    private Singleton2() {
    }

    /**
     * 通过获取唯一的类实例对象的方法
     * @return {@link Singleton2}
     */
    private static Singleton2 getInstance() {
        return singleton;
    }
}

这种方式跟上面的方式是一样的。

懒汉式

懒汉式,主要体现在懒这个字上,也就是不会提前准备吃的,饿了的时候再去想办法。

懒汉式(线程不安全)

看如下代码

package com.itlab1024.singleton;

/**
 * 懒汉式实现单例,线程不安全
 * @author itlab1024
 */
public class Singleton3 {
    // 定义类的实例对象
    private static Singleton3 singleton;

    /**
     * 比如使用private修改默认构造方法,禁止使用new关键字创建类的实例对象
     */
    private Singleton3() {

    }

    /**
     * 通过获取唯一的类实例对象的方法,获取的时候来判断类的对象实例是否为空,如果为空就创建,否则就直接返回。
     * @return {@link Singleton3}
     */
    private static Singleton3 getInstance() {
        if (null == singleton) {
            singleton = new Singleton3();
        }
        return singleton;
    }
}

需要注意的是,在声明类的成员变量的时候我去掉了final关键字,这是因为我要在getInstance中判断并初始化类的实例对象。 这种创建方式能够实现延时加载的效果。因为获取实例对象的时候,先判断有没有,没有创建,有则直接返回。 但是要特别注意的是:这种实现方式,线程不安全,也就是多线程情况下,可能会出现创建多个类实例的情况。

实际开发中不能使用!

来分析下什么时候会出现这种情况:比如有两个线程,A和B。当A和B同时请求到if (null == singleton)的时候就会发现目前jvm中没有该类的实例对象 所以都会执行singleton = new Singleton3();代码,也就会出现该类的两个类实例对象。

懒汉式(线程安全)

要实现线程安全,就要借助synchronized关键字。可以用其修饰getInstance方法,来保证多线程访问的有序性。

package com.itlab1024.singleton;

/**
 * 懒汉式实现单例,线程安全
 * @author itlab1024
 */
public class Singleton4 {
    // 定义类的实例对象
    private static Singleton4 singleton;

    /**
     * 比如使用private修改默认构造方法,禁止使用new关键字创建类的实例对象
     */
    private Singleton4() {

    }

    /**
     * 通过获取唯一的类实例对象的方法,获取的时候来判断类的对象实例是否为空,如果为空就创建,否则就直接返回。
     * 使用synchronized关键字保证线程安全。
     * @return {@link Singleton4}
     */
    private static synchronized Singleton4 getInstance() {
        if (null == singleton) {
            singleton = new Singleton4();
        }
        return singleton;
    }
}

没错,这种写法确实解决了线程安全问题,但是他也引出来一个问题,什么问题呢?仔细分析上面的代码,会发现,synchronized关键字放到了方法上, 这就会出现如下情况:比如A线程调用了getInstance方法,此时之后的线程要等待,等类的实例对象创建完毕后返回,接下来后面的线程继续访问getInstance, 特别注意:此时后面的这些线程依然需要同步等待(就是因为synchronized修饰在了方法上)。但是这不应该,因为此时类实例对象已经创建完毕了,其他的 线程来获取的时候直接返回就可以了(没有线程安全问题了)。 也就是说synchronized同步的范围过大了,应该只同步创建类实例对象的相关代码。 这也就引出了另一种创建方式。代码块方式。 实际开发中不允许使用!

懒汉式(同步代码块)【一】

package com.itlab1024.singleton;

/**
 * 懒汉式实现单例,线程不安全,同步代码块
 *
 * @author itlab1024
 */
public class Singleton6 {
    // 定义类的实例对象
    private static Singleton6 singleton;

    /**
     * 比如使用private修改默认构造方法,禁止使用new关键字创建类的实例对象
     */
    private Singleton6() {

    }

    /**
     *
     * @return {@link Singleton6}
     */
    private static Singleton6 getInstance() {
        if (null == singleton) {
            synchronized (Singleton6.class) {
                singleton = new Singleton6();
            }
        }
        return singleton;
    }
}

跟上面的代码相比,我只修改了getInstance方法,将同步方法修改为了同步代码块。这确实是缩小了同步的范围。 修改之后,我们看下,当类的实例对象没有创建的时候会进入同步代码块,上锁,然后创建类的实例对象,以后在请求getInstance方法的时候,就不会走同步 代码块,也就是说不上锁。直接返回了类的实例对象。 但是可但是。他也是有问题的,什么问题呢?当多个都走到同步代码块的时候,是加上锁了,但是同时访问到改代码块的线程最终都是能获取到锁的,也就是最终 依然创建多个类的实例对象。

实际开发中不允许使用!

懒汉式(同步代码块)【二】 (线程安全、双重检查[Double Check])

上面的问题,我们可以再一层检查

package com.itlab1024.singleton;

/**
 * 懒汉式(同步代码块)【二】 (线程安全、双重检查[Double Check])
 *
 * @author itlab1024
 */
public class Singleton7 {
    // 定义类的实例对象
    private static volatile Singleton7 singleton;

    /**
     * 比如使用private修改默认构造方法,禁止使用new关键字创建类的实例对象
     */
    private Singleton7() {

    }

    /**
     * @return {@link Singleton7}
     */
    private static Singleton7 getInstance() {
        if (null == singleton) {
            synchronized (Singleton7.class) {
                if (null == singleton) {
                    singleton = new Singleton7();
                }
            }
        }
        return singleton;
    }
}

与之前不同的是,上面代码再new实例之前,又判断了一次是否为空. 这就解决了,多线程同时进入代码块的时候会创建多实例的情况。同时变量使用了volatile修饰。他有什么作用呢? 要说清楚这个问题,就得说明下类的初始化问题。问题就出现在instance = new Singleton7()这断代码上。 在java的内存模型中,volatile关键字作用可以是保证可见性或者禁止指令重排,instance = new Singleton7()其实并不是一个原子操作。 他是会被分为三步的:

  • 1:是给 singleton 分配内存空间;
  • 2:开始调用 Singleton 的构造函数等,来初始化 singleton;
  • 3:将 singleton 对象指向分配的内存空间(执行完这步 singleton 就不是 null 了)。

分为了上面三步,但是实际却不是安装上面这三步顺序执行的,计算机会将指令重排。 假设按照指令重排后按照1-3-2的顺序执行,那么当执行到3的时候,此时instance肯定就不是null了,这时候,另一个线程也来获取instance。然后判断是否为 空,发现不是空,就返回了。但是该对象并没有执行2步,所以就报错了。 此时volatile关键字保证可见性或者禁止指令重排的作用就发挥作用,告诉计算机不要重拍指令,就按照上面的顺序执行就行了。如果不执行完毕,那么对象 就是null的。

volatile 的可见性和禁止指令重排,我这里就简单说明下,不懂的可以自行百度,详细了解下。

生产环境强烈推荐使用,线程安全!

静态内部类方式的单例

上面尝试了饿汉式和懒汉式的写法,接下来来看下静态内部类的方式。

package com.itlab1024.singleton;

/**
 * 静态内部类的方式创建单例
 *
 * @author itlab1024
 */
public class Singleton8 {

    /**
     * 比如使用private修改默认构造方法,禁止使用new关键字创建类的实例对象
     */
    private Singleton8() {

    }
    // 内部类
    private static final class SingletonHolder {
        private static final Singleton8 singleton = new Singleton8();
    }

    /**
     * @return {@link Singleton8}
     */
    private static Singleton8 getInstance() {
        return SingletonHolder.singleton;
    }
}

这里有一个内部类SingletonHolder里面定义了一个静态的Singleton8实例对象,在getInstance中直接返回该类实例对象。 这里就得提一个概念,类装载是安全的,在外部类装在的时候,内部类是不会装载的,也就是说,当调用getInstance的时候,内部类SingletonHolder才会 装载,并且初始化了一个类的单例类对象(线程安全的)。

此种方法,没有使用懒汉式,双重检查那样的volatilesynchronized关键字,而是借助了类的加载机制(线程安全)的策略,代码精简,同时也线程安全,强烈推荐使用!

枚举方式的单例

上面所有的实现方式其实都有一个问题,那就是无法阻止通过反射来阻止创建类的实例对象。但是枚举方式却可以! 枚举方式,即简单,又安全


package com.itlab1024.singleton;

/**
 * 枚举方式创建单例
 *
 * @author itlab1024
 */
public enum Singleton9 {
    INSTANCE;

    public void counter() {

    }

    public static void main(String[] args) {
        Singleton9.INSTANCE.counter();
    }
}

可以看到,我定义了一个枚举类,定义了一个INSTANCE,然后使用其调用了counter方法。不管多少线程来访问,都不会出现线程问题。而且也无法通过反射来 创建。 这是为什么呢?枚举为什么有这个能力呢?

  • 首先:因为虚拟机加载枚举类时候会保证线程安全的被初始化。
  • 另外因为在序列化方面,Java中有明确规定,枚举的序列化和反序列化是有特殊定制的。这就可以避免反序列化过程中由于反射而导致的单例被破坏问题。

枚举方式强烈推荐使用,据说在Effective Java一书中提到,并且也是强烈推荐的!

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表