1.单例模式简介
1>.所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法);
例如:
比如Hibernate的SessionFactory,它充当数据存储源的代理,并负责创建Session对象.SessionFactory并不是轻量级的,一般情况下,一个项目通常只需要一个SessionFactory就够,这是就会使用到单例模式;
2>.单例模式八种形式:
①.
饿汉式(静态常量)
②.
饿汉式(静态代码块)
③.懒汉式(线程不安全)
④.懒汉式(线程安全,同步方法)
⑤.懒汉式(线程安全,同步代码块)
⑥.
双重检查
⑦.
静态内部类
⑧.
枚举
2.饿汉式(静态常量)
2.1.特点
①.构造器私有化(防止在其他地方创建(new)对象的实例);
②.类的内部创建(私有)对象实例;
③.向外暴露一个静态的公共(public)方法获取对象的实例;
2.2.代码实现
/**
* 单例模式--饿汉式(静态代常量)
*/
public class Hungry01 {
public static void main(String[] args) {
SingletonDemo singletonDemo = SingletonDemo.getInstance();
SingletonDemo singletonDemo1 = SingletonDemo.getInstance();
//对象实例只有一份
System.out.println(singletonDemo == singletonDemo1); //true
System.out.println(singletonDemo.equals(singletonDemo1)); //true
}
}
//问题1:为什么加final?
//为了防止在子类中不适当的覆盖了父类中的方法,破坏单例
//问题2:如果实现了序列化接口,还要做什么来防止反序列化破坏单例?
final class SingletonDemo implements Serializable {
//1.私有化构造器,防止外部创建对象实例
//问题3:为什么设置为私有?是否能防止反射创建新的实例?
//构造器私有化,之后在其他类中就无法通过构造函数来创建对象的实例
//不能防止反射创建新的实例
private SingletonDemo() {
}
//2.类的内部创建对象的实例(静态常量,由jvm保证线程安全)
//问题4:这样初始化是否能保证单例对象创建时的线程安全?
//能!静态成员变量的初始化操作是在类加载阶段完成的,在类加载阶段由JVM保证代码的线程安全性!
//因此静态成员变量是天生的线程安全的!
private final static SingletonDemo INSTANCE = new SingletonDemo();
//3.对外提供一个获取对象实例的公共方法
//问题5:为什么提供静态方法而不是直接将INSTANCE设置为public,说出你知道的理由
//使用方法可以提供更好的封装性,可以在内部实现一些懒惰的初始化操作!
//使用方法可以对创建单例对象时有更多的控制!
//使用方法可以提供泛型的支持!
public static SingletonDemo getInstance() {
return INSTANCE;
}
//问题2解决方案
//在反序列化的过程中,一旦发现readResolve()方法返回了一个对象,那么就使用方法中返回的这个对象
//而不会把真正反序列化那个字节码生成的对象当成反序列化的结果!
//保证使用的反序列化之后使用的是同一个对象,而不会生成新的对象;
public Object readResolve() {
return INSTANCE;
}
}
2.3.优缺点
2.3.1.优点
这种写法比较简单,就是在类装载的时候就完成实例化,驻留在内存中,其他线程直接使用即可,每个线程拿到的对象实例都是同一个.避免了线程同步问题;
2.3.2.缺点
在类装载的时候就完成实例化,没有达到Lazy Loading(懒加载)的效果.如果从始至终从未使用过这个实例,则会造成内存的浪费;
2.4.总结
这种方式基于classloder 机制避免了多线程的同步问题,不过,对象实例intance在类装载时就实例化,在单例模式中大多数都是调用 getInstance()方法获取对象实例intance会导致类装载,但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化对象实例intance就没有达到lazy loading 的效果,因为对象实例intance早就被创建出来了;
3.饿汉式(静态代码块)
3.1.代码实现
/**
* 单例设计模式--饿汉式--静态代码快
*/
public class Hungry02 {
public static void main(String[] args) {
SingletonDemo02 singletonDemo02 = SingletonDemo02.getInstance();
SingletonDemo02 singletonDemo021 = SingletonDemo02.getInstance();
System.out.println(singletonDemo02.equals(singletonDemo021)); //true
}
}
final class SingletonDemo02 {
//静态成员
private static SingletonDemo02 instance;
//静态代码块中进行对象实例化
static {
instance = new SingletonDemo02();
}
//私有化构造函数
private SingletonDemo02() {
}
//对外提供一个获取实例的方法
public static SingletonDemo02 getInstance() {
return instance;
}
}
3.2.优缺点
3.2.1.优点
这种方式和上面的"静态常量"方式类似,只不过将类的实例化过放到了静态代码块中,他同样也是在类装载的时候执行静态代码块中的代码,初始化类的实例,优点和上面一样;
3.2.2.缺点
会造成内存资源的浪费,缺点也和上面的一样;
4.懒汉式–线程不安全
4.1.代码实现
/**
* 单例模式--懒汉式--线程不安全
*/
public class Lazy01 {
public static void main(String[] args) {
LazyDemo1 lazyDemo1 = LazyDemo1.getInstance();
LazyDemo1 lazyDemo11 = LazyDemo1.getInstance();
//在单线程环境下可以保持两次获取的是同一个对象,但是多线程无法保证!!!
System.out.println(lazyDemo1.equals(lazyDemo11)); //true
}
}
class LazyDemo1 {
//静态成员变量
private static LazyDemo1 INSTANCE = null;
//私有化构造器
private LazyDemo1() {
}
//对外提供一个获取对象实例的公有方法,在方法中创建对象的实例,达到懒加载的目的
public static LazyDemo1 getInstance() {
if (instance == null) {
instance = new LazyDemo1();
}
return instance;
}
}
4.2.优缺点
4.2.1.优点
起到了懒加载的效果,但是只能在单线程环境下使用;
4.2.2.缺点
如果在多线程环境下,一个线程进入了"if(instance==null){…}"判断语句块,还没来得及往下执行(创建对象实例),这个线程就被挂起了,另一个线程通过判断语句进来了,然后创建对象实例,然后返回结果,之前被挂起的线程获取cpu执行权,继续执行,创建对象实例…,此时就会产生多个实例,无法达到单例模式的要求,所以在多线程环境下不能使用这种方式!!!
5.懒汉式–线程安全(同步方法)
5.1.代码实现
/**
* 单例模式--懒汉式--线程安全(同步方法)
*/
public class Lazy02 {
public static void main(String[] args) {
LazyDemo02 lazyDemo02 = LazyDemo02.getInstance();
LazyDemo02 lazyDemo021 = LazyDemo02.getInstance();
System.out.println(lazyDemo02.equals(lazyDemo021)); //true
}
}
final class LazyDemo02 {
//静态成员变量
private static LazyDemo02 INSTANCE = null;
//私有化构造器
private LazyDemo02() {
}
//对外提供一个获取对象实例的公共方法,在该方法中创建对象实例
//为了保证线程安全,在方法上加上一个synchronized同步关键字
//分析这里的线程安全,并说明有什么缺点?
//这里不会出现线程安全问题,因为使用了Synchronized关键字,可以保证代码在多线程环境下的原子性,有序性,可见性!
//唯一的缺点是锁的范围有点大,每一个线程调用该方法获取单例对象时都要进行加锁,阻塞,影响性能!!!
public static synchronized LazyDemo02 getInstance() {
if (INSTANCE == null) {
INSTANCE = new LazyDemo02();
}
return INSTANCE;
}
}
5.2.优缺点
5.2.1.优点
可以解决线程安全问题;
5.2.2.缺点
方法级别的同步效率太低了,每个线程执行getinstance()方法获取对象实例的时候,都需要等上一个线程释放锁然后争抢到锁之后才能获取到,而其实这个方法只需要执行一次即可,后面的线程要想获取到对象实例直接return即可,但是在实际开发中,不推荐使用这种方式!!!
6.懒汉式–线程安全(同步代码块)
代码省略,不推荐使用!!!
7.懒汉式–线程安全(双重检测)
7.1.代码实现
final class LazyDemo03 {
//静态成员变量
//问题1:解释为什么要加volatile?
//volatile可以保证已经被创建好的对象实例对于下一个将要获取对象实例的线程是可见的!
//volatile可以防止在Synchronized同步代码块内发生指令重排序,避免多线程环境下出现线程安全问题!
//注意:volatile无法保证代码原子性,所以要和Synchronized一起使用!!!
private static volatile LazyDemo03 INSTANCE = null;
//私有构造器
private LazyDemo03() {
}
//问题2:对比实现3,说出这样做的意义?
//缩小了锁的范围,当对象的实例被创建好同步到主内存之后,后续调用该方法获取单例对象的线程无需再加锁,阻塞,
//而是直接返回结果,性能得到极大的提升!
public static LazyDemo03 getInstance() {
if (INSTANCE == null) {
//第一批多个线程到这里等待获取锁
//第二批多个线程进入这个方法获取对象实例,发现对象实例已经被创建好了,直接返回结果,不需要在同步等待了
synchronized (LazyDemo03.class) {
//当某一个线程获取到锁,创建了对象实例,释放锁,返回对象实例
//其他线程获取到锁,发现对象实例已经被创建好了,直接返回结果,对象实例只是被创建了一次
//问题3:为什么还要在这里加为空判断,之前不是判断过了吗?
//为了防止出现在首次创建实例对象时,多个线程并发的问题
//例如线程t1得到锁,进入到同步代码块中创建对象实例,但是创建好的对象实例还未同步到主内存中
//线程t2也进来了,此时它得到的对象实例是null,然后它在同步代码块之外等着/阻塞
//当线程t1执行完毕之后释放锁,此时对象实例已经同步到主内存中
//然后线程t2获取锁,进入到同步代码块中,如果这里不加判断,那么线程t2又创建了一个对象的实例
//并且用新创建的对象实例覆盖之前线程t1创建的对象实例,最终导致对象被创建了多个实例!!!
if (INSTANCE == null) {
//当某一个线程获取到锁,创建了对象实例,释放锁,返回对象实例
//其他线程获取到锁,发现对象实例已经被创建好了,直接返回结果,对象实例只是被创建了一次
INSTANCE = new LazyDemo03();
}
}
}
return INSTANCE;
}
}
7.2.优缺点
进行两次检查,可以保证线程安全,而且创建对象实例的过程也只是执行了一次,多线程环境下提高了效率,重要的是他还是可以达到懒加载的效果;
8.静态内部类
8.1.说明
①.
这种方式采用了类装载的机制保证初始化实例时候只有一个线程;
②.
在外部类被装载时并不会立即装载静态内部类,而是在使用静态内部类的(成员)时候才会装载静态内部类,从而完成外部对象的实例化;
③.
静态内部类的静态属性只会在第一次加载静态内部类的时候初始化,所以在这里,jvm帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的;
8.2.代码实现
/**
* 单例模式--静态内部类
*/
public class Lazy04 {
public static void main(String[] args) {
//100个线程
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "获取对象实例:" + LazyDemo04.getInstance());
}, "线程" + i).start();
}
}
}
final class LazyDemo04 {
//私有化构造函数
private LazyDemo04() {
}
//静态内部类,天生线程安全,当使用内部的静态成员才会被装载到内存中,达到懒加载的效果
//问题1:属于懒汉式还是饿汉式?
//懒汉式,只有在调用getInstance()方法获取对象实例的时候才会加载当前静态内部类,初始化对象的实例
//如果只是用到外部的中的其他成员变量/方法,那么是不会触发当前这个静态内部类的类加载操作的!!!
private static class LazyDemo04Instance {
//在静态内部中使用静态常量进行对象实例化
//静态成员变量在类加载阶段由jvm保证线程安全性,是天生的线程安全!
private final static LazyDemo04 INSTANCE = new LazyDemo04();
}
//对外提供一个获取对象实例的方法
//问题2:在创建时是否有并发问题?
//不会!
public static LazyDemo04 getInstance() {
//使用静态内部类的(成员)时候才会进行内部类的装载,从而进行对象实例化
return LazyDemo04Instance.INSTANCE;
}
}
8.3.优缺点
保证线程安全,静态内部类实现了懒加载的效果,从而提高效率;
9.枚举单例
9.1.代码实现
/**
* 单例模式--枚举
*/
public class Lazy05 {
public static void main(String[] args) {
LazyDemo05 lazyDemo05 = LazyDemo05.INSTANCE;
LazyDemo05 lazyDemo051 = LazyDemo05.INSTANCE;
System.out.println(lazyDemo05.equals(lazyDemo051)); //true
}
}
//问题1:枚举单例是如何限制实例个数的?
//枚举类中的对象在定义时有几个,那么将来就有几个,枚举中定义的对象相当于枚举类的静态成员变量
//问题2:枚举单例在创建时是否有并发问题?
//并没有,枚举类中定义的对象是静态成员变量,线程安全性是在类加载阶段由jvm保证的,天生的线程安全!
//问题3:枚举单例能否被反射破坏单例?
//不能.
//问题4:枚举单例能否被反序列化破坏单例?
//枚举类默认都实现了序列化口
//由于在实现序列化接口的过程中已经考虑到了被反序列化破坏单例的问题,而且已经提前做了处理
//因此枚举单例可以避免被反序列化破坏单例
//问题5:枚举单例属于懒汉式还是饿汉式?
//饿汉式,静态成员变量
//问题6:枚举单例如果希望加入一些在单例创建时的初始化逻辑,该如何做?
//可以在枚举类中添加一个构造函数,然后将初始化逻辑添加到构造函数中即可!
enum LazyDemo05 {
INSTANCE;
}
9.2.优缺点
这借助JDK1.5中添加的枚举来实现单例模式.不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象.这种方式是Effective Java作者Josh Bloch提倡的方式!!!
10.单例模式JDK源码分析
1>.JDK中,java.lang.Runtime
就是经典的单例模式(饿汉式);
11.单例模式注意事项
1>.单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能;
2>.当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new;
3>.单例模式使用的场景:
①.需要频繁的进行创建和销毁的对象;
②.创建对象时耗时过多或耗费资源过多(即:重量级对象)但又经常用到的对象;
③.工具类对象;
④.频繁访问数据库或文件的对象(比如数据源、session工厂等);