单例模式是像我们这些小白使用的设计模式之一,确保单例对象的类必须保证仅有一个实例,整个系统只有一个全局对象,避免产生过多的对象造成资源浪费。
使用场景:比如访问数据库时候,或者app更新的Download实例。
关键点:
1,构造函数不对外开发,一般为Private(私有化使得不能通过new的形式构造单例类对象)
2 ,通过一个静态方法或者枚举返回单例类对象
3,确保单例类对象有且只有一个(确保多线程下也只有一个对象)
4,确保单例类对象在反序列化不会重新构建对象
单例获取方式:
public class SingletonTest {
//自己内部定义一个静态对象,供自己使用
private static final SingletonTest sSingletonTest = new SingletonTest();
//构造函数私有
private SingletonTest() {
}
//公有的静态方法,向外暴露获取单例对象的接口
public static SingletonTest getInstance(){
return sSingletonTest;
}
}
懒汉模式(需要的时候才去new对象,懒)
使用synchronized修饰,获取单例是一个同步方法,会检查到其他线程有没有使用。保证在多线程下单例对象唯一。但是,每次获取单例模式都会进行同步,造成资源浪费,所以一般不建议使用。
public class SingletonTest {
private static SingletonTest sSingletonTest;
//构造函数私有
private SingletonTest() {
}
public static synchronized SingletonTest getInstance(){
if(sSingletonTest == null){
sSingletonTest = new SingletonTest();
}
return sSingletonTest;
}
}
DCL(double check lock)双层检锁模式
DCL方式优点是:能在需要的时候初始化单例,又能保证线程安全,而且单例对象初始化后再调用getInstance不进行同步锁,避免资源浪费。
public class SingletonTest {
private static SingletonTest sSingletonTest = null;
private SingletonTest(){
};
public static SingletonTest getInstance(){
if(sSingletonTest == null){
//同步代码块的部分,如果已经初始化了,就不会造成同步浪费
synchronized (SingletonTest.class){
if(sSingletonTest == null){
sSingletonTest = new SingletonTest();
}
}
}
return sSingletonTest;
};
}
静态内部类只有在被引用的时候才会初始化,静态变量 sSingletonTest 被创建出来。继承了懒加载的延时加载特性。而且静态变量又能保证唯一性,也是线程安全。
public class SingletonTest {
//静态内部类,
private static class HelperSinger{
private static SingletonTest sSingletonTest = new SingletonTest();
}
private SingletonTest(){}
public static SingletonTest getInstace(){
return HelperSinger.sSingletonTest;
}
}
枚举单例
枚举类型:枚举类型是Java 5中新增特性的一部分,它是一种特殊的数据类型,之所以特殊是因为它既是一种类(class)类型却又比类类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。
推荐使用枚举,首先枚举跟java中其他类是一样的,不仅有字段还可以有方法,而且枚举还能保证枚举实例的创建是线程安全的。而且不会被反序列化和反射构造器创建新的对象,推荐使用。
public enum SingletonTest {
INSTANCE;
public void doYourNeed(){
//你的逻辑
}
}
防止反序列化
上面几种情况除了枚举,都是可以通过序列化将一个单例的实例对象写到磁盘,然后在读取回来,从而有效的获取一个实例。先看未处理参数的对象地址:
如果你的单例类不需要序列化,可以不管这个过程
单例类,实现Serializable 接口,才能序列化。
public class SingletonTest implements Serializable {
//静态内部类,
private static class HelperSinger{
private static SingletonTest sSingletonTest = new SingletonTest();
}
private SingletonTest(){}
public static SingletonTest getInstace(){
return HelperSinger.sSingletonTest;
}
}
测试类,完成对象序列化存储和读取,比较对象地址。
public class SerializeTest {
public static void main(String[] args) throws Exception{
SingletonTest s1 = SingletonTest.getInstace();
SingletonTest s2 = SingletonTest.getInstace();
System.out.println("s1对象的地址="+s1); // sc1,sc2是同一个对象
System.out.println("s2对象的地址="+s1);
//通过序列化方法构造的对象(首先序列化对象,实现Serializable接口)
//把s1对象写入本地
FileOutputStream fos = new FileOutputStream("test.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s1);
oos.close();
fos.close();
//将保存对象从文件中读出来赋值给s3
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.out"));
//通过序列化得到的对象
SingletonTest s3 = (SingletonTest)ois.readObject();
ois.close();
System.out.println("s3对象的地址="+s3);
}
}
**对比发现:s1和s2是通过单例模式getInstace()创建处理的,对象地址为同一个;
而将s1序列化后再读取出来的对象s3,对象地址是不一样的,所以不能保证单例,是两个实例**
解决方案:
反序列化操作提供了一个很特别的钩子函数hook函数,类中具有一个私有的、被实例化得方法readResolve(),这个方法可以让我们控制对象的反序列化,所以可以添加该方法,返回当前的单例实例。
修改后如下:
public class SingletonTest implements Serializable {
//静态内部类,
private static class HelperSinger{
private static SingletonTest sSingletonTest = new SingletonTest();
}
private SingletonTest(){}
public static SingletonTest getInstace(){
return HelperSinger.sSingletonTest;
}
//防止反序列化产生多个对象
private Object readResolve() throws ObjectStreamException{
return SingletonTest.getInstace();
}
}
再次运行SerializeTest ,打印台输出如下:
成功的防止了反序列化多个对象。
通过反射产生多个实例
反射可以获取到对象的实例应该是非常常见的一种,这里的单例模式一样会被反射构造器拿到实例。代码如下:
单例类(静态内部法):
public class SingletonTest {
//静态内部类,
private static class HelperSinger{
private static SingletonTest sSingletonTest = new SingletonTest();
}
//私有构造函数
private SingletonTest(){}
public static SingletonTest getInstace(){
return HelperSinger.sSingletonTest;
}
}
测试类,通过反射获取实例
public class ReflectTest {
public static void main(String[] args) throws Exception{
SingletonTest s1 = SingletonTest.getInstace();
SingletonTest s2 = SingletonTest.getInstace();
System.out.println("s1对象的地址="+s1); // sc1,sc2是同一个对象
System.out.println("s2对象的地址="+s1);
//通过反射构造器
//取得Class对象
Class clazz = (Class) Class.forName("com.hu.test.test.SingletonTest");
//获取到无参构造器
Constructor constructor = clazz.getDeclaredConstructor(null);
//跳过权限检查(私有无视掉)
constructor.setAccessible(true);
//获取实例
SingletonTest s5 = constructor.newInstance();
SingletonTest s6 = constructor.newInstance();
//s5和s6不是同一个对象
System.out.println("s5对象的地址="+s5);
System.out.println("s6对象的地址="+s6);
}
}
运行测试类ReflectTest ,看控制台输出对象地址:
s5和s6和s1分别对应的不同实例,通过反射获得多个对象。
解决办法,在构造器里面抛错。如果想创建第二个实例就报错。修改如下:
public class SingletonTest {
//静态内部类,
private static class HelperSinger{
private static SingletonTest sSingletonTest = new SingletonTest();
}
//私有构造函数
private SingletonTest(){
// 防止反射获取多个对象的漏洞
if (null != HelperSinger.sSingletonTest) {
throw new RuntimeException("单例被反射");
}
}
public static SingletonTest getInstace(){
return HelperSinger.sSingletonTest;
}
}
再次运行测试类ReflectTest ,看控制台输出对象地址:
把两者结合下,既能防止反序列化产生多个对象,又能不被反射构造器创建多个对象。而静态内部类又具有懒加载机制和线程安全,跟枚举单例一样是单例模式很好的选择。合并防止反序列化和反射代码如下:
public class SingletonTest implements Serializable {
//静态内部类,
private static class HelperSinger{
private static SingletonTest sSingletonTest = new SingletonTest();
}
//私有构造函数
private SingletonTest(){
// 防止反射获取多个对象的漏洞
if (null != HelperSinger.sSingletonTest) {
throw new RuntimeException("单例被反射");
}
}
public static SingletonTest getInstace(){
return HelperSinger.sSingletonTest;
}
//防止反序列化产生多个对象
private Object readResolve() throws ObjectStreamException {
return SingletonTest.getInstace();
}
}
总上所述,为了防止反序列化和反射构造器等问题,可以使用枚举单例模式,也可以使用改造后静态内部类单例方式。当然还是枚举类比较简洁,几句代码。