上一篇:设计模式基本概述
1.什么是单例模式?
单例模式是java种最简单的设计模式之一,它提供了一种创建对象的最佳方式。此种设计模式保证一个类只有一个实例,并且提供一个访问该实例的全局访问点(就像是一个学校只有一个校长)。
2.单例模式的优点?
1.单例模式因为在内存中只有一个实例,避免了频繁地创建实例、销毁实例,所以极大地节省了系统资源地开销。
2.设置全局访问点,优化共享资源访问。
3.常见地单例模式?
1.饿汉式
此种设计模式线程安全,调用效率高,不能够延时加载
/**
* 饿汉单例模式
*/
public class Singleton01 {
//让构造函数私有化,这样就不会被实例化
private Singleton01() {
}
//创建Singleton对象(因为使用了static关键字,所以类加载地时候就会被初始化,所以不会有线程安全问题)
private static Singleton01 instance = new Singleton01();
//获取唯一可用对象,因为线程安全,所以不需要使用synchronized关键字,效率更高
public static Singleton01 getInstance() {
return instance;
}
}
饿汉式不管这个对象用不用,反正类已加载地时候就会创建,如果想要在使用地时候再创建此对象,则可以使用懒汉式。
2.懒汉式
此种设计模式线程不安全(可用synchronized解决),调用效率不高,能够延时加载
/**
* 懒汉式
*
* 懒嘛!要用地时候才去创建对象
*/
public class Singleton02 {
//构造方法私有化
private Singleton02() {
}
//类初始化地时候,不立即加载对象
private static Singleton02 instance;
/**
*给一个公共地获取对象地方法(懒汉式本来线程不安全,但是可以使用synchronized关键字保证线程安全,
*
* 也正是使用了synchronized关键字使得调用效率降低
*/
public static synchronized Singleton02 getInstance(){
if(instance==null){
instance=new Singleton02();
}
return instance;
}
}
代码注释中说了这种懒汉式的单例虽然可以使用synchronized
关键字使得线程安全,但是效率降低了,为了提高效率,则可以使用DCL( double-checked locking)双重校验锁
。
3.DCL( double-checked locking)双重校验锁
/**
* DCL双重校验锁
*/
public class Singleton03 {
//1.构造方法私有化
private Singleton03() {
}
//2.类初始化的时候,不立即加载对象
private static Singleton03 instance;
//3.给个获取该对象的公共方法
public static Singleton03 getInstance() {
//判断instance是否为空
if (instance == null) {
//instance为空立即上锁
synchronized (Singleton03.class) {
//再次判断instance是否为空
if (instance == null) {
instance = new Singleton03();
}
}
}
return instance;
}
}
DCL双重校验锁不是上来就直接将整个publice
方法给锁上而是将锁更加细化,不仅保证了对象在使用的时候才去创建而且提高了效率。但是!
DCL这种方式因为JMM(java内部模型)
的原因,偶尔可能会出问题:
因为不是原子性操作,在内存中一般会经历下面3步:
1)分配内存
2)执行构造方法
3)指向地址
而在执行代码的时候,为了提高性能,编译器和处理器常常会对指令进行重排序。重排序分为三种:编译器重排序,指令级并行重排序,内存系统重排序,实现优化,优化结果可能是
1)初始化 Singleton4 对象;
2)把 Singleton4 对象地址赋给instance变量;
也有可能是这样
1)初始化一半Singleton4对象;
2)把Singleton4对象地址赋给instance变量;
3)初始化剩下的Singleton4对象;
如果是第二种,则DCL
在多线程的情况下可能出现的情况:当第一个线程判断instance==null
为true
,进入synchronized
内,执行instance=new Singleton03()
的时候,初始化了一般就将地址赋给instance
变量,此时第二个线程判断instance==null
的时候发现instance
不为null,然后执行了return instance
,而实际上此时第一个线程可能正在执行剩下的instance=new Singleton03()
。
我们可以使用volatile
关键字能够大幅避免此种情况,但是也只能避免指令重排,继续优化有一种静态内部类式
4.静态内部类式
/**
* 静态内部类式
*/
public class Singleton04 {
//构造方法私有化
private Singleton04() {
}
//创建静态内部类
private static class InnerClass {
//在静态内部类中创建对象实例化
private static final Singleton04 instance=new Singleton04();
}
public static Singleton04 getInstance(){
return InnerClass.instance;
}
}
此种方式:外部类没有静态属性,不会在类加载的时候就初始化,只有调用了getInstance()
方法的时候才回去加载静态内部类,加载的时候线程安全所以不用考虑线程安全问题。所以此种模式又保证了线程安全,又符合lazy loading(延时加载)
。
到了第4种方式应该说已经属于非常优秀的了,但是java中有一种非常牛X的机制叫反射,它能够无视你是否是private
的,一得就能得到!这也导致了静态内部类式
也可能被破坏单例;
/**
* 静态内部类式
*/
public class Singleton04 {
//构造方法私有化
private Singleton04() {
}
//创建静态内部类
private static class InnerClass {
//在静态内部类中创建对象实例化
private static final Singleton04 instance = new Singleton04();
}
public static Singleton04 getInstance() {
return InnerClass.instance;
}
}
class test {
public static void main(String[] args) throws Exception {
Singleton04 instance = Singleton04.getInstance();
//使用反射破坏单例
//通过class对象获取构造器
Constructor<Singleton04> singleton04Constructor = Singleton04.class.getDeclaredConstructor(null);
//设置无视private
singleton04Constructor.setAccessible(true);
//创建一个实例
Singleton04 singleton04 = singleton04Constructor.newInstance();
boolean result = instance == singleton04;
System.out.println("两个实例对象是否一样:" + result);
}
}
从结果来看,使用反射直接破坏了单例,那么之前的几种单例实现方式也都可以通过反射进行破坏。
为了防止通过反射来破坏单例,我们可以使用枚举。
5.枚举
枚举是在JDK1.5开始引入的,它线程安全、不能延时加载、调用效率高
我们通过反射创建对象实例的newInstance()
方法的源码中就说了如果是枚举会抛出异常!
所以枚举是一种比较推荐的单例模式写法
/**
* 枚举
*/
public enum Singleton05 {
INSTANCE;
public Singleton05 getInstance() {
return INSTANCE;
}
}
/**
* 测试
*/
class Test {
public static void main(String[] args) {
Singleton05 instance1 = Singleton05.INSTANCE;
Singleton05 instance2 = Singleton05.INSTANCE;
boolean result = instance1 == instance2;
System.out.println("两个实例是否一致:" + result);
}
}