单例设计模式
单例设计模式介绍
单例模式就是采取一定的方法保证在整个的软件体系中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)
比如在Hibemate的SessionFactory,它充当数据存储源的代理并负责创建Session对象,SessionFactory并不是轻量级的,一般情况下,一个项目通常需要一个SessionFactory就够,这就使用到单例模式。
单例设计模式的八种方式
1. 饿汉式(静态常量)
2. 饿汉式(静态代码块)
3. 懒汉式(线程不安全)
4. 懒汉式(线程安全,同步方法)
5. 懒汉式(线程安全,同步代码块)
6. 双重检查
6. 静态内部类
7. 枚举
1、饿汉式(静态常量)
步骤:
1、构造器私有化(private,防止new)
2、类的内部创建对象
3、向外部提供一个静态的公共方法,getInstance
package com.xhl.Singleton;
/*
* 饿汉式
* 类加载到内存后,就实例化一个单例,JVM保证线程安全
* 简单实用,推荐使用
* 唯一的缺点:不管用到与否,类装载时就完成了实例化
* class.forName(""),只把class放到内存,不对他实例化,
* 如果用这种方式吧Mrg01加载到内存,INSTANCE因为是静态的变量,所以一定会实例化
*
*
*/
public class Mrg01 {
private static final Mrg01 INSTANCE = new Mrg01();
private Mrg01(){};
public static Mrg01 getInstance() {
return INSTANCE;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
Mrg01 m1 = Mrg01.getInstance();
Mrg01 m2 = Mrg01.getInstance();
System.out.println(m1==m2);
}
}
优缺点说明:
优点:
这种写法比较简单,就是在类加载的时候就完成实例化,避免了线程同步的问题。
缺点:
在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终都未使用过这个实例,则会造成内存浪费(如果不需要使用,干嘛要装载类呢,大多数情况使用这个单例模式就ok)
2、饿汉式(静态代码块)
package com.xhl.Singleton;
/*
* 跟第一个一个意思
*
*/
public class Mrg02 {
private static final Mrg02 INSTANCE ;
static {
INSTANCE = new Mrg02();
}
private Mrg02(){};
public static Mrg02 getInstance() {
return INSTANCE;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
Mrg02 m1 = Mrg02.getInstance();
Mrg02 m2 = Mrg02.getInstance();
System.out.println(m1==m2);
}
}
优缺点说明:
这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是-样的。
结论:这种单例模式可用,但是可能造成内存浪费
3、懒汉式(线程不安全)
package com.xhl.Singleton;
/*
* lazy Loading
* 懒汉式单例
* 虽然达到了按需初始化的目的,但是带来了线程不安全的问题
*/
public class Mrg03 {
private static Mrg03 INSTANCE ;
private Mrg03(){};
public static Mrg03 getInstance() {
if(INSTANCE==null) {
//由于线程速度太快,很难看到线程不安全的结果,在这里进行一个缓冲
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
INSTANCE = new Mrg03();
}
return INSTANCE;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
for(int i=0;i<100;i++) {
new Thread(new Runnable() {
@Override
public void run() {
// 同一个类的不同对象的hash码是不同的
System.out.println(Mrg03.getInstance().hashCode());
}
}).start();
}
}
}
分析:
可以看到产生了不同的hash码,初始化了多个实例。
优缺点说明:
1、起到了Lazy Loading的效果,但还是只能在单线程下使用。
2、如果在多线程下,一个线程进入了if(INSTANCE==null)判断语句块,还未来得及向下执行,另一个线程也通过了这个判断语句,这时就会产生多个实例。所以在多线程环境下不可以使用这种方式。
3、结论:在实际开发中,不用使用这种方式。
4、懒汉式(线程安全,同步方法)
package com.xhl.Singleton;
/*
* lazy Loading
* 懒汉式单例
* 虽然达到了按需初始化的目的,但是带来了线程不安全的问题
* 可以通过synchronized解决,但是带来了效率的下降
*/
public class Mrg04 {
private static Mrg04 INSTANCE ;
private Mrg04(){};
public static synchronized Mrg04 getInstance() {
if(INSTANCE==null) {
//由于线程速度太快,很难看到线程不安全的结果,在这里进行一个缓冲
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
INSTANCE = new Mrg04();
}
return INSTANCE;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
for(int i=0;i<100;i++) {
new Thread(new Runnable() {
@Override
public void run() {
// 同一个类的不同对象的hash码是不同的
System.out.println(Mrg04.getInstance().hashCode());
}
}).start();
}
}
}
优缺点说明:
1、解决了线程安全问题
2、效率太低了,每个线程在想获得类的实例时候,执行getInstance0方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低。
3、结论:在实际开发中,不推荐使用这种方式
5、懒汉式(线程安全,同步代码块)
package com.xhl.Singleton;
/*
* lazy Loading
* 懒汉式单例
* 虽然达到了按需初始化的目的,但是带来了线程不安全的问题
* 可以通过synchronized解决,但是带来了效率的下降
*/
public class Mrg05 {
private static Mrg05 INSTANCE ;
private Mrg05(){};
public static Mrg05 getInstance() {
synchronized (Mrg05.class) {
if(INSTANCE==null) {
//由于线程速度太快,很难看到线程不安全的结果,在这里进行一个缓冲
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
INSTANCE = new Mrg05();
}
}
return INSTANCE;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
for(int i=0;i<100;i++) {
new Thread(new Runnable() {
@Override
public void run() {
// 同一个类的不同对象的hash码是不同的
System.out.println(Mrg05.getInstance().hashCode());
}
}).start();
}
}
}
不推荐使用,书上写的synchronized 在if后面,我感觉,这样的话线程还是不安全。。。
6、双重检查
package com.xhl.Singleton;
/*
* 双重检查
*/
public class Mrg06 {
private static volatile Mrg06 INSTANCE ;
private Mrg06(){};
public static Mrg06 getInstance() {
if(INSTANCE==null) {
synchronized (Mrg06.class) {
if(INSTANCE==null) {
//由于线程速度太快,很难看到线程不安全的结果,在这里进行一个缓冲
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
INSTANCE = new Mrg06();
}
}
}
return INSTANCE;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
for(int i=0;i<100;i++) {
new Thread(new Runnable() {
@Override
public void run() {
// 同一个类的不同对象的hash码是不同的
System.out.println(Mrg06.getInstance().hashCode());
}
}).start();
}
}
}
在之前的基础上先判断INSTANCE是否为空,可以省去很多不必要的麻烦,不用每次都上同步锁,进行了一个性能的优化。这种方法比较完美。
优缺点说明:
1、Double- Check概念是多线程开发中常使用到的,如代码中所示,我们进行了两次if (INSTANCE== null)检查,样就可以保证线程安全了。
2、这样,实例化代码只用执行一-次,后面再次访问时,判断if (singleton= = null),直接returm实例化对象,也免的反复进行方法同步
3、线程安全;延迟加载;效率较高
4、结论:在实际开发中,推荐使用这种单例设计模式
7、静态内部类
package com.xhl.Singleton;
/*
* 静态内部类方式
* JVM保证单例
* 加载外部类时不加载内部类,可以实现懒加载(比较完美)
*/
public class Mrg07 {
private Mrg07(){};
private static class Mrg07Horder{
private static final Mrg07 INSTANCE = new Mrg07();
}
public static Mrg07 getInstance() {
return Mrg07Horder.INSTANCE;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
for(int i=0;i<100;i++) {
new Thread(()->{
System.out.println(Mrg07.getInstance().hashCode());
}).start();
}
}
}
优缺点说明:
1、这种方式采用了类装载的机制来保证初始化实例时只有一个线程。
2、静态内部类方式在Mrg07类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载Mrg07Horder类,从而完成Mrg07的实例化。
3、类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
4、优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高
5、结论:推荐使用.
8、枚举
package com.xhl.Singleton;
/*
* 枚举
* 不仅可以解决线程同步,还可以解决反序列化
*/
public enum Mrg08 {
INSTANCE;
public static void main(String[] args) {
// TODO Auto-generated method stub
for(int i=0;i<100;i++) {
new Thread(()->{
System.out.println(Mrg08.INSTANCE.hashCode());
}).start();
}
}
}
优缺点说明:
1、这借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
2、这种方式是Effective Java作者Josh Bloch提倡的方式
3、结论:推荐使用
单例模式在JDK应用的源码分析
我们JDK中,java.lang.Runtime就是经典的单例模式(饿汉模式,第一种)
单例模式注意事项和细节说明
单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能
当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new
单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即**:重量级对象**),但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、session工厂 等)
补充:登记式单例
登记式单例类是为了克服饿汉和懒汉式单例类均不可继承式的缺点而设计的。
是否 Lazy 初始化:是
是否多线程安全:是
描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟饿汉式方式不同的是:饿汉式方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比饿汉式方式就显得很合理。
(在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。)
package cn.singleton;
import java.util.HashMap;
public class RegSingleton {
private static HashMap map = new HashMap<>();
static {
RegSingleton rs = new RegSingleton();
map.put(rs.getClass().getName(), rs);
}
protected RegSingleton() {
}
public static RegSingleton getInstance(String name) {
if(name==null) {
name = RegSingleton.class.getName();
}
if(map.get(name)==null) {
try {
map.put(name, Class.forName(name).newInstance());
} catch (Exception e) {
// TODO Auto-generated catch block
System.out.println("Error happed!\n"+e.getMessage());
}
}
return (RegSingleton)map.get(name);
}
}
package cn.singleton;
public class RegChildSingleton extends RegSingleton{
public RegChildSingleton() {
}
public static RegChildSingleton getIntance() {
return (RegChildSingleton)RegSingleton.getInstance(RegChildSingleton.class.getName());
}
}