定义
单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点(静态方法)。单例模式属于创建型模式。常见的ServletContext、ServletContextConfig、ApplicationContext以及数据库的连接池等都是以单例形式存在的。
常见的单例模式
- 饿汉式单例
饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。绝对线程安全,在线程还没出现以前就是实例化了,不会存在访问安全问题。
优点:没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好。
缺点:类加载的时候就初始化,不管用与不用都占着空间,浪费了内存空间。
代码示例如下:
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){}
//全局访问点
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
还可以利用静态代码块来实现:
public class HungryStaticSingleton {
private static final HungryStaticSingleton hungrySingleton;
static {
hungrySingleton = new HungryStaticSingleton();
}
private HungryStaticSingleton(){}
public static HungryStaticSingleton getInstance(){
return hungrySingleton;
}
}
- 懒汉式单例
懒汉式单例是在被外部类调用时才创建实例。
示例如下:
public class LazySimpleSingleton {
private LazySimpleSingleton(){}
private static LazySimpleSingleton lazy = null;
public static LazySimpleSingleton getInstance(){
if(lazy == null){
lazy = new LazySimpleSingleton();
}
return lazy;
}
}
不过,上面的单例存在线程安全隐患,为了保证线程安全,我们可以采用双重检查锁的机制:
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazy = null;
private LazyDoubleCheckSingleton(){}
public static LazyDoubleCheckSingleton getInstance(){
if(lazy == null){
synchronized (LazyDoubleCheckSingleton.class){
if(lazy == null){
lazy = new LazyDoubleCheckSingleton();
}
}
}
return lazy;
}
}
但是,用到synchronized关键字,总归是要上锁,对程序性能还是存在一定影响的。我们可以从类初始化角度来考虑,采用静态内部类的方式:
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton(){}
public static final LazyInnerClassSingleton getInstance(){
return LazyHolder.LAZY;
}
private static class LazyHolder{
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
静态内部类实现的单例模式是线程安全的,也不会在类加载时就创建实例,而是在调用getInstance()方法时才进行创建,达到了懒加载的效果。看起来好像是很完美的方法了,但是,我们依然可以通过反射或序列化来破坏单例
,代码如下:
public class ReflectTest {
public static void main(String[] args) {
try {
Class<?> clazz = LazyInnerClassSingleton.class;
Constructor c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);
Object instance1 = c.newInstance();
Object instance2 = c.newInstance();
System.out.println(instance1 == instance2);
} catch (Exception e){
e.printStackTrace();
}
}
}
运行结果如下:
通过结果看,这两个实例不是同一个,这就违背了单例模式的原则了。
那么,接下来就来了解下枚举式单例。
- 枚举式单例
枚举式单例模式是注册式单例的一种,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。枚举式可以有效避免反射和序列化的破坏,同时也是《Effective Java》书中推荐的一种单例实现写法,利用枚举的特性,不仅实现起来比较简单,还可以利用JVM来帮我们保证线程安全和单一实例的问题。
代码实现如下:
public enum EnumSingleton {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance(){return INSTANCE;}
}
验证如下:
通过反射创建:
public class EnumSingletonTest {
public static void main(String[] args) {
try {
Class clazz = EnumSingleton.class;
Constructor c = clazz.getDeclaredConstructor(String.class,int.class);
c.setAccessible(true);
Object o = c.newInstance();
} catch (Exception e){
e.printStackTrace();
}
}
}
结果如下:
很显然, Cannot reflectively create enum objects,jdk不允许通过反射来创建枚举类型。一旦使用反射,便会抛出异常。
通过序列化创建:
public class EnumSingletonTest {
public static void main(String[] args) {
try {
EnumSingleton instance1 = null;
EnumSingleton instance2 = EnumSingleton.getInstance();
instance2.setData(new Object());
FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
ObjectOutputStream oos = new
ObjectOutputStream(fos); oos.writeObject(instance2);
oos.flush();
oos.close();
FileInputStream fis = new
FileInputStream("EnumSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
instance1 = (EnumSingleton) ois.readObject();
ois.close();
System.out.println(instance1.getData());
System.out.println(instance2.getData());
System.out.println(instance1.getData() == instance2.getData());
}catch (Exception e){
e.printStackTrace();
}
}
}
结果如下:
但是枚举式单例也存在一些问题。因为它在类加载之时就将所有的对象初始化放在类内存中,这其实和饿汉式并无差异,不适合大量创建单例对象的场景。那么,接下来看注册式单例模式的另一种写法,即容器式单例模式。
- 容器式单例
代码如下:
public class ContainerSingleton {
private ContainerSingleton(){}
private static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();
public static Object getInstance(String className){
Object instance = null;
if(!ioc.containsKey(className)){
try {
instance = Class.forName(className).newInstance();
ioc.put(className, instance);
} catch (Exception e){
e.printStackTrace();
}
return instance;
} else {
return ioc.get(className);
}
}
}
容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的。
- ThreadLocal线程单例
ThreadLocal 不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,天生的线程安全。下面我们来看代码:
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> threadLocaLInstance =
new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
private ThreadLocalSingleton(){}
public static ThreadLocalSingleton getInstance(){
return threadLocaLInstance.get();
}
}
测试代码:
public class ThreadLocalSingletonTest {
public static void main(String[] args) {
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
Thread t1 = new Thread(new ExectorThread());
Thread t2 = new Thread(new ExectorThread());
t1.start();
t2.start();
System.out.println("End");
}
}
结果我们会发现,在主线程main中无论调用多少次,获取到的实例都是同一个,在两个子线程中分别获取到了不同的实例。那么ThreadLocal是如果实现这样的效果的呢?我们知道上面的单例模式为了达到线程安全的目的,给方法上锁,以时间换空间。ThreadLocal将所有的对象全部放在ThreadLocalMap中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程间隔离的。