前言
最近在做一个简单的项目,需要调用大量的无状态函数,首先就想到了之前用过的单例模式设计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();
}
.....
}
public class Singleton {
private static class SingletonHolder {
private static Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
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;
}
}
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;
}
}
很好理解,前面提到懒汉模式的加锁的目的变化了,从“不能同时创建”变为了“不能同时获得”,导致很多不必要的加锁阻塞。这里首先判断一下是否是没有创建实例的竞争,如果是,才加锁,这样的话就会放行普通的获取实例的调用。然后再二次判断是否实例为空,为空才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.");
}
}
原始代码:
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
中
(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();
}
}
结果:
三、总结
单例实现方法 | 是否推荐 |
---|---|
饿汉模式(三种方法) | √ |
懒汉模式 | × |
双重校验锁 | × |
枚举 |