• 首页

  • 文章归档

  • 隔壁朋友

  • 关于博主
H B L A O
H B L A O

hblao

获取中...

03
11
java

Java设计模式-单例模式

发表于 2021-03-11 • Java设计模式-单例模式 • 被 47 人看爆

单例模式(Singleton Pattern):
类型:创造型模式。

在这个模式中,指的是一个类的实例只能被初始化一次,后续使用的时候一直使用的就是这个初始化的对象即可,在整个程序运行的期间,这个对象始终是同一个。

如何创建?
说到整个类对象只存在一个,很快就能够让人联想到使用static关键字修饰,确实如此,下面介绍一下如何创建单例模式对象。

构造方法私有化。这样做的目的是什么,目的就是只有本类才能使用这个构造方法,才可以创建该对象,而其他类是不可以创造这个类的对象的,这样就保证了创建对象只能由本类来完成。
持有一个本类的静态对象,并用static关键字修饰。既然只能由本类持有该对象,所以在本类中有一个静态的对象,被static关键字修饰,只会被创建一次;这里的只被创建一次的意思就是只被new一次,否则多次new的话会导致产生不一样的对象;
提供外部访问接口函数。同样的,如果本类持有的这个单例对象是public的话,容易被外部给修改,所以设置其为私有变量,通过函数提供的外部访问接口进行访问。
有哪些不同形式的单例模式?
饿汉式
/**
*

  • 饿汉式的单例模式;

*/

public class SingleMonkey {

// 构造方法要私有化;
private SingleMonkey(){}
// 猴子的年龄;
private int age = new Random().nextInt(100);

// 手握一个本类static实例,一开始就加载实例;
private static SingleMonkey singleMonkey = new SingleMonkey();

// 提供一个外部访问接口,用来获取Singleton实例;
public static SingleMonkey getSingleMonkey() {
    return singleMonkey;
}

public void getAge() {
    System.out.println("The Monkey's age is "+ age);
}

}
说到饿汉式,顾名思义就是创建类的时候就new了一个对象,然后通过get函数返回这个对象即可;这里变量年龄age的作用就是用来判断其是否是同一个对象的;由于年龄是随机的,所以每次new一个对象的时候,很大几率不一样;

接下来我们来运行一下:

