目录
一、单例模式的介绍
1、介绍
所谓的单例,就是指单实例,由该类自己负责创建对象,同时对外提供一种唯一访问该对象的方式。这样外部程序在使用时,直接访问即可。单例模式有以下几个注意点:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象(外部程序)提供这一实例。
2、应用场景
- 如果只要求生成一个对象的时候,如:一个国家的总统、班级的班长等。
- 当对象需要被共享的场合。单例模式只允许创建一个对象,所以共享该对象不仅可以节省内存,而且还能加快对象访问速度。如:Web 中的配置对象、数据库的连接池等。
- 当某个类需要被频繁的实例化,而创建的对象又频繁被销毁的时候。如多线程的线程池、网络连接池等。
3、优缺点
(1)优点
- 内存中只有一个对象,节省了内存空间。
- 避免了频繁的创建和销毁对象,提高了性能。
- 避免了对共享资源的多重访问。
(2)缺点
- 不适用于变化频繁的对象。
- 违背了单一职责原则,不仅有内部逻辑的实现,还要对外提供实例对象。
- 单例模式中不存在抽象层,因此单例类不利于扩展。
二、单例模式的实现
单例模式分为饿汉式和懒汉式两种。两种的区别在于:饿汉式一开始就创建了对象实例,而懒汉式则是在使用的时候才去创建对象实例。单例模式的实现过程基本遵循以下步骤:
- 构造方法私有化。
- 类的内部创建对象。
- 对外提供静态方法,用于获取对象实例。
单例模式的UML类图如下:
1、饿汉式
(1)实现方式一(静态常量)
代码如下:
class Singleton{
//创建对象
private static final Singleton instance = new Singleton();
//构造方法私有化
private Singleton(){}
//用于外部访问,获取实例对象
public static Singleton getInstance(){
return instance;
}
}
(2)实现方式二(静态代码块)
代码如下:
class Singleton{
//创建对象
private static Singleton instance;
//静态代码块中创建对象
static {
instance = new Singleton();
}
//构造方法私有化
private Singleton(){}
//用于外部访问,获取实例对象
public static Singleton getInstance(){
return instance;
}
}
我们可以使用下面的测试代码,来验证在外部获取到的对象是否为同一个对象。
public class SingletonMode {
public static void main(String[] args) {
Singleton obj1 = Singleton.getInstance();
Singleton obj2 = Singleton.getInstance();
//对比创建的两个对象地址是否相等
System.out.println(obj1 == obj2);//true
System.out.println(obj1.hashCode());//1163157884
System.out.println(obj2.hashCode());//1163157884
}
}
说明:通过上面的测试结果可以看到:通过两次使用静态方法获取单例类的对象,它们的地址和hashcode都是一致的,这样满足了我们的既定目标。
值得注意的是:饿汉式是在类加载的时候完成初始化工作,也就是说在访问单例对象之前就已经创建好了,所以并不存在线程安全问题(天生安全)。由于饿汉式一上来就创建了单例类的实例,在不使用的时候会存在内存浪费的问题。
2、懒汉式
(1)实现方式一(线程不安全)
代码如下:
class Singleton{
//创建对象
private static Singleton instance;
//构造方法私有化
private Singleton(){}
//用于外部访问,获取实例对象
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
同样的,我们可以通过上面的那个测试代码来验证两次获取到的是否为同一个对象实例。上述代码存在线程安全问题,虽然测试得到的结果与饿汉式的一致,但如果在多线程环境下,那么懒汉式就会出问题,来看下面的代码:
public class SingletonMode {
public static void main(String[] args) {
new Thread(()->{
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Singleton singleton = Singleton.getInstance();
System.out.println(singleton.hashCode());//913559464
}).start();
new Thread(()->{
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Singleton singleton = Singleton.getInstance();
System.out.println(singleton.hashCode());//1752078285
}).start();
}
}
上面的代码用两个线程去分别获取单例类的实例,如果运行多次,可以发现会出现两次的hashcode不一致。这是为什么呢?来分析一下:当线程A和线程B同时去获取对象实例时,假设此时线程发现instance为null,那么线程A还未来得及向下继续执行。正在这个时候,线程B也进来了,它发现instance也是null,也去创建了instance实例,这样就得到了两个实例对象,这就有违我们的初衷了。所以一般不建议使用这种方式。
(2)实现方式二(同步方法)
代码如下:
class Singleton{
//创建对象
private static Singleton instance;
//构造方法私有化
private Singleton(){}
//用于外部访问,获取实例对象
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
说明:这种方式虽然解决了线程安全问题,但是效率低下。每个类在获得对象实例的时候,都要同步。但实际场景下,我们只需要在instance去做实例化即可,当instance不为null的时候,直接返回即可。
3、双重检查(DCL)
代码如下:
class Singleton{
private static volatile Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
//判断instance是否为null,如果为空,再进行加锁操作
if(instance == null){
//同一时刻,只有持有锁的线程可以进入,其他线程等待。
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
说明:这种方法既保证了安全性,也提高了性能。
4、静态内部类
代码如下:
class Singleton{
private Singleton(){}
private static class Inner{
private static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return Inner.instance;
}
}
说明:这种方式在第一次调用getInstance()方法时,JVM才去加载 Inner 类并初始化instance实例,只有一个线程可以获得对象的初始化锁,其他线程无法进行初始化,这样就保证了对象的唯一性,达到了延迟加载和线程安全的目的。
5、枚举单例类
代码如下:
enum Singleton{
INSTANCE;
public void method(){}
}
public class SingletonMode {
public static void main(String[] args) {
Singleton singleton = Singleton.INSTANCE;
singleton.method();
}
}
说明:这种方式默认枚举实例的创建是线程安全的,并且在任何情况下都是单例。实际上枚举类隐藏了私有的构造器,而且枚举类的域是相应类型的一个实例对象。
三、单例模式的应用
上面讲了这么多,那么有没有地方是真正使用到单例模式呢?这里介绍一下JDK中的Runtime类,毕竟主角都是最后出场的。Runtime类的实现就是使用了饿汉式单例模式,部分源码如下:
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
....
}