目录
  1. 1. 单例模式
    1. 1.1. 定义
    2. 1.2. 单例模式示意图
    3. 1.3. 流程
    4. 1.4. 代码实现
    5. 1.5. 应用场景
    6. 1.6. 单例模式优缺点
      1. 1.6.1. 优点
      2. 1.6.2. 缺点
    7. 1.7. 单例的扩展
    8. 1.8. 文献引用
单例模式

单例模式

定义

Ensure a class has only one instance , and provide a global point of access to it.
确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

单例模式示意图

单例模式

流程

  1. 客户向单例提供者请求实例
  2. 单例提供者使用类名.getInstance()从单例类中获取唯一实例
  3. 这个唯一实例在单例类中先被创建(单例类只能创建一个实例)。单例类通过暴露getInstance()这个静态方法,让外界可以获取这个单个实例。

代码实现

  1. 普通单例模式(饿汉式)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 普通单例模式(饿汉式)
* 一开始就初始化,线程安全,比较常用,但容易产生垃圾
*
* 为什么线程安全?
* 类加载的方式是按需加载,且只加载一次。当单例类加载完成后,单例实例就被创建供系统使用,线程每次只能获取这一个实例。
*/
public class SingleTonCommon {
private static final SingleTonCommon singleTon = new SingleTonCommon();

/* 不让外界调用创建对象,只能让singleTon成员变量创建对象 */
private SingleTonCommon(){

}

public static SingleTonCommon getInstance(){
return singleTon ;
}

/* 单例类里面的类方法不能被外界访问 */
public static void test(){
System.out.println("普通单例模式(饿汉式)");
}

/* 可以被外界访问的方法 */
public void getTest(){
test();
}
}
  1. 普通单例模式(懒汉式)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 普通单例模式(懒汉式)
* 线程不安全,延迟初始化。
*
* 为什么线程不安全?
* 当两个线程同时创建单例对象,线程1到达singleTon == null , 线程1判断完后,还没有创建对象,线程2就抢占cpu资源,开始运行。
* 这时线程2也到达singleTon == null,并且判断为空,创建一个对象。之后释放cpu资源,线程1再次抢占cpu资源。
* 这时线程1还是原来的状态singleTon == null,判断为空,线程1再次创建一个对象,这就产生两个对象。
*/
public class SingleTonCommon2 {
private static SingleTonCommon2 singleTon ;

private SingleTonCommon2(){

}

public static SingleTonCommon2 getInstance(){
if (singleTon == null){
singleTon = new SingleTonCommon2();
}
return singleTon ;
}

/* 单例类里面的类方法不能被外界访问 */
public static void test(){
System.out.println("普通单例模式(懒汉式)");
}

/* 可以被外界访问的方法 */
public void getTest(){
test();
}
}
  1. synchronized单例模式(懒汉式线程安全版)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* synchronized单例模式单例模式
*
* 线程安全,延迟初始化
*
* 为什么线程安全?
* 双重锁单例模式就是懒汉式的升级版,懒汉式在singleTon == null判断时可能会有其他线程能创建多的对象。
* 但是这里使用synchronized同步锁,使得每次判断只能进一个,就只能创建一个对象。
*/
public class SingleSynchronized {
private static SingleSynchronized singleTon ;

private SingleSynchronized(){

}

public static SingleSynchronized getInstance(){
// 第一次判断singleTon == null是在synchronized代码块之外判断,为了避免已经创建了对象还要进入同步代码块的情况,提高效率。
if (singleTon == null){
// 与懒汉式情况不同,当线程1到singleTon == null,并进入判断,但是还没创建线程就被线程2抢占cpu资源,然后线程2先一步创建对象
// 这时虽然线程1已经进入判断,但是因为加入了synchronized线程同步锁,synchronized保证两个线程都是为了创建同一个对象,线程2已经创建线程1就不需要再创建了。
synchronized (SingleSynchronized.class){
if (singleTon == null){
singleTon = new SingleSynchronized();
}
}
}
return singleTon ;
}

/* 单例类里面的类方法不能被外界访问 */
public static void test(){
System.out.println("synchronized单例模式");
}

/* 可以被外界访问的方法 */
public void getTest(){
test();
}
}
  1. volatile单例模式(懒汉式线程安全版)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/** volatile可以保证可见性,同时保证JVM指令不会进行重排序。
* 创建对象是需要有几个步骤,通过jvm指令来创建的
* 创建对象步骤:获取1.singleTon对象需要的内存地址-->2.初始化singleTon对象-->3.将引用变量singleTon指向获取的内存地址
* volatile禁止jvm对指令进行重排序,因此指令顺序总是1-->2-->3
*/
public class SingleTonVolatitle {
private volatile static SingleTonVolatitle singleTon ;

private SingleTonVolatitle(){

}

/**
* 虽然volatile不能保证原子性,但是singleTon赋值堆中的引用是原子操作
* 如果被两个线程执行, new SingleTonVolatitle()会执行两次,但是赋值操作只进行一次。
* @return
*/
public SingleTonVolatitle getInstance(){
if (singleTon == null) {
singleTon = new SingleTonVolatitle();
}
return singleTon ;
}

/* 单例类里面的类方法不能被外界访问 */
public static void test(){
System.out.println("volatile单例模式");
}

/* 可以被外界访问的方法 */
public void getTest(){
test();
}
}
  1. 静态内部类单例模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 静态内部类单例模式
