设计模式(一)— 单例设计模式
1、什么是单例模式
当我们需要某个类只创建一个实例的时候,就用到了单例设计模式。比如加载配置文件的类。
单例模式便是创建型设计模式的一种。
单例的可以分为三个主要的步骤来实现:
私有化静态变量
私有化构造
创建一个public方法,供外界调用
2、饿汉式
public class Hungry {
private Hungry() {}
private static Hungry hungry = new Hungry();
public static Hungry getInstance() {
return hungry;
}
}
hungry 为 static 的关系,在类加载过程中就会执行。由此带来的好处是Java的类加载机制本身为我们保证了实例化过程的线程安全性(没有线程安全的问题)。
缺点是:不管我们是否使用这个类,它都会在加载时实例化,占用我们的空间。
3、懒汉式
public class Lazy {
private static Lazy lazy = null;
private Lazy() {}
public static Lazy getInstance() {
if(lazy == null) {
lazy = new Lazy();
}
return lazy;
}
}
懒汉模式的好处就在于当我们用到的时候才会去实例化。
缺点就是:单例时为了确保我们的系统中只存在一个实例,上述的代码在单线程中是能保证的,但是在多线程中就不能保证了。
下边我们测试一下多线程下的单例:
public class Lazy {
private static Lazy lazy = null;
private Lazy() {
System.out.println(Thread.currentThread().getName() + " is init !!!");
}
public static Lazy getInstance() {
if(lazy == null) { // ------------1
lazy = new Lazy(); // ------------2
}
return lazy;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
Lazy.getInstance();
}).start();
}
}
}
如果我们多跑几次,一定会出现产生多个实例的情况。
Thread-0 is init !!!
Thread-2 is init !!!
Thread-1 is init !!!
原因解释:
假设现在有两个线程A和B,首先A线程调用 getInstance 方法,当执行到语句1时会判断对象是否为空,由于该类还没被实例化,所以条件成立,遍进入到花括号中准备执行语句2,正如前面所说线程的切换是随机,当正准备执行语句2时,线程A突然停在这里了,CPU切换到线程B去执行。当线程B执行这个方法时,也会判断语句1的条件是否成立,由于A线程停在了语句1和2之间,实例还未创建,所以条件成立,也会进入到花括号中,注意此时线程B并未停止,而是顺利的执行语句2,创建了一个实例,并返回。然后线程B又切换回了线程A,别忘了,这时,线程A还停在语句1和2之间,切换回它的时候就又继续执行下面的代码,也就是执行语句2,创建了一个实例,并返回。这样,两个对象就被创建出来了,我们的单例模式也就失效了。
解决:
给 getInstance 这个方法加把锁:
public static synchronized Lazy getInstance() {
if(lazy == null) {
lazy = new Lazy();
}
return lazy;
}
带来的问题
synchronized锁住了整个方法,降低了执行了效率。
4、 双重检查锁
针对上边出现的问题,我们修改一下锁的范围,将同步方法改为同步代码块
public class Lazy {
private static Lazy lazy = null;
private Lazy() {
System.out.println(Thread.currentThread().getName() + " is init !!!");
}
public static Lazy getInstance() {
if(lazy == null) { // -------1
synchronized (Lazy.class){ // -------2
lazy = new Lazy(); // -------3
}
}
return lazy;
}
}
上边的代码会出现什么问题呢?
如果有两个线程A、B调用 getInstance 方法。假设A先调用,当A调用方法时,会执行语句1进行条件判断,由于对象尚未创建,所以条件成立,正准备执行语句2来获取同步锁。我们上面也分析过了,线程的切换是随机的,还未执行语句2时,线程A突然停这了,切换到线程B执行。当线程B调用 getInstance 方法时也会执行语句1进行条件的判断,由于这时实例还未创建,所以条件成立,注意这时线程B还是没有停,又继续执行了语句2和3,即获取了同步锁并创建了Singleton对象。这时线程B切换回A,由于A此时还停在语句1和2之间,切回A时,就又继续执行语句2和3,即获取同步锁并创建了Singleton对象,这样两个对象就被创建出来了,synchronized 也失去了意义。
解决:
在步骤2和步骤3中间在加一个空判断
public class Lazy {
private static Lazy lazy = null;
private Lazy() {
System.out.println(Thread.currentThread().getName() + " is init !!!");
}
public static Lazy getInstance() {
if(lazy == null) {
synchronized (Lazy.class){
if(lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
}
代码到了这一步,即解决了多线程下重复实例化的问题,也提高了代码的执行效率,同时也是懒加载,只有使用到的时候才会实例化。
现在还存在什么问题呢???
我们先来了解一下java虚拟机在创建对象的时候,会具体做哪些事情呢?
-
在栈内创建 lazy 变量,在堆内存中开辟出一块空间用于存放Lazy实例对象,该空间会得到一个随机地址,假设为0x0001;
-
对 Lazy 对象进行初始化;
-
将 lazy 变量 指向该对象,也就是将该对象的地址0x0001赋值给 lazy变量,此时lazy就不为null了;
还有就是程序的运行过程中实际上就是CPU在执行一条条的指令,有的时候CPU为了提高执行效率,会将指令的顺序打乱,但是不会影响到程序的运行结果,也就是所谓的指令重排序。
了解了以上这些,我们再来看一下上边的代码会有什么问题呢?
假设现在有两个线程A、B,CPU先切换到线程A,当执行上述创建对象语句时,假设是以132的顺序执行,当线程A执行完3时(执行完第3步后 lazy 就不为null了),突然停住了,CPU切换到了线程B去调用 getInstance 方法,由于 lazy 此时不为null,就直接返回了lazy,但此时步骤2是还没执行的,返回的对象还是未初始化的,这样程序也就出问题了。
解决:
这个时候就要用到我们的 volatile 了,volatile可以避免上述出现的指令重排序问题。
现在来看一下完整的DCL代码(Double Check Lock)
package com.zzp.design.singleton;
public class Lazy {
private static volatile Lazy lazy = null;
private Lazy() {
System.out.println(Thread.currentThread().getName() + " is init !!!");
}
public static Lazy getInstance() {
if(lazy == null) {
synchronized (Lazy.class){
if(lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
}
存在的问题:
如果说我们只是简单的使用的话,到这里已经是可以使用了,但是这还不是最安全的。为什么这么说呢??? 因为java还有一个反射机制,它可以不管你的构造是否是私有的,都能拿到你的构造创建对象。
public static void main(String[] args) throws Exception {
Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true); //设置为true,就能操作private修饰的变量或者是方法
Lazy lazy1 = declaredConstructor.newInstance();
Lazy lazy2 = declaredConstructor.newInstance();
System.out.println(lazy1);
System.out.println(lazy2);
}
执行一下我们的代码:
com.zzp.design.singleton.Lazy@135fbaa4
com.zzp.design.singleton.Lazy@45ee12a7
我们可以看到,我们通过反射创建的实例还是无法保证是唯一的,神奇吧!!!!那我们试着去解决一下它。
package com.zzp.design.singleton;
import java.lang.reflect.Constructor;
public class Lazy {
private static volatile Lazy lazy = null;
private static boolean flag = false;
private Lazy() {
if(!flag) {
flag = true;
}else{
throw new RuntimeException("请不要试着用反射破坏单例!!!!");
}
}
public static Lazy getInstance() {
if(lazy == null) {
synchronized (Lazy.class){
if(lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
public static void main(String[] args) throws Exception {
Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true); //设置为true,就能操作private修饰的变量或者是方法
Lazy lazy1 = declaredConstructor.newInstance();
Lazy lazy2 = declaredConstructor.newInstance();
System.out.println(lazy1);
System.out.println(lazy2);
}
}
这个时候再去打印一下我们的结果:
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.zzp.design.singleton.Lazy.main(Lazy.java:36)
Caused by: java.lang.RuntimeException: 请不要试着用反射破坏单例!!!!
at com.zzp.design.singleton.Lazy.<init>(Lazy.java:14)
... 5 more
这样就解决了别人试图用反射破坏你的单例模式!!!!
这就是道高一尺,魔高一丈!!!
那有没有更简单快捷,同时又能避免上述问题的办法呢????
答案肯定是有的!!!
5、静态内部类实现单例
//静态内部类实现单例
public class Honner {
// 只有调用该静态内部类时才会创建该对象
public static class InnerClass {
private static final Honner honner = new Honner();
}
private Honner () {
}
public static Honner getInstance() {
return InnerClass.honner;
}
}
在我们的Honner类中创建一个InnerClass的静态内部类,在静态内部类中创建对象的实例。
由于JVM的特性,只有在使用到静态内部类的时候,才会实例化该对象,实现了延迟加载,又避免了多线程下线程不安全的问题。
6、使用枚举实现单例
public enum User {
user; //这个user就相当于创建User对象的对象实例,也就是不需要创建对象,直接拿这个值就行
private String userNm;
public String getUserNm() {
return userNm;
}
public void setUserNm(String userNm) {
this.userNm = userNm;
}
}
class test1{
public static void main(String[] args) {
User user1 = User.user;
}
}
这样我们就能直接拿到枚举创建的单例。
是因为枚举类的构造方法是私有的,你是无法调用到的并且你也无法通过反射来创建该实例,这也是枚举的独特之处。
枚举是不让我们通过反射来创建对象,所以是安全的。
现在我们通过反射来验证一下枚举到底能不能通过反射创建对象呢?
public static void main(String[] args) throws Exception {
Constructor<User> declaredConstructor = User.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
User user = declaredConstructor.newInstance();
System.out.println(user);
}
运行一下我们的程序:
这个时候我们的运行报错了,看一下原因。 是说User类没有无参的构造导致报错的。
这个时候就很奇怪了,明明User这个枚举类中没有构造方法,不是应该就只有一个默认的无参构造吗?
带着我们的疑惑,我们去看一下编译后的User类
我们看到IDEA中编译后的class文件中,User确实只有一个无参构造,咦???
感觉我们被它骗了,这个时候怎么办呢?
这个时候我们要用到一个jad反编译工具,Jad是可以将java中的**.class文件反编译成对应的.java文件**的一个工具。
下载链接 http://www.javadecompilers.com/jad
将jad拷贝到我们的User类路径下,执行命令 反编译出java文件
jad -s java User.class
这个时候就能拿到我们的java文件
打开看一下这个文件,我们可以看到,它的构造有两个参数,一个是String,一个是int。
现在我们再去试一下我们的反射创建实例:
public static void main(String[] args) throws Exception {
Constructor<User> declaredConstructor = User.class.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
User user = declaredConstructor.newInstance();
System.out.println(user);
}
我们运行一下我们的程序:
看到一个异常,说是不能够通过反射创建枚举对象。好了,到了这一步,我们验证出,枚举确实不能通过反射创建对象。
这个时候,我就又疑惑了,为什么枚举不能通过反射创建对象呢????
我们来看一下 newInstance 的源码:
它里边有这么一行话,如果这个类是枚举的话,就抛出异常。
Constructor<User> declaredConstructor = User.class.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
User user = declaredConstructor.newInstance();
System.out.println(user);
}
关于单例模式,就总结到这!!!