Java--单例模式

本文详细介绍了设计模式中的单例模式,包括其概念、应用场合和多种实现方法,如饿汉模式、懒汉模式、静态内部类、枚举以及双重校验锁,并讨论了各种实现的优缺点和线程安全性。

前言

这里我要介绍的是设计模式中的单例模式,我会介绍设计模式,单例模式的概念和应用的场合。

接下来就是单例模式的这几种写法啦,饿汉,懒汉,静态内部类,枚举和双重校验锁。

设计模式

设计模式 (Design Pattern):是一套被反复是用,多数人知晓的,经过分类编目的,代码设计经验的总结.

目的:使用设计模式是为了可重用代码,让代码更容易被别人理解,保证代码可靠性.

设计模式有许多,这里就介绍最基本,也是最常用的单例模式。

单例模式

有些对象只需要一个,如:配置文件、工具类、线程池、缓存、日志对象等。

多个程序读取一个配置文件,实际上配置文件还是只有一个,如果创造出多个实例,就会导致很多问题,占用资源过多,不一致的结果等。

而要保证整个应用中只有一个实例,就要用单例模式来实现啦。

方法

首先我们先来创建一个 SingleTon 类:

/*
 * 单例模式 SingleTon
 * 应用场合:有些对象只需要一个就够了
 * 作用:保证整个应用程序中实例的个数有且只有一个
 */
public class Singleton {

}

饿汉模式

我们通常创建对象,是通过 new 构造方法来实例化,每次去实例化就是创建了一个新的对象,所以要想只实例化一次,就要从构造方法入手。

public class Singleton {

    //1.将构造方法私有化,不允许外部直接创建对象
    private Singleton() {
    }

    //2.创建类的唯一实例,使用private static修饰
    private static Singleton instance = new Singleton();

    //3.提供一个用于获取实例的方法,使用public static修饰
    public static Singleton getInstance() {
        return instance;
    }
}

从上面的代码可以清楚的知道,我们为了不频繁的创建 SingleTon 的实例,就将构造方法私有化,然后在类里创建一个静态的实例对象,最后通过类方法去获取这个对象。我们用 Test 类来测试一下每次获取的实例是不是同一个。

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

        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        if (s1 == s2) {
            System.out.println("s1和s2是同一个实例");
        } else {
            System.out.println("s1和s2不是同一个实例");
        }
    }
}

因为 == 是判断两个变量是不是同一个引用,所以它们相等说明是同一个实例。

之所以叫饿汉,是因为这个实例是在类被创建时就创建啦,所以把它看作饥饿的人迫不及待的想要吃到这个“实例”。

虽然是类装载时实例化,但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到单例,这是要注意的地方。

懒汉模式

public class Singleton {

    //1.将构造方法私有化,不允许外部直接创建对象
    private Singleton2() {
    }

    //2.申明类的唯一实例,使用private static修饰
    private static Singleton2 instance = null;

    //3.提供一个用于获取实例的方法,使用public static修饰
    public synchronized static Singleton2 getInstance() {
        if (instance == null) {
            instance = new Singleton2();
        }
        return instance;
    }
}

懒汉模式就是在类加载是并不实例化对象,直到调用方法时才实例化,所以是懒汉模式。饿汉模式的特点是加载类时比较慢,但运行时获取对象的速度比较快,线程安全,懒汉模式相反。为了线程安全,因为加了synchronize,所以虽然线程安全,但效率很低。

静态内部类

public class Singleton {
        private static class SingletonHolder {
            private static final Singleton instance = new Singleton();
        }

        private Singleton() {
        }

        public static final Singleton getInstance() {
            return SingletonHolder.instance;
        }
    }

这种方式同样利用了类加载的机制来保证初始化 instance 时只有一个线程,但它跟懒汉和饿汉方式不同的是(很细微的差别),懒汉和饿汉是只要Singleton类被装载了,那么 instance 就会被实例化(没有完全达到lazy loading效果)。

而这种方式是Singleton类被装载了,instance却不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有显示通过调用 getInstance 方法时,才会显示装载 SingletonHolder 类,从而实例化 instance 。

想象一下,如果实例化 instance 很消耗资源,我们想让它延迟加载,另外一方面,我不希望在 Singleton 类加载时就实例化,因为我不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比懒汉和饿汉方式显得更合理。

枚举

这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

public class Singleton {

    public enum Something {  
        INSTANCE;  
        private Singleton instance;
        private Something() {
            instance = new Singleton();
        }
        public Singleton getInstance() {
            return instance;
        }
    } 
}

首先,在枚举中我们明确了构造方法限制为私有,在我们访问枚举实例时会执行构造方法,同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。

也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也被保证实例化一次。

让我们用 Test 来测试一下:

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

        Singleton s1 = Singleton.Something.INSTANCE.getInstance();
        Singleton s2 = Singleton.Something.INSTANCE.getInstance();
        if (s1 == s2) {
            System.out.println("s1和s2是同一个实例");
        } else {
            System.out.println("s1和s2不是同一个实例");
        }
}

我们再来看一下Enum这个类的声明:

public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable

可以看到,枚举也提供了序列化机制。某些情况,比如我们要通过网络传输一个数据库连接的句柄,会提供很多帮助。

单元素的枚举类型已经成为实现Singleton的最佳方法。

双重校验锁

“双重检查加锁“的方式可以既实现线程安全,又能够使性能不受到很大的影响。

所谓双重检查加锁机制,指的是:并不是每次进入 getInstance 方法都需要同步,而是先不同步,进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查。

进入同步块后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。

双重检查加锁机制的实现会使用一个关键字volatile,被volatile 修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。

public class Singleton {

    private volatile static Singleton instance;  
    private Singleton (){}  
    public static Singleton getInstance() {  
    if (instance == null) {  
        synchronized (Singleton.class) {  
           if (instance == null) {  
              instance = new Singleton();  
           }  
        }  
    }  
    return instance;  
    } 
}

说明:由于volatile关键字可能会屏蔽掉虚拟机中的一些必要的代码优化,所以运行效率并不是很高。因此一般建议,没有特别的需要,不要使用。也就是说,虽然可以使用”双重检查加锁“机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。

以上就是要给大家讲的所有内容啦,希望大家能够从这篇博客中学到一些东西,将单例模式应用到工作开发中去,这将提高我们的代码效率和编程水平。

结束语:本文仅用来学习记录,参考查阅。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值