单例模式作为23种设计模式之一,有着它特定的需求场景,比如一些内部数据结构不需要发生变化的实例(一些工具类)。
单例模式的核心就是只允许有一个该类的静态实例,并且这个静态实例必须由这个类自己对外提供,也就是说只能由这个类自己实例化自己。由于只允许这个类实例化自己,就意味着我们在其他类里不能使用new关键字为这个类实例化,所以这个类的构造函数应该用private修饰。
单例模式分为三种模式,分别为饿汉式,懒汉式,静态内部类。
饿汉式单例中,类在一进入内存就完成了对自身静态实例的初始化,永久驻存在内存中。所以即使有多个线程在同一时刻调用该类的获取实例方法,也不会存在该对象再次被初始化的情况。饿汉式是线程安全的。
饿汉实例
package com.huang.Instance;
//饿汉模式
public class HungryInstance {
private HungryInstance(){};
private static HungryInstance hungryInstance = new HungryInstance();
public static HungryInstance getInstance(){
return hungryInstance;
}
}
获取该实例只需要通过HungryInstance.getInstance()方法即可获得该类的唯一静态实例。
在懒汉式单例中,类不会在被加载进内存的时候直接将自身实例初始化,而是在对外提供的获取实例的方法内进行初始化。具体实现如下
package com.huang.Instance;
//懒汉模式
public class LazyInstance {
private LazyInstance() {
};
private static LazyInstance lazyInstance = null;
public static LazyInstance getInstance() {
if (lazyInstance == null) {
lazyInstance = new LazyInstance();
}
return lazyInstance;
}
}
看上去没什么问题,相较于饿汉模式也只是实例化时间不同而已,的确,在单线程的情况下,饿汉式和懒汉式没有区别,但是在多线程下懒汉式会存在线程安全问题,通过getInstance方法提供的实例将不再是唯一实例,违背了单例模式的设计原则。下面是一个多线程测试类:
package com.huang.test;
import com.huang.Instance.LazyInstance;
public class Test {
public static void main(String[] args) {
//线程1
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(LazyInstance.getInstance());
}
}).start();
//线程2
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(LazyInstance.getInstance());
}
}).start();
//线程3
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(LazyInstance.getInstance());
}
}).start();
}
}
该测试类的main方法中有三个线程,分别会打印出它们所获取到的实例在堆(heap)中的内存地址,让我们来看一下结果:
很明显内存地址发生了变化,这是因为线程的执行速率非常快,快过了实例化的速率。比如线程1进入getInstance的if条件语句,条件成立开始实例化,此时线程2也进入if条件语句,由于线程1还未完成实例化,所以创建的对象依旧为null,所以线程2的if条件也成立,也会进行实例化,在getInstance方法中加入线程睡眠会将问题放大的更清晰。
package com.huang.Instance;
//懒汉模式
public class LazyInstance {
private LazyInstance() {
};
private static LazyInstance lazyInstance = null;
public static LazyInstance getInstance() {
if (lazyInstance == null) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
lazyInstance = new LazyInstance();
}
return lazyInstance;
}
}
输入的结果:
这个时候我们就需要通过线程锁+双重判断的方式来确保所提供的实例是全局唯一的,具体方式如下:
package com.huang.Instance;
//懒汉模式
public class LazyInstance {
private LazyInstance() {
};
private static LazyInstance lazyInstance = null;
public static LazyInstance getInstance() {
if (lazyInstance == null) {
//确保锁的对象应该是类对象,而不是实例,同一时刻只能有一个线程访问这个类。
synchronized (LazyInstance.class) {
if (lazyInstance == null) {
lazyInstance = new LazyInstance();
}
}
}
return lazyInstance;
}
}
现在就不会出现实例多次被初始化的情况了。
懒汉式存在线程安全问题,即在多线程的情况下可能会出现实例多次被创建的情况,因此不推荐使用懒汉式。如果使用了懒汉式,需要加同步锁来解决线程安全问题。
静态内部类也是线程安全的,具体实现如下:
package com.huang.Instance;
//静态内部类实现
public class InnerClassInstance {
private InnerClassInstance() {
};
private static class InnerClass{
private static InnerClassInstance innerClassInstance = new InnerClassInstance();
}
public InnerClassInstance getInstance(){
return InnerClass.innerClassInstance;
}
}
在getInstance方法中会先将静态内部类InnerClass加载入内存,在加载的同时完成实例化。
单例模式可以减少代码中new关键字的出现,减少程序对内存的占用。但是对于扩展显得非常的困难,对于一些需要增加属性或方法的类,只能通过修改类的代码来实现。