文章目录

前言

最近在做一个简单的项目,需要调用大量的无状态函数,首先就想到了之前用过的单例模式设计API类。这是在去年实习的时候发现他们后台用PHP编写的,包括连接数据库之类的操作都用的是同一个类来操作,非常方便,仔细看看源码发现这个类就是一个单例模式设计的。不过最近翻看了一些资料,发现JAVA的单例模式并不简单:PHP并没有线程安全的问题,一个请求在结束后生命周期就结束了,PHP设计单例模式仅仅是为了如果在同一个页面多次处理,可以不用重复创建对象而已;JAVA则不同,需要考虑两个线程同时访问的情况。

简单介绍下PHP的单例模式怎么设计,非常简单,保证三点就可以:

(1)建立一个私有的静态成员变量,保存实例;

(2)构造函数和克隆函数都不允许使用;

(3)做一个public的获取实例的函数,自行实例化或者返回实例化后的对象。

class bwbAPI{
    //存储实例的静态成员变量
    private static $s_instance;

    //构造函数,禁止外部实例化
    private function __construct(){
    }

    //重写clone防止用户进行clone
    public function __clone(){
        //当用户clone操作时产生一个错误信息
        trigger_error("Can't clone object",E_USER_ERROR);
    }

    public static function getInstance() {
        if(self::$s_instance instanceof self) {
            return self::$s_instance;
        }
        self::$s_instance = new self();
        return self::$s_instance;
    }
}

在调用的时候:

require ("xxxxxxx/bwbAPI.php");
$bwb = bwbAPI::getInstance();
$bwb->query("xxxxxxxxx");

超级方便,当然设计了这种东西,一定要保护好,比如nginx设置禁止直接访问这个文件所在目录。

回到主题,还记得去年被实习老大内推去面大疆创新,因为很多原因啥都没看就上(海文哥对不起浪费了机会QwQ),问了句“你知道哪些JAVA的设计模式”,我的回答是“MVC”………平时多准备一点,当机会到的时候才能抓住,那么,来分析和实践一下JAVA单例模式吧。

一、使用单例模式还是使用静态方法类

很自然能想到这个问题,既然只需要让它存在一个对象,那使用静态方法类不就好了吗?这里有几个重点问题:

1、静态类和静态方法类的区别

静态类,指的是用static修饰的类,通常定义的类是不能使用static的,编译都通不过,不信的话实践下:

静态类实际上应该称为“静态内部类”,是写在类里面的类,通常只是为了项目打包方便。放在内部才能使用static关键字修饰:

而静态方法类是我自己起的名字(请JAVA大佬指教它的本名),指的是一个类,它的所有成员变量都是静态成员变量,所有方法都是静态方法。比如java.lang.Math类:

所以我们平时才可以直接调用Math.ceil(3.2)、Math.max(22,33)等等方法。

2、单例模式和静态方法类的区别

(1)代码结构上

单例模式可以有非静态方法和成员的,而且只要获得了实例就可以去调用;

静态方法类通常来说全是静态方法,如果有非静态方法,是不能直接调用的。

(2)编程思想上

单例模式是普通的类,只不过它是有一个实例而已,符合JAVA面向对象的思想;

静态方法类通常又称为工具类,它更像是面向过程的一个函数集。

(3)JAVA特性上

单例模式符合所有面向对象的特性,可以去继承类、可以实现接口、可以被继承、方法可以被重写、可以用于多态(不同实现);

而静态方法类不能。

(4)生命周期上

单例模式可以延迟初始化,并且一直到运行结束才会被回收;

静态方法类在第一次使用时就会被加载,执行完静态方法后就会被回收,如果频繁调用会导致频繁地初始化和释放。

(5)实例化上

单例模式需要进行实例化(通过静态方法中的new);

静态方法类不需要实例化,可以直接调用。

(6)内存占用上

单例模式调用哪个方法,就载入哪个方法,但是它需要长时间地维护一个对象;

静态方法类需要把所有静态方法都载入内存,不管你用不用。

(7)运行速度上

《单例模式和静态类的区别》作者称从日志打印看,静态方法比实例方法更快。

《java中的单例模式与静态类》作者称静态方法比实例方法更快,因为静态的绑定是在编译期就进行的。

(8)线程与共享

单例模式的多线程控制很方便,适合维护或者共享一些配置状态信息;

静态方法类的多线程控制则非常糟糕。

3、单例模式和静态方法类的选择