public class TestDemo {
public static void main(String [] args) {

    /**
     *  饿汉式;
     */
    for (int i = 0; i < 10; i++) {
        // 获取monkey实例;
        SingleMonkey singleMonkey = SingleMonkey.getSingleMonkey();
        // 调用其内部方法;
        singleMonkey.getAge();
    }

}
结果:

The Monkey's age is 73
The Monkey's age is 73
The Monkey's age is 73
The Monkey's age is 73
The Monkey's age is 73
The Monkey's age is 73
The Monkey's age is 73
The Monkey's age is 73
The Monkey's age is 73
The Monkey's age is 73
你会发现这个猴子的age都是73,这也反映了这个猴子对象是同一个对象;

懒汉式
懒汉式是针对饿汉式的缺点进行改正的,由于饿汉式在类加载的时候就new了对象(无论使用与否),这会导致内存的浪费,而懒汉式的思想则是需要使用的时候才去new对象,以下是代码:

/**
*

  • 懒汉式的单例模式;

*/

public class SingleMonkey {

// 构造方法要私有化;
private SingleMonkey(){}

private int age = new Random().nextInt(100);

// 手握一个本类static实例,一开始先不加载实例;
private static SingleMonkey singleMonkey;

// 提供一个外部访问接口,用来获取Singleton实例,需要的时候才去新建对象;
public static SingleMonkey getSingleMonkey() {
    if (singleMonkey == null) {
        singleMonkey = new SingleMonkey();
    }
    return singleMonkey;
}

public void getAge() {
    System.out.println("The Monkey's age is "+ age);
}

}
我们同样运行上面的调用函数,结果如下:

The Monkey's age is 79
The Monkey's age is 79
The Monkey's age is 79
The Monkey's age is 79
The Monkey's age is 79
The Monkey's age is 79
The Monkey's age is 79
The Monkey's age is 79
The Monkey's age is 79
The Monkey's age is 79
乍一看似乎没有什么问题,但是懒汉式的问题是存在的,并且很严重;在处理单线程的时候,懒汉式不会出现问题,但是在处理多线程的时候,懒汉式的弊端就出来了,我们来运行下列代码:

/**
* 懒汉式;
*/
for (int i = 0; i < 10; i++) {
new Thread() {
public void run(){
SingleMonkey.getSingleMonkey().getAge();
}
}.start();
}
这里是建立了10个线程,每个线程都去调用singleMonkey中的getSingleMonkey方法,结果如下:

The Monkey's age is 0
The Monkey's age is 61
The Monkey's age is 89
The Monkey's age is 63
The Monkey's age is 61
The Monkey's age is 73
The Monkey's age is 89
The Monkey's age is 35
The Monkey's age is 27
The Monkey's age is 68
结果是这样的,很多猴子的年龄竟然不一样,这样至少说明了他们不是同一个对象,为什么会出现这样的问题呢?

我们得了解线程的大致工作原理,在一个程序运行的时候,会有主内存,这些地方是用来存放程序中变量的,但我们开启一个线程的时候,这里面的变量就会拷贝到线程的变量区,每一个线程都有自己独立的变量区,它们相互独立,没有联系;

并且线程在内存中是需要抢夺资源的,也就是说它们不会好好的排队,一个一个按顺序的执行完毕,有可能一个线程执行到某个阶段,它就中断了,原因是它的CPU执行权被其他线程夺去了,于是乎它会静静地等待,等到CPU执行权重新回到自己手上的时候,就从它中断的地方继续执行下去;

那我们来看看懒汉式的代码:

public static SingleMonkey getSingleMonkey() {
if (singleMonkey == null) {
singleMonkey = new SingleMonkey();
}
return singleMonkey;
}
我们有10个线程在并发执行,每个线程都在不断地争夺资源,现在我们假设线程A抢到了资源,于是它加载进去,可是好景不长,在他执行到getSingleMonkey方法中的if判断中,正要准备new对象的时候,它被中断了,被线程B抢夺走了资源,此时singleMonkey仍然是null(因为A没有成功new对象就被中断了),所以线程B也通过了if的判断,进入到了if中,然后成功new了一个SingleMonkey对象;所以此时在线程B中是一个age为60的singleMonkey(假设);在这之后线程A卧薪尝胆,终于有朝一日,它重新夺回了CPU的执行权,然后从它中断的地方,也就是if中继续执行,然后线程A也成功new了一个SIngleMonkey对象,假设其年龄为17....

那这样一来我们就产生了两个不同的对象,这就是多线程在懒汉式中产生的问题,其实懒汉式并不是严格意义的单例模式,因为它的对象不止一个;造成这种问题的原因在于,多个线程同时调用getSingleMonkey方法,导致了多个对象的产生;

要解决这个问题,就需要对这个方法加上锁,意义在于每次只能有一个线程对该方法进行调用,也就是使用synchronized关键字;设想一个情况,我们的方法被放到一个房子中,一开始这个房子没有锁,那么房子外面的线程就可以争抢着进到房子里面去;但是加上synchronized关键字之后呢,就相当于给房子上了锁,每次拿到钥匙的线程才可以进去,等它处理完毕之后,出来把钥匙交给下一个线程的时候,得到钥匙的线程才可以进去房子里面,这样一来就保证了线程会一个一个调用这个方法,也就不会出现同时调用的情况;

代码如下:

// 提供一个外部访问接口,用来获取Singleton实例;
public synchronized static SingleMonkey getSingleMonkey() {
    if (singleMonkey == null) {
        singleMonkey = new SingleMonkey();
    }
    return singleMonkey;
}

运行结果:

The Monkey's age is 68
The Monkey's age is 68
The Monkey's age is 68
The Monkey's age is 68
The Monkey's age is 68
The Monkey's age is 68
The Monkey's age is 68
The Monkey's age is 68
The Monkey's age is 68
The Monkey's age is 68
会发现结果变回来了.

但是如果了解synchronized锁的话,你们会知道其实它是一个比较重的锁,就是操作起来会很费劲,占用运行内存,因为如果每次都要去判断一下锁对象,就会增加更多的时间;下面是对加锁懒汉式的改进:

DCL式(锁双检验式) ps:名字瞎取的 :)
public class SingleMonkey {

// 构造方法要私有化;
private SingleMonkey(){}

// 猴子的年龄;
private int age = new Random().nextInt(100);

// 手握一个本类static实例,一开始先不加载实例;
private volatile static SingleMonkey singleMonkey;

// 提供一个外部访问接口,用来获取Singleton实例,这里使用双检查;
public static SingleMonkey getSingleMonkey() {
    if (singleMonkey == null) {
        synchronized (SingleMonkey.class) {
            if (singleMonkey == null) {
                singleMonkey = new SingleMonkey();
            }
        }
    }
    return singleMonkey;
}

public void getAge() {
    System.out.println("The Monkey's age is "+ age);
}

}
到这里你可能会有疑问,因为线程都是有自己独立的工作区间的,尽管new了对象,但是可能线程还没有执行完,这个对象还没有加载到主存中(,所以其他线程判断的时候,单例对象仍为null,依然会进入到第一层循环的synchronized锁前等待;

首先可以观察到,它对SingleMonkey是否为空的判断进行了两次,然后synchronized锁放在了这两次之间,这里主要说明一下最外层的if判断,内层的和上面加锁的懒汉式思想差不多;为什么会在最外层在判断一次呢?是因为每次让线程来开锁都会占去一部分开销,所以我们在判断是否开锁的时候,优先判断一下这个单例对象是否存在,如果存在了,直接用就好了,不必再去开锁判断;

到这里你可能会有疑问,因为线程都是有自己独立的工作区间的,尽管new了对象,但是可能线程还没有执行完,这个对象还没有加载到主存中(这里的主存是线程资源共享的地方),以其他线程判断的时候,单例对象仍为null,依然会进入到第一层循环的synchronized锁前等待;

但是请注意单例对象前的volatile关键字,这个关键字在这里就起到了作用,被volatile关键字修饰的变量或变量,只要其发生改变,就会立刻将其加载到主存中(null -> new SingleMonkey()),也就是说,这个时候对于所有线程来说,这个对象都是可见的,这个实例已经有了,不再是null了.

所以上面的方法的锁,只会在使用少数几次(最开始第一个if语句冲进来了几个线程),等到单例对象成功创建了之后,由于第一层判断进不去,这个锁也再也用不着了,也就省去了每个线程都判断锁的状态(尤其在线程多,或者功能繁琐的时候,这种方法效果或更加显著).

优缺点?

饿汉式:

优点:不会出现多线程的同步问题;由于没有锁,效率很高;
缺点:类加载的时候就创建对象,初始化,浪费内存;(现在这不是问题了)
懒汉式:

优点:需要时才会初始化,节省内存;
缺点:线程不安全,会出现多线程同步问题;
加锁懒汉式:

优点:不会出现多线程同步问题;
缺点:使用锁,效率不高;
DCL式:

优点:效率较高,并且不会出现多线程同步问题;
缺点:这个锁只有一开始有用,后面基本用不着了(不知道算不算缺点)
单例模式的优缺点:
优点:

优点:在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例;避免对资源的多重占用;

缺点:

没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化;

分享到:
Java设计模式-工厂模式
java设计模式-模板模式
  • 文章目录
  • 站点概览
hblao

Hihblao

QQ Email RSS
看爆 Top5
  • 人到中年 188次看爆
  • Java-笔记-2021年(持续更新) 144次看爆
  • 树轮 117次看爆
  • 暖春微凉 104次看爆
  • Spring AOP 67次看爆

Copyright © 2021 hblao · 京ICP备16001163号

Proudly published with Halo · Theme by fyang · 站点地图 · 百度统计