5分钟搞懂Java书写正确的单例模式
定义
确保一个类只有一个实例,自行实例化并向系统提供这个实例,属于创建性模式。
分类
饿单例模式(类加载时实例化一个对象给自己的引用)
懒单例模式(调用取得实例的方法如getInstance时才会实例化对象)(java中饿单例模式性能优于懒单例模式,c++中一般使用懒单例模式)
登记式单例模式(用容器管理实例)
单例模式优点
在内存中只有一个对象,节省内存空间。
避免频繁的创建销毁对象,可以提高性能。
避免对共享资源的多重占用。
适用场景
需要频繁实例化然后销毁的对象。
创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
有状态的工具类对象。
频繁访问数据库或文件的对象。
代码实现
0.有问题的版本(请理解为什么会有问题!)
//只能在单线程上使用,在多线程不能使用该方式
public class Singleton{
private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
1.懒汉式完美版
解决多线程访问时引发的问题
- synchronized 效率低下的问题
解决在jit环境下指令重排引起的问题
- 非原子操作会涉及到指令重排
public class Singleton1 {
// 使用volatile关键字禁止指令重排
private static volatile Singleton1 instance = null;
// 构造函数私有化
private Singleton1() {
}
//public static synchronized Singleton1 getInstance() {
// 若在方法上加了锁,这样虽然解决了问题,但是因为用到了synchronized,会导致很大的性能开销,并且加锁其实只需要在第一次初始化的时候用到,之后的调用都没必要再进行加锁。
public static Singleton1 getInstance() {
if (instance == null) { //第一次if 因为仅创建一次 为了避免开销过多
synchronized (Singleton1.class) {
if (instance == null) {//第二次加锁的if 防止实例创建多次
instance = new Singleton1(); // 这里会有指令重排
// 可大致分为3步:1。分配空间 2。初始化对象 3。instance指向该对象
// 若2。3重排多线程情况下会出现第二个线程判断instance不为空
// 返回了未初始化的实例导致发生错误
}
}
}
return instance;
}
}
为什么要用volatile?
语句1private static Singleton1 instance = null;
语句2instance=new Singleton();
在语句2中并不是一个原子操作,在JVM中其实是3个操作:
1.给instance分配空间
2.调用 Singleton 的构造函数来初始化
3.将instance对象指向分配的内存空间(instance指向分配的内存空间后就不为null了);
在JVM中的及时编译存在指令重排序的优化,也就是说不能保证1,2,3执行的顺序,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是 1-3-2,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
通过添加volatile就可以解决这种报错,因为volatile可以保证1、2、3的执行顺序,没执行玩1、2就肯定不会执行3,也就是没有执行完1、2instance一直为空
2.饿汉式完美版
解决了上述代码过于复杂的问题。
缺点:
- 可能由于初始化的太早,造成资源的浪费
- 如果初始化本身依赖于一些其他数据,那么也就很难保证其他数据会在它初始化之前准备好。
public class SingletonStudy {
//这里加final是为了防止内部将这个属性覆盖掉
private static final SingletonStudy singletonStudy = new SingletonStudy();
private SingletonStudy() {
}
//这里加final是为了防止子类重写父类
public static final SingletonStudy getInstance() {
return singletonStudy;
}
}
3.懒汉饿汉结合版本
/*
*
* Effective Java 中的代码
* 懒汉式和饿汉式可以完美结合吗?
* 从外面看是懒汉式。里面确实饿汉。可以延迟加载资源,解决饿汉资源占用的问题。
*
*
* 魔法: 通过ClassLoader来保证同步,使INSTANCE是一个真·单例。
* 同时,由于SingletonHolder是一个内部类,
* 只在外部类的Singleton的getInstance()中被使用,
* 所以它被加载的时机也就是在getInstance()方法第一次被调用的时候。
*
* */
public class SingletonStudy {
private static class SingletonHolder {
private static final SingletonStudy INSTANCE = new SingletonStudy();
}
private SingletonStudy() {
}
public static final SingletonStudy getInstance() {
return SingletonHolder.INSTANCE;
}
}
4.枚举版
由于创建枚举对象的时候是线程安全的上述的所有因为并发引发问题都不复存在,所以不需要添加额外的代码
需要jdk1.5以上
public enum SingleInstance {
INSTANCE;
public void function(){
}
}
5.登记式单例模式(支持继承)
以上1234均不支持继承
public class Singleton{
//Spring最底层的这个容器就是一个map,说白了,IOC容器就是一个map
private static Map<String, Singleton7> map = new ConcurrentHashMap<String, Singleton7>();
//每个class对应一个map的key,也就是唯一的id
static {
Singleton7 singleton7 = new Singleton7();
map.put(singleton7.getClass().getName(), singleton7);
}
//保护默认构造函数
protected Singleton7() {
}
//静态工厂方法,返回此类唯一的实例
public static Singleton7 getInstance(String name) {
if (name == null) {
name = Singleton7.class.getName();
}
if (map.get(name) == null) {
try {
map.put(name, (Singleton7) Class.forName(name).newInstance());
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
return map.get(name);
}
}