《JAVA Static方法与单例模式的理解》作者称,他最近用sonar测评代码质量的时候,发现工程中一些util类,以前写的static方法都提示最好用单例的方式进行改正。所以,看起来似乎单例模式更加推荐,这里列出几个考虑因素:

考虑因素推荐选择 
涉及文件读写(考虑并发)单例模式
涉及数据库(考虑状态)
单例模式
全是工具函数静态方法类
萌新小白静态方法类 
不知道该如何选择单例模式

所以,尽管我的任务里面不需要对状态信息进行维护,也几乎全是工具函数,但是后期很可能会涉及数据库的处理,并且会对方法有频繁的调用,我不希望产生大量的开销,因此使用单例模式来设计。

二、JAVA单例模式的实现方法

1、饿汉模式

1.1 经典饿汉模式

public class Singleton{
    private static Singleton instance = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
        return instance;
    }
}

【核心实现方式】在类加载时就创建实例,利用classloder机制避免了多线程的同步问题。

【优点】简便易懂,实现了单例

【缺点】无法做到延迟加载(lazy loading)

【推荐】YES √

【相关信息】

饿汉的含义就是拿起饭碗就开吃,刚加载类就把实例创建了。这种方式非常常用,比如java.lang.Runtime类就是这样实现的单例:

有一个重要问题,那就是为什么可以利用classloader机制避免多线程同步问题呢?这句话在好多博客里都提到过,可是没有人解释为什么,这里我来简单地分析一下。我们知道,要想使用一个类,首先得把class文件载入JVM。类的生命周期一共分为加载、验证、准备、解析、初始化、使用、卸载。JDK1.7提出,有且只有5种情况必须对类进行初始化,其中一种的一部分描述为“调用一个类的静态方法的时候需要进行初始化”,也就是说,当我们执行Singleton.getInstance()的时候,这个类就会被初始化。而初始化阶段就是执行类构造器<clinit>()方法的过程,<clinit>()方法会自动收集所有静态变量的赋值动作和静态语句块的操作一起执行,而执行的顺序就是语句在代码中出现的顺序(题外话,如果没有写静态相关的东西,是可以不产生<clinit>方法的)。重点来了,虚拟机会保证类的<clinit>方法在多线程环境中被正确加锁、同步。也就是说,如果我们在代码里产生多个线程调用Singleton.getInstance(),那么一个线程执行clinit,其他线程都会被阻塞。这就是classloader机制避免多线程同步问题的原因。有关详细的机制可以参考《Java虚拟机类加载机制》,有关更多饿汉和类加载的有趣面试题可参考【面试题】java类加载机制探索

1.2 饿汉模式变形1

public class Singleton {
    private static Singleton instance = null;
    static {
        Singleton.instance = new Singleton();
    }
    private Singleton (){}
    public static Singleton getInstance() {
        return Singleton.instance;
    }
}

同样的思路,只不过用静态语句块去实现了而已。很常用,比如Hibernate框架自动生成的session工厂就是典型的静态语句块单例:

public class Main {
    private static final SessionFactory ourSessionFactory;

    static {
        try {
            ourSessionFactory = new Configuration().
                    configure("hibernate.cfg.xml").
                    buildSessionFactory();
        } catch (Throwable ex) {
            throw new ExceptionInInitializerError(ex);
        }
    }

    public static Session getSession() throws HibernateException {
        return ourSessionFactory.openSession();
    }
    .....
}

1.饿汉模式变形2——静态内部类

