单例模式
单例模式就是采取一定的方法保证在整个软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。
1.饿汉式(静态常量)
- 构造器私有化(防止new)
- 类的内部创建对象
- 向外暴露一个静态的公共方法(getInstance())
public class SingletonTest01 {
public static void main(String[] args) {
Singleton singleton01 = Singleton.getInstance();
Singleton singleton02 = Singleton.getInstance();
//两个实例的地址相同,表示它们是同一对象实例
System.out.println(singleton01 == singleton02);
//两个实例的hashcode也相同,也表示它们是同一对象实例
System.out.println(singleton01.hashCode());
System.out.println(singleton02.hashCode());
}
}
class Singleton {
/**
* 1.构造器私有化,外部不能new
*/
private Singleton() {
}
/**
* 2.本类内部创建对象实例
*/
private final static Singleton INSTANCE = new Singleton();
public static Singleton getInstance() {
return INSTANCE;
}
}
这种方式基于classloader机制避免了多线程的同步问题,不过,instance在类装载的时候就实例化,在单例模式中大多数都是调用getInstance方法,但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance就没有达到Lazy Loading的效果,可能会造成内存的浪费。
2.饿汉式(静态代码块)
这种方式和上一个方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点与上一个方式相同,同样可能造成内存浪费。
public class SingletonTest01 {
public static void main(String[] args) {
Singleton singleton01 = Singleton.getInstance();
Singleton singleton02 = Singleton.getInstance();
//两个实例的地址相同,表示它们是同一对象实例
System.out.println(singleton01 == singleton02);
//两个实例的hashcode也相同,也表示它们是同一对象实例
System.out.println(singleton01.hashCode());
System.out.println(singleton02.hashCode());
}
}
class Singleton {
/**
* 1.构造器私有化,外部不能new
*/
private Singleton() {
}
private static Singleton INSTANCE;
static {
INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return INSTANCE;
}
}
3.懒汉式(线程不安全)
懒汉式的方式的确起到了Lazy Loading的效果,但是只能在单线程下使用;如果在多线程下,多个线程操作同一个临界资源,这时便会产生多个实例,所以在多线程环境下不能使用这种方式。在实际开发中,不要使用这种方式。
public class SingletonTest01 {
public static void main(String[] args) {
Singleton singleton01 = Singleton.getInstance();
Singleton singleton02 = Singleton.getInstance();
//两个实例的地址相同,表示它们是同一对象实例
System.out.println(singleton01 == singleton02);
//两个实例的hashcode也相同,也表示它们是同一对象实例
System.out.println(singleton01.hashCode());
System.out.println(singleton02.hashCode());
}
}
class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
4.懒汉式(线程安全,同步方法)
这种方式的确解决了线程不安全问题,但是带来了效率问题,每个线程在想要获取类的实例时,执行getInstance方法都要进行同步。
其实这个方法只执行一次实例化代码就够了,后面的线程想要获取该类的实例,直接return就行了,方法进行同步效率太低了,在实际开发中,不推荐使用这种方式
public class SingletonTest01 {
public static void main(String[] args) {
Singleton singleton01 = Singleton.getInstance();
Singleton singleton02 = Singleton.getInstance();
//两个实例的地址相同,表示它们是同一对象实例
System.out.println(singleton01 == singleton02);
//两个实例的hashcode也相同,也表示它们是同一对象实例
System.out.println(singleton01.hashCode());
System.out.println(singleton02.hashCode());
}
}
class Singleton {
private static Singleton instance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
5.懒汉式(线程安全,同步代码块)
这种方式本意是想对上一种方式进行改进,但是这种同步并不能起到线程同步的作用,可能在多线程下产生多个实例,因此在实际开发中,不能使用这种方式。
public class SingletonTest01 {
public static void main(String[] args) {
Singleton singleton01 = Singleton.getInstance();
Singleton singleton02 = Singleton.getInstance();
//两个实例的地址相同,表示它们是同一对象实例
System.out.println(singleton01 == singleton02);
//两个实例的hashcode也相同,也表示它们是同一对象实例
System.out.println(singleton01.hashCode());
System.out.println(singleton02.hashCode());
}
}
class Singleton {
private static Singleton instance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
6.双重检查(Double-Check)
解决了线程安全问题,同时解决了懒加载问题,同时保证效率。在实际开发中强烈推荐使用。
volatile的作用是防止在创建实例的时候,还没初始化完,有其他线程进来,此时实例已经不为空,多线程拿到的实例数据就不一样,加了该关键字后,先初始化完成后再赋值给实例。
volatile相对于synchronized来说是一个轻量级的锁,只能修饰变量。
通俗点解释就是: 多线程环境下一个线程修改了volatile修饰的变量时,别的线程能立马就读到这个新值。
public class SingletonTest01 {
public static void main(String[] args) {
/*HashMap<String, Integer> hm = new HashMap<>();
Runnable task = () -> {
Singleton s = Singleton.getInstance();
String sCode = s.hashCode() + "";
hm.merge(sCode, 1, (a, b) -> a + b);
};
// 模拟多线程环境下使用 Singleton 类获得对象
for (int i = 0; i < 1000; i++) {
new Thread(task).start();
}
//输出结果
for (Map.Entry<String, Integer> entry : hm.entrySet()) {
System.out.println(entry.getKey() + "\t" + entry.getValue());
}*/
Singleton singleton01 = Singleton.getInstance();
Singleton singleton02 = Singleton.getInstance();
//两个实例的地址相同,表示它们是同一对象实例
System.out.println(singleton01 == singleton02);
//两个实例的hashcode也相同,也表示它们是同一对象实例
System.out.println(singleton01.hashCode());
System.out.println(singleton02.hashCode());
}
}
class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
7.静态内部类
这种方式采用类装载的机制来保证初始化实例时只有一个线程
- 当类被装载的时候,其静态内部类不会被装载;
- 当调用类的静态方法时,其静态内部类会被装载;
- 当类被装载的时候,线程是安全;
类的静态属性只会在第一次加载类的时候初始化,所以JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程无法进入。
静态内部类也是非常好地实现单例模式的方式
public class StaticInnerSingleton {
private StaticInnerSingleton() {
}
private static class Holder {
static StaticInnerSingleton instance = new StaticInnerSingleton();
}
public static StaticInnerSingleton getInstance() {
return Holder.instance;
}
}
7.1 通过反射获取破坏静态内部类的单例模式
public class StaticInnerReflectTest {
public static void main(String[] args) {
try {
Class<?> clazz = StaticInnerSingleton.class;
Constructor<?> c = clazz.getDeclaredConstructor((Class<?>[]) null);
c.setAccessible(true);
Object o1 = c.newInstance();
StaticInnerSingleton o2 = StaticInnerSingleton.getInstance();
System.out.println(o1 == o2 ? "相等" : "不等");
} catch (NoSuchMethodException | InstantiationException | InvocationTargetException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
防止这种情况的发生, 可以在类的构造方法中判断实例是否为空, 如果不为空抛出异常, 这样反射就无法执行
7.2 通过序列化破坏静态内部类的单例模式
8.枚举
借助枚举方式实现单例模式,不仅能避免多线程同步问题,而且能防止反序列化重新创建新的对象;
Josh Bloch 推荐使用的方式
public class SingletonTest {
public static void main(String[] args) {
Singleton s1 = Singleton.INSTANCE;
Singleton s2 = Singleton.INSTANCE;
System.out.println(s1 == s2);
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
s1.sayHello();
}
}
enum Singleton {
/**
* 实例
*/
INSTANCE;
public void sayHello() {
System.out.println("hello");
}
}
9.单例注册表(Spring的单例模式实现)
解决单例类不能继承的痛点
package com.hoki.singleton;
import java.util.HashMap;
public class RegSingleton {
public static void main(String[] args) {
RegSingleton singleton01 = RegSingleton.getInstance(null);
RegSingleton singleton02 = RegSingleton.getInstance(null);
//两个实例的地址相同,表示它们是同一对象实例
System.out.println(singleton01 == singleton02);
//两个实例的hashcode也相同,也表示它们是同一对象实例
System.out.println(singleton01.hashCode());
System.out.println(singleton02.hashCode());
}
static private HashMap<String, Object> registry = new HashMap<>();
//静态块,在类被加载时自动执行
static {
RegSingleton rs = new RegSingleton();
registry.put(rs.getClass().getName(), rs);
}
/**
* 受保护的默认构造函数,如果为继承关系,则可以调用,克服了单例类不能为继承的缺点
*/
protected RegSingleton() {
}
/**
* 静态工厂方法,返回此类的唯一实例
*
* @param name
* @return
*/
public static RegSingleton getInstance(String name) {
if (name == null) {
name = "com.hoki.singleton.RegSingleton";
}
if (registry.get(name) == null) {
try {
registry.put(name, Class.forName(name).newInstance());
} catch (Exception ex) {
ex.printStackTrace();
}
}
return (RegSingleton) registry.get(name);
}
}
Spring框架源码实现如下:
public abstract class AbstractBeanFactory implements ConfigurableBeanFactory{
/**
* 充当了Bean实例的缓存,实现方式和单例注册表相同
*/
private final Map singletonCache=new HashMap();
public Object getBean(String name)throws BeansException{
return getBean(name,null,null);
}
...
public Object getBean(String name,Class requiredType,Object[] args)throws BeansException{
//对传入的Bean name稍做处理,防止传入的Bean name名有非法字符(或则做转码)
String beanName=transformedBeanName(name);
Object bean=null;
//手工检测单例注册表
Object sharedInstance=null;
//使用了代码锁定同步块,原理和同步方法相似,但是这种写法效率更高
synchronized(this.singletonCache){
sharedInstance=this.singletonCache.get(beanName);
}
if(sharedInstance!=null){
...
//返回合适的缓存Bean实例
bean=getObjectForSharedInstance(name,sharedInstance);
}else{
...
//取得Bean的定义
RootBeanDefinition mergedBeanDefinition=getMergedBeanDefinition(beanName,false);
...
//根据Bean定义判断,此判断依据通常来自于组件配置文件的单例属性开关
//<bean id="date" class="java.util.Date" scope="singleton"/>
//如果是单例,做如下处理
if(mergedBeanDefinition.isSingleton()){
synchronized(this.singletonCache){
//再次检测单例注册表
sharedInstance=this.singletonCache.get(beanName);
if(sharedInstance==null){
...
try {
//真正创建Bean实例
sharedInstance=createBean(beanName,mergedBeanDefinition,args);
//向单例注册表注册Bean实例
addSingleton(beanName,sharedInstance);
}catch (Exception ex) {
...
}finally{
...
}
}
}
bean=getObjectForSharedInstance(name,sharedInstance);
}
//如果是非单例,即prototpye,每次都要新创建一个Bean实例
//<bean id="date" class="java.util.Date" scope="prototype"/>
else{
bean=createBean(beanName,mergedBeanDefinition,args);
}
}
...
return bean;
}
}
10.JDK中Runtime的实现
这里使用的就是饿汉式,因为我们肯定会用到它,所以在内存不浪费的前提下,使用饿汉式可以保证线程安全。
public class Runtime {
private static Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
//剩下的代码省略
}
11.单例模式的使用场景
需要频繁地进行对象的创建和销毁,创建对象时耗时过多或耗费资源过多(即重量级对象),但又经常用到的对象如: 工具类对象,频繁访问数据库或文件的对象(数据源、session工厂等)。