单例模式规定一个类仅有一个实例,并提供一个访问它的全局访问点。就是说单例类必须满足一下几点
单例模式作为众多设计模式中最简单的设计模式之一,原理非常简单,下面主要介绍单例模式的几种不同的实现方式。
public class HungarySingleton {
private HungarySingleton(){};
// 静态变量
private static final HungarySingleton instance = new HungarySingleton();
// 必须使用static,不加就需要调用方创建实例然后调用,而构造函数是私有的
public static HungarySingleton getInstance(){
return instance;
}
}
之所以成为“饿汉式”是因为在加载类的时候已经完成了静态变量的初始化。这个方式的优点就是不用考虑线程安全问题,缺点也很明显,如果一个对象初始化需要很长的时间而又没有被调用,就造成了资源浪费。
又称为延迟加载,在需要调用时候才会去创建对象。
public class LazySingleton {
private LazySingleton() {}
// 使用静态变量记录类的唯一实例
private static LazySingleton instance = null;
public static LazySingleton getInstance(){
if(instance == null){
instance = new LazySingleton();
}
return instance;
}
}
以上代码在单线程环境下没有任何问题,但是在多线程环境下就会出现创建多个实例的问题,解决方法非常简单,使用同步即可,可以使用同步方法或同步代码块,又因为同步比较消耗性能,在判断instance为null时再使用同步代码块,改进后的代码如下
public class LazySingleton {
private LazySingleton() {}
// 使用静态变量记录类的唯一实例
private static LazySingleton instance = null;
public static LazySingleton getInstance(){
if(instance == null){ // 1
// 同步代码块
synchronized (LazySingleton.class) { // 2
instance = new LazySingleton(); // 3
}
}
return instance;
}
}
同步是非常浪费性能的,因为一次只能有一个线程执行,其它线程等待,所有在同步之前先判断instance是否为空,然后在同步。分析一下以上代码:有两个线程A和线程B,都是首次执行,假设现在线程A和B都执行到“代码1处”,继续向下执行,假设线程A获得锁执行同步代码块,线程B在“代码2处”等待,线程A执行到“代码3处”创建对象退出同步代码块并释放锁,此时已经创建了对象。然后线程B获得锁,执行“代码3处”再次创建对象,这就导致出现了多个对象。解决方法也很简单,在同步代码块内部做一次非空判断即可,这也是所谓的“双重检测”。
public class DoubleSingleton {
private volatile static DoubleSingleton instance;
private DoubleSingleton(){}
public static DoubleSingleton getInstance(){
if(instance == null){
synchronized (DoubleSingleton.class) {
if (instance == null) {
instance = new DoubleSingleton();
}
}
}
return instance;
}
public static void main(String[] args) {
ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < 10000; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println(DoubleSingleton.getInstance().hashCode());
}
});
}
pool.shutdown();
}
}
需要注意instance变量需要使用volatile关键词修饰,来保证在多线程下能够正确的处理instance变量,必须在jdk5之后才可以使用volatile关键字。假如没有volatile关键字修饰的话,会出现一个问题:在Java编译器中,JVM会对代码进行优化,也就是重排序,也就是说DoubleSingleton类的初始化和instance变量赋值顺序 不可预料,假如一个线程在没有同步化的条件下读取instance,并调用该对象的方法,可能对象的初始化还没有完成,从而造成程序错误。
一直没有模拟出来没有volatile关键字修饰程序会出现什么错误,希望看到的小伙伴不吝赐教!!
可以看到使用饿汉式实现单例模式优点就是不用考虑多线程,缺点是比较占用内存。懒汉式方式比较繁琐,volatile也会降低程序的性能。那么有没有一种结合两者优点的方式呢?可以使用静态内部类来完成
public class InnerSingleton {
private InnerSingleton(){}
private static class Singleton{
private static InnerSingleton single = new InnerSingleton();
}
public static InnerSingleton getInstance(){
return Singleton.single;
}
}
只有在调用getInstance()方法时才会创建对象,既保证了线程安全,又保证了延迟加载。
《Effective Java》第三条:Java5之后,可以使用枚举实现单例,可以防止多次序列化以及反射攻击,同时又非常简洁,可以说是单例模式最佳的实现方式。
public enum EnumSingleton {
INSTANCE;
public void print(){
System.out.println("I am EnumSingleton");
}
}
下面对单例模式进行改造,使用缓存的思想模拟实现“多例模式”,既程序中一个类的实例对象可以存在有限多个(比如说3个),和单例模式唯一的区别就是程序中可以有多个实例对象
public class SomeSingleton {
private SomeSingleton() {}
// 存储创建的实例(模拟缓存)
private static Map map = new HashMap<>(3);
// 计数器从1开始
private volatile static int num = 1;
// 控制最多有三个实例
private static final int MAX_NUM = 3;
// 锁
final static ReentrantLock lock = new ReentrantLock();
public static SomeSingleton getInstance() {
lock.lock();
try {
// 从缓存中取出实例
SomeSingleton instance = map.get(num);
if (instance == null) {
instance = new SomeSingleton();
// 计数器作为map的key
map.put(num, instance);
}
// 计数器+1
num++;
if (num > MAX_NUM) {
// 重置计数器
num = 1;
}
return instance;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
final Set set = new HashSet<>();
ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < 10000; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
// System.out.println(getInstance().hashCode());
set.add(getInstance().hashCode());
}
});
}
System.out.println(set.size());
pool.shutdown();
}
}
不管有多少个线程调用,都只会创建3个实例对象,完成了控制有限过个实例对象。
单例模式的实质就是控制实例对象在程序中的数量有且仅有一个,并且只能自己创建。实现单例可以使用“静态内部类”和“枚举”方式,不过“静态内部类”需要注意反射攻击以及序列化破坏。
JDK中的单例模式
java.lang.Runtime
java.text.NumberFormat
Spring中
在Spring中默认的bean都是单例的