*
* 只有第一次调用getInstance方法时,虚拟机才加载静态内部类并初始化,只有一个线程获得对象的初始化锁,其他线程无法进行初始化。
*/
public class SingleTonStaticInner {
private SingleTonStaticInner(){

}

public SingleTonStaticInner getInstance(){
return Inner.singleTon ;
}

/**
* 静态内部类只有在被加载的时候才会被初始化,并且其生命周期直到系统结束才会结束。
* 这样就保证有且只有一个对象,不管有多少为线程调用getInstance,都是取的同一个对象
* 因此,静态内部类能保证单例唯一性,延迟单例实例化以及保证线程安全
*
* 但是静态内部类无法传递参数
*/
private static class Inner{
private static final SingleTonStaticInner singleTon = new SingleTonStaticInner();
}
}
  1. 枚举单例模式
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 枚举单例模式
* 枚举和普通类一样,都拥有字段和方法
* 枚举的构造方法只能被声明为私有构造方法
* 枚举是线程安全的,并且在任何情况下,它都是一个单例(我们通过枚举类.INSTANCE就能获得枚举对象)
*/
public enum SingleTonEnum {
INSTANCE;

public static SingleTonEnum Instance(){
return SingleTonEnum.INSTANCE ;
}
}
  1. 静态代码块单例模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 静态代码块单例模式
* 静态代码块是在单例类在虚拟机中一加载就被调用,并且只调用一次
* 因此静态代码块单例也是线程安全的
*/
public class SingleTonStatic {
private static SingleTonStatic singleTon ;
private SingleTonStatic(){

}

static {
singleTon = new SingleTonStatic();
}
}

应用场景

  1. 要求生成唯一序列号的环境。
  2. 在整个项目中需要一个共享访问点或共享数据,例如一个web页面的计数器,不用把每次刷新都记录到数据库,使用单例模式保持计数器的值,并确保线程安全。
  3. 创建一个对象需要消耗的资源很多,如要访问IO或数据库。
  4. 需要定义大量的静态常量或静态方法。

单例模式优缺点

优点

  1. 对象的创建会占用内存,只有一个实例对象,这样减少内存开支。
  2. 对象会占用系统资源,只有一个实例对象,会减少系统性能开销。
  3. 多个实例在内存中可能会产生对多重占用问题,单例模式可以避免这种情况发生。
  4. 单例模式可以在全局设置一个访问点,这样可以优化和共享资源访问。

缺点

  1. 没有接口,扩展困难。
  2. 在并发环境下,单例模式没有完成是不能进行测试的,也不能使用mock虚拟一个对象。
  3. 单例模式与单一职责原则有冲突。

单例的扩展

能产生固定数量对象的模式叫有上限的多例模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.ArrayList;

/**
* 单例的扩展
* 有上限的多例
*/
public class SingleTonExtension {
/* 定义需要产生的对象数量 */
private static int maxNumSingleTon = 2 ;

/* 将所有产生的对象放入这个容器,ArrayList是线程不安全的,可以使用线程安全的容器 */
private static ArrayList<SingleTonExtension> singleTons = new ArrayList<>();

private SingleTonExtension(){

}

/* 使用静态代码块实现单例 */
static {
for (int i=0 ; i<maxNumSingleTon ; i++) {
singleTons.add(new SingleTonExtension());
}
}
}

文献引用

  1. Java双重校验锁实现单例模式
  2. 设计模式之单例模式
  3. volatile
  4. Java 枚举
  5. Java之static静态代码块
  6. 深入理解单例模式:静态内部类单例原理
文章作者: rack-leen
文章链接: http://yoursite.com/2019/05/19/Java/Java%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/%E5%88%9B%E5%BB%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 rack-leen's blog
打赏
  • 微信
  • 支付宝

评论