一、源码应用
事实上,我们在JDK或者其他的通用框架中很少能看到标准的单例设计模式,这也就意味着他确实很经典,但严格的单例设计确实有它的问题和局限性,我们先看看在源码中的一些案例
1、jdk中的单例
jdk中有一个类的实现是一个标准单例模式(饿汉式),即Runtime类,该类封装了运行时的环境。每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。 一般不能实例化一个Runtime对象,应用程序也不能创建自己的 Runtime类实例,可以通过 getRuntime 方法获取当前Runtime运行时对象的引用。
public class Runtime {
private static final Runtime currentRuntime = new Runtime ( ) ;
private static Version version;
public static Runtime getRuntime ( ) {
return currentRuntime;
}
private Runtime ( ) { }
. . .
}
2、MyBatis中的单例
Mybaits中的org.apache.ibatis.io.VFS使用到了单例模式。VFS就是Virtual FileSystem的意思,mybatis通过VFS来查找指定路径下的资源。查看VFS以及它的实现类,不难发现,VFS的角色就是对更“底层”的查找指定资源的方法的封装,将复杂的“底层”操作封装到易于使用的高层模块中,方便使用者使用。
public class public abstract class VFS {
private static class VFSHolder {
static final VFS INSTANCE = createVFS ( ) ;
@SuppressWarnings ( "unchecked" )
static VFS createVFS ( ) {
return vfs;
}
}
public static VFS getInstance ( ) {
return VFSHolder . INSTANCE ;
}
}
二、安全问题
1、反射入侵
我们可以通过反射获取私有构造器进行构造,如下代码:
@Slf4j
public class TestReflectSingleton {
private static volatile TestReflectSingleton instance;
private TestReflectSingleton ( ) { }
public static TestReflectSingleton getInstance ( ) {
if ( instance == null ) {
synchronized ( TestReflectSingleton . class ) {
if ( instance == null ) {
instance = new TestReflectSingleton ( ) ;
}
}
}
return instance;
}
public static void main ( String [ ] args) {
Class < TestReflectSingleton > cls = TestReflectSingleton . class ;
try {
Constructor < TestReflectSingleton > constructor = cls. getDeclaredConstructor ( ) ;
constructor. setAccessible ( true ) ;
TestReflectSingleton instance1 = TestReflectSingleton . getInstance ( ) ;
TestReflectSingleton instance2 = constructor. newInstance ( ) ;
boolean flag = instance2 == instance1;
log. info ( "flag -> {}" , flag) ;
log. info ( "flag -> {}" , flag) ;
} catch ( NoSuchMethodException e) {
throw new RuntimeException ( e) ;
} catch ( InvocationTargetException e) {
throw new RuntimeException ( e) ;
} catch ( InstantiationException e) {
throw new RuntimeException ( e) ;
} catch ( IllegalAccessException e) {
throw new RuntimeException ( e) ;
}
}
}
这样输出的结果是false,证明对象不是同一个对象,我们可以在构造方法中再加一个判断对象是否为空的条件即可。代码如下:
@Slf4j
public class TestReflectSingleton {
private static volatile TestReflectSingleton instance;
private TestReflectSingleton ( ) {
if ( instance != null ) {
throw new RuntimeException ( "实例:【" + this . getClass ( ) . getName ( ) + "】已经存在,该实例只被实例化一次!" ) ;
}
}
public static TestReflectSingleton getInstance ( ) {
if ( instance == null ) {
synchronized ( TestReflectSingleton . class ) {
if ( instance == null ) {
instance = new TestReflectSingleton ( ) ;
}
}
}
return instance;
}
public static void main ( String [ ] args) {
Class < TestReflectSingleton > cls = TestReflectSingleton . class ;
try {
Constructor < TestReflectSingleton > constructor = cls. getDeclaredConstructor ( ) ;
constructor. setAccessible ( true ) ;
TestReflectSingleton instance1 = TestReflectSingleton . getInstance ( ) ;
TestReflectSingleton instance2 = constructor. newInstance ( ) ;
boolean flag = instance2 == instance1;
log. info ( "flag -> {}" , flag) ;
log. info ( "flag -> {}" , flag) ;
} catch ( NoSuchMethodException e) {
throw new RuntimeException ( e) ;
} catch ( InvocationTargetException e) {
throw new RuntimeException ( e) ;
} catch ( InstantiationException e) {
throw new RuntimeException ( e) ;
} catch ( IllegalAccessException e) {
throw new RuntimeException ( e) ;
}
}
}
直接抛出异常,这样就可以防止反射入侵啦
2、序列化与反序列化问题
@Slf4j
public class TestSerializeSingleton implements Serializable {
private static TestSerializeSingleton instance;
private TestSerializeSingleton ( ) { }
public static TestSerializeSingleton getInstance ( ) {
if ( instance == null ) {
instance = new TestSerializeSingleton ( ) ;
}
return instance;
}
public static void main ( String [ ] args) {
String url = "DesignPatterns/src/main/resources/singleton.txt" ;
TestSerializeSingleton singleton = TestSerializeSingleton . getInstance ( ) ;
FileOutputStream fos = null ;
ObjectOutputStream oos = null ;
FileInputStream fis = null ;
ObjectInputStream ois = null ;
try {
fos = new FileOutputStream ( url) ;
oos = new ObjectOutputStream ( fos) ;
oos. writeObject ( singleton) ;
fis = new FileInputStream ( url) ;
ois = new ObjectInputStream ( fis) ;
Object o = ois. readObject ( ) ;
log. info ( "他们是同一个实例吗?{}" , o == singleton) ;
} catch ( FileNotFoundException e) {
throw new RuntimeException ( e) ;
} catch ( IOException e) {
throw new RuntimeException ( e) ;
} catch ( ClassNotFoundException e) {
throw new RuntimeException ( e) ;
} finally {
try {
if ( fos != null ) {
fos. close ( ) ;
}
if ( oos != null ) {
oos. close ( ) ;
}
if ( fis != null ) {
fis. close ( ) ;
}
if ( ois != null ) {
ois. close ( ) ;
}
} catch ( IOException e) {
throw new RuntimeException ( e) ;
}
}
}
}
输出结果如下:
readResolve()方法可以用于替换从流中读取的对象,在进行反序列化时,会尝试执行readResolve方法,并将返回值作为反序列化的结果,而不会克隆一个新的实例,保证jvm中仅仅有一个实例存在,所以在单例中添加readResolve方法:
public Object readResolve ( ) {
return instance;
}
再次运行代码输出结果如下:
三、单例存在的一些问题
1、它不支持面向对象编程
我们都知道,面向对象的三大特性是封装、继承、多态 。单例将构造私有化,直接导致的结果就是,他无法成为其他类的父类,这就相当于直接放弃了继承和多态的特性,也就相当于损失了可以应对未来需求变化的扩展性,以后一旦有扩展需求,我们不得不新建一个十分【雷同】的单例。
2、极难的横向扩展
单例类只能有一个对象实例。如果未来某一天,一个实例已经无法满足我们的需求,我们需要创建一个,或者更多个实例时,就必须对源代码进行修改,无法友好扩展。
在系统设计初期,我们觉得系统中只应该有一个数据库连接池,这样能方便我们控制对数据库连接资源的消耗。所以,我们把数据库连接池类设计成了单例类。但之后我们发现,系统中有些 SQL 语句运行得非常慢。这些 SQL 语句在执行的时候,长时间占用数据库连接资源,导致其他 SQL 请求无法响应。为了解决这个问题,我们希望将慢 SQL 与其他 SQL 隔离开来执行。为了实现这样的目的,我们可以在系统中创建两个数据库连接池,慢 SQL 独享一个数据库连接池,其他 SQL 独享另外一个数据库连接池,这样就能避免慢 SQL 影响到其他 SQL 的执行。
如果我们将数据库连接池设计成单例类,显然就无法适应这样的需求变更,也就是说,单例类在某些情况下会影响代码的扩展性、灵活性。所以,数据库连接池、线程池这类的资源池,最好还是不要设计成单例类。实际上,一些开源的数据库连接池、线程池也确实没有设计成单例类。