public class Singleton {
    private static class SingletonHolder {
        private static Singleton INSTANCE = new Singleton();
    }
    private Singleton (){}
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

【核心实现方式】创建一个静态内部类(静态类),利用该静态类加载时的classloder机制避免了多线程的同步问题。

【优点】实现了单例,支持了延迟加载

【缺点】无法做到延迟加载(lazy loading)

【推荐】YES √

【相关信息】

和经典饿汉模式的区别在于,这里它在初始化的时候并不会创建实例,而在调用Singleton.getInstance()的时候触发了SingletonHolder.INSTANCE,从而使得SingletonHolder类开始加载,那么利用classloader机制就避免了多线程重复new的行为。

2、懒汉模式

public class Singleton{
    private static Singleton instance = null;
    private Singleton(){}
    public static Singleton getInstance(){
        if(null == instance){
            instance = new Singleton();
        }
        return instance;
    }
}

【核心实现方式】用静态变量保存实例,如果已经存在实例则直接返回它。

【优点】实现了延迟加载

【缺点】没有考虑多线程

【推荐】NO ×

【相关信息】

哇,这不就是之前PHP的那种方法吗。然而JAVA中会有多线程的问题,试想当两个线程同时new,那么就会有两个实例,破坏了单例模式的思想。那么可以这样解决,在getInstance方法上加一把同步锁:

public static synchronized Singleton getInstance(){

然而,本来我们的意思是不要同时去创建实例就好了,现在变为了不要同时获得实例。如果本来已经创建好了,两个线程同时想要获取这个实例,也需要阻塞,这浪费时间了吶。

3、双重校验锁

public class Singleton {
    private static Singleton instance = null;
    private Singleton(){}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

【核心实现方式】修改懒汉模式的加锁机制

【优点】实现了单例,支持了延迟加载

【缺点】jdk1.5后才能正常实现单例

【推荐】NO ×

【相关信息】

很好理解,前面提到懒汉模式的加锁的目的变化了,从“不能同时创建”变为了“不能同时获得”,导致很多不必要的加锁阻塞。这里首先判断一下是否是没有创建实例的竞争,如果是,才加锁,这样的话就会放行普通的获取实例的调用。然后再二次判断是否实例为空,为空才new。似乎很完美,然而问题在于JAVA有指令重排优化,为了达到更好的性能,JAVA根据情况可能会对指令调换顺序,new操作和赋值操作是不知道谁先谁后的。也就是说,如果在调用构造函数之前,就已经给instance分配了内存并赋值了默认值,这时候instance就不是null了,如果恰好发生切换,另一个线程就会认为已经创建好了实例,直接return instance,访问到了错误的地址,程序就GG了。jdk1.5以后的版本增加了volatile关键字,可以达到禁止语义重排优化的目的,我们可以这样写:

private static volatile Singleton instance = null;

这样就不会出现问题了。

4、枚举

public enum Singleton{
    INSTANCE;
    public void whateverMethod(){
        System.out.println("test enum singleton.");
    }
}

【核心实现方式】利用枚举的特性

【优点】规避了常见的单例缺点,比如线程同步问题、反序列化创建新实例、反射攻击等等

【缺点】无法做到延迟加载(lazy loading),jdk1.5以后才支持

【推荐】YES √

【相关信息】

这是《Effective Java》一书中推荐的方法,它利用了jdk1.5以后出现的新数据结构——枚举的所有特性来几乎完美地实现了单例模式。enum可以有很多成员和方法,这使得我们可以在枚举中定义成员、编写方法。enum有一个默认的private构造器,防止被多次构造,这不就是单例的特性吗。enum就是一个普通的类,它继承自java.lang.Enum,有同学将class反编译后发现是这样的对应关系:

原始代码:

public enum myEnum {
    INSTANCE;
}

反编译代码:

public final class myEnum extends Enum<myEnum> {
      public static final myEnum INSTANCE;
      public static myEnum[] values();
      public static myEnum valueOf(String s);
      static {};
}

在这之后会对这些静态代码进行初始化,也就是说,非常类似于饿汉模式。并且采用了final关键字,无法被继承。至于为什么能防范反序列化,是因为枚举的writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法都是被禁用的,就防止了通过readObject来返回新实例,但是可以通过相同的名字进行valueOf。为什么能防反射,是因为java.lang.reflect.Constructor,屏蔽掉了enum,会直接抛出异常:

(jdk1.8 / java.lang.reflect.Constructor / 第520行)

if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");

简直了……完美。

调用方法:

public class SingletonTest {
    public static void main(String[] args){
        Singleton.INSTANCE.whateverMethod();
    }
}

结果:

三、总结

单例实现方法是否推荐
 饿汉模式(三种方法) √
 懒汉模式 ×
 双重校验锁 ×
 枚举 √  

参考资料

1、《Java虚拟机类加载机制》

2、【面试题】java类加载机制探索

3、《Java单例模式——并非看起来那么简单》

4、《Java:单例模式的七种写法》

5、《Java单例模式(Singleton)以及实现》

6、《【单例深思】枚举实现单例原理》

7、《为什么不能通过反射来实例化 枚举类》

8、《关于枚举式单例的一些详解》

9、《Java枚举enum以及应用:枚举实现单例模式》


转载请注明出处http://www.bewindoweb.com/209.html | 三颗豆子
分享许可方式知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议
重大发现:转载注明原文网址的同学刚买了彩票就中奖,刚写完代码就跑通,刚转身就遇到了真爱。
你可能还会喜欢
具体问题具体杠