文章目录
单例模式
介绍
所谓的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。
步骤:
- 构造器私有化 (防止 new )
- 类的内部创建对象
- 向外暴露一个静态的公共方法。 getInstance()
1、饿汉式
package com.kuang.single;
//饿汉式单例:一上来就把所有的加载 耗费在内存
/**
* * 饿汉式(静态常量)应用实例
* 步骤如下:
* 1) 构造器私有化 (防止 new )
* 2) 类的内部创建对象
* 3) 向外暴露一个静态的公共方法。 getInstance
*/
public class Hungrysingle {
// 一上来就加载 可能会浪费空间
private byte[] data1 = new byte[1024*1024];
private byte[] data2 = new byte[1024*1024];
private byte[] data3 = new byte[1024*1024];
private byte[] data4 = new byte[1024*1024];
//1)、私有构造器
private Hungrysingle(){
}
//2)、类的内部创建对象
private final static Hungrysingle HUNGRYSINGLE = new Hungrysingle();
//3)、向外暴露一个静态的公共方法。 getInstance
public static Hungrysingle getInstance(){
return HUNGRYSINGLE;
}
public static void main(String[] args) {
Hungrysingle instance1 = Hungrysingle.getInstance();
Hungrysingle instance2 = Hungrysingle.getInstance();
System.out.println(instance1 == instance2);//true
}
}
1)、 优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
2) 、缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
4) 、结论:这种单例模式可用, 可能造成内存浪费
2、懒汉式
double checked locking
1)、懒汉式(线程不安全)
package com.kuang.single;
/**
* 所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,
* 对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。**
* 懒汉式单例:用的时候再加载
*/
public class LazyMan {
private LazyMan() {
System.out.println(Thread.currentThread().getName()+" ->OK");
}
private static LazyMan lazyMan ;
//单线程下没有问题,但是在多线程下有问题
public static LazyMan getInstance(){
//用的时候再加载
if (lazyMan == null) {
lazyMan = new LazyMan();
}
return lazyMan;
}
//测试单线程下懒汉式单例
public static void main(String[] args) {
LazyMan.getInstance();
LazyMan.getInstance();
}
}
结果:
main ->OK
(单线程下只有一个对象被创建,没有问题,再来测试多线程下)
优缺点说明:
1)、 起到了Lazy Loading的效果,但是只能在单线程下使用。
- 、如果在多线程下,一个线程进入了if (lazyMan == null) 判断语句块,还未来得及 往下执行,另一个线程也通过
了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式
3)、 结论:在实际开发中,不要使用这种方式.
2)、多线程并发测试单例模式:
package com.kuang.single;
/**
* 所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,
* 对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。**
* 懒汉式单例:用的时候再加载
*/
public class LazyMan {
private LazyMan() {
System.out.println(Thread.currentThread().getName()+" ->OK");
}
private static LazyMan lazyMan ;
//单线程下没有问题,但是在多线程下有问题
public static LazyMan getInstance(){
//用的时候再加载
if (lazyMan == null) {
lazyMan = new LazyMan();
}
return lazyMan;
}
//测试多线程并发
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
结果:(不唯一)
Thread-1 ->OK
Thread-3 ->OK
Thread-2 ->OK
Thread-0 ->OK
可以看到多线程下,有多个对象被创建,有问题
解决办法:
1)使用synchronized修饰getInstance()方法
提供一个静态的共有方法,加入同步处理的代码,解决线程安全问题
静态方法是属于类的而不属于对象的。同样的,synchronized修饰的静态方法锁定的是这个类的所有对象。
class LazyMan {
private LazyMan() {
System.out.println(Thread.currentThread().getName()+" ->OK");
}
private static LazyMan lazyMan ;
//单线程下没有问题,但是在多线程下有问题
public synchronized static LazyMan getInstance(){
//用的时候再加载
if (lazyMan == null) {
lazyMan = new LazyMan();
}
return lazyMan;
}
//测试多线程并发
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
----------------
Thread-0 ->OK
优缺点说明:
-
、解决了线程不安全问题
-
、效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行
一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低
- 、结论:在实际开发中,不推荐使用这种方式
2)双重检测+锁+volatile
修改getInstance()方法,其他地方不变
private static volatile LazyMan lazyMan ;
//解决办法:双重检测+锁 DCL懒汉式+volatile
public static LazyMan getInstance(){
//用的时候再加载
if(lazyMan == null){
synchronized (LazyMan.class){
if (lazyMan == null) {
lazyMan = new LazyMan();//不是原子性操作
/*
执行步骤:
1、分配内存空间
2、执行构造方法,初始化对象
3、把这个对象指向这个空间
会造成【指令重排】--->解决方法:lazyMan用volatile修饰
*/
}
}
}
return lazyMan;
}
结果:
Thread-0 ->OK(始终只有一个)所以成功了
- Double-Check概念是多线程开发中常使用到的,如代码中所示,我们进行了两次 if(lazyMan == null)检查,这
样就可以保证线程安全了。
-
这样,实例化代码只用执行一次,后面再次访问时,判断 if(lazyMan == null),直接return实例化对象,也避免的反复进行方法同步.
-
线程安全;延迟加载;效率较高
-
结论:在实际开发中,推荐使用这种单例设计模式
3)、反射会破坏单例:
知识点【反射】:
public static void main(String[] args) throws Exception {
LazyMan instance = LazyMan.getInstance();
//用反射创建对象
//无参构造器
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyMan instance1 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance1);
}
结果:
main ->OK
main ->OK
com.kuang.single.LazyMan@14ae5a5
com.kuang.single.LazyMan@7f31245a
单例模式被破坏
因为反射走的是无参构造,所以可以在构造函数中进行判断:
修改构造方法:
private LazyMan() {
synchronized (LazyMan.class){
if(lazyMan != null){
throw new RuntimeException("不要试图用反射破坏");
}
}
}
测试结果:
Exception in thread "main" java.lang.reflect.InvocationTargetException......
Caused by: java.lang.RuntimeException: 不要试图用反射破坏
at com.kuang.single.LazyMan.<init>(LazyMan.java:15)
... 5 more
反射被阻止。
若两个对象都是用反射创建的呢?
public static void main(String[] args) throws Exception {
//用反射创建对象
//无参构造器
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyMan instance1 = declaredConstructor.newInstance();
LazyMan instance = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance1);
}
结果:两个对象,单例模式又被破坏了
com.kuang.single.LazyMan@14ae5a5
com.kuang.single.LazyMan@7f31245a
解决办法:加入标志位
//标志位
private static boolean flag = true;
private LazyMan() {
synchronized (LazyMan.class){
if(flag == true){
flag = false;
}else{
throw new RuntimeException("不要试图用反射破坏");
}
}
}
结果:
Caused by: java.lang.RuntimeException: 不要试图用反射破坏
假设已经找到这个隐藏的变量flag,把它也破坏了:
public static void main(String[] args) throws Exception {
//破坏标识位
Field flag = LazyMan.class.getDeclaredField("flag");
flag.setAccessible(true);
//无参构造器
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyMan instance1 = declaredConstructor.newInstance();
flag.set(instance1,true);
LazyMan instance = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance1);
}
结果:
com.kuang.single.LazyMan@7f31245a
com.kuang.single.LazyMan@6d6f6e28
可以看出,单例模式被破坏了
接着分析:
进入newInstance():
LazyMan instance = declaredConstructor.newInstance();
枚举:
package com.kuang.single;
//枚举:
/**
* 枚举:本身是一个Class了
*/
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) {
EnumSingle instance1 = EnumSingle.INSTANCE;
EnumSingle instance2 = EnumSingle.INSTANCE;
System.out.println(instance1);
System.out.println(instance2);
}
}
结果:
INSTANCE
INSTANCE
试图破坏枚举:
分析EnumSingle里面的源代码:
有一个无参构造函数,对它进行破坏:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//正常获取
EnumSingle instance1 = EnumSingle.INSTANCE;
//反射获取
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
结果:
Exception in thread "main" java.lang.NoSuchMethodException: com.kuang.single.EnumSingle.<init>()
我们应该看到的是Cannot reflectively create enum objects这句话,但是结果却是:java.lang.NoSuchMethodException:(这个类中没有空参的构造器)
IDEA骗了我们
有空参构造,说明这个代码也骗了咱们
引入jad.exe
修改代码:
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
结果:出现了Cannot reflectively create enum objects(反射不能破坏枚举的单例)
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.kuang.single.Test.main(EnumSingle.java:26)
3、单例模式在JDK应用中的源码分析:
- 、我们JDK中,java.lang.Runtime就是经典的单例模式(饿汉式)
单例模式注意事项和细节说明
-
单例模式保证了 系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能
-
当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new
-
单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、session工厂等)