本文章参考慕课DocMike老师的讲解,作为个人笔记,也希望能帮到需要的人
1.单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
1)饿汉模式
饿汉模式的3个特点:
1、有一个private的构造函数(不会在系统中的其他代码内被实例化)
2、mInstance的变量是static的
3、getInstance的方法也是static的(向外部提供获取单例的方法)
不足之处:无法对instance实例做延时加载(instance 在类装载时就实例化)
因为有时候单例的创建过程会很慢,变量初始化内容特别多。
因为已经把这个单例变量设置为static,所以在JVM虚拟机加载单例这个类的时候,单例对象就会被建立。如果这个时候,这个单例类在系统中还扮演其他角色,比如说还会被其他的场景初始化,那么在任何使用这个单例类的地方,都会初始化这个沉重的单例变量,而不管是否被用到。
instance会初始化很多的数据,导致他的static变量会被重复的加载
优化:懒汉模式
Code:
package singleton;
/**
* 饿汉模式
* @author stoneWang_L
*缺点:无法对mHungrySingleton这个变量做延时加载,因为单例的变量初始化有可能有很多变量
*/
public class HungrySingleton {
private static final HungrySingleton mHungrySingleton = new HungrySingleton();
private HungrySingleton() {
System.out.println("Singleton is create");
}
public static HungrySingleton getHungrySingleton() {
return mHungrySingleton;
}
public static void createString() {
System.out.println("createString in Singleton");
}
public static void main(String[] args) {
HungrySingleton.createString();
}
}
2)懒汉模式
优点:懒汉模式完成了延时加载的功能
不足之处:
在多线程并发下这样的实现是无法保证实例唯一的。
优化:懒汉线程安全
package singleton;
public class LazySingleton {
private static LazySingleton mInstance = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
//第一次调用的时候会被初始化
if (null == mInstance) {
mInstance = new LazySingleton();
}
return mInstance;
}
public static void createString() {
System.out.println("create String");
}
public static void main(String[] args) {
LazySingleton.createString();
}
}
在多线程并发下,懒汉是完全失效的。
package singleton;
public class MyThread extends Thread{
@Override
public void run() {
System.out.println(LazySingleton.getInstance().hashCode());
}
public static void main(String[] args) {
MyThread[] mts = new MyThread[10];
for (int i=0; i<mts.length; i++) {
mts[i] = new MyThread();
}
for (int j=0; j<mts.length; j++) {
mts[j].start();
}
}
}
结果,hashCode值不一致
13676004
5506328
5506328
5506328
13676004
33221309
33221309
5506328
5506328
5506328
失效的原因:LazySingleton的getInstance方法不是同步的。
所以在多线程的环境下,如果一个线程A正在创建一个LazySingleton,在他完成赋值操作以前,线程2可能正好判断这个instance为空,然后线程2也会走进创建LazySingleton,这个时候导致多个实例被创建,也印证了它的HashCode值是不一样的。
3)懒汉线程安全
优点:保证了懒汉模式下的线程安全
不足:影响性能(对instance判空都需要先同步,浪费时间)
synchronied虽然能保证线程安全,但是非常影响性能,因为只能有一个线程去读取里面的数据,这样读取实例的时候,另外的线程就不能读取。
优化:DCL(双重检查锁机制)
code:
package singleton;
public class LazySafetySingleton {
private static LazySafetySingleton instance;
private LazySafetySingleton() {
}
public static void createString() {
System.out.println("create String");
}
public static void main(String[] args) {
LazySafetySingleton.createString();
}
//方法一:方法名中声明synchronized关键字,这样就可以保证,出现非线程安全问题,由于多个线程可以同时进入getInstance方法,这里使用了synchronized关键字保证只有一个线程可以进入getInstance方法,这样的懒汉单例是线程安全的
public static synchronized LazySafetySingleton getInstance() {
if (instance == null) {
instance = new LazySafetySingleton();
}
return instance;
}
//方法二:同步代码块实现。不在方法中声明,而是锁住LazySafetySingleton.class这个对象来保证它的线程安全
// public static LazySafetySingleton getInstance() {
// synchronized (LazySafetySingleton.class) {
// if (instance == null) {
// instance = new LazySafetySingleton();
// }
// }
// return instance;
// }
}
4)DCL(double-checked locking,双重检查锁机制)
优点:解决了懒汉的线程安全和性能上的缺点,但是
不足之处:JVM的即时编译器中存在指令重排序的优化
解决办法:把instance变量声明为volatile
优化:静态内部类/枚举
code:
package singleton;
public class DclSingleton {
private static volatile DclSingleton mInstance = null; //使用volatile修饰,禁止JVM指令重排序
// private static DclSingleton mInstance = null;
private DclSingleton() {}
public void doSomething() {
System.out.println("do sth.");
}
public static DclSingleton getInstance() {
//避免不必要的同步
if (mInstance == null) {
//同步
synchronized (DclSingleton.class) {
//在第一次调用时初始化
if (mInstance == null) {
/*这一步看似完美,实则不是原子操作,不是原子操作意味着JVM会做这样一个事。
第一步会给instance分配内存,第二步调用DclSingleton的构造方法来初始化变量,第三步将mInstance这个对象指向我们JVM分配的内存空间,但是JVM有这样一个缺点,在即时编译器中存在指令重排序的优化。
就是说以上的3步不会按照我们想要的顺序执行,有可能第三步在第二步之前,有可能第二步在第一步之前。
所以这个时候就会造成线程的不安全,也会造成报错。
解决方法:将instance方法设置成volatile就行了,它能保证本地不会存在instance的副本,而每次都会到内存中去读取,实际上完全是。
实际上使用volatile关键字可以禁止JVM的指令重排序优化,原来的三步顺序就不会变了。
另外volatile修饰的这个变量,它的赋值操作会有一个内存屏障,读操作的时候不会被被重排序到内存屏障之前*/
mInstance = new DclSingleton();
}
}
}
return mInstance;
}
}
5)静态内部类
静态内部类:
JVM给我们提供了同步控制,什么时候提供的:其实JVM提供给我们2个关键字,static,final
首先,我们可以通过static进行区块的初始化数据,这个时候就保证我们的数据在内存中是独一份的,final字段访问的时候,由于final字段修饰的变量初始化完成之后,他就无法被修改,所以final字段也是线程安全的。
这就是JVM提供给我们的同步控制。
优点:JVM本身的机制保证了线程安全/没有性能缺陷
原因:static/final
三个优点:
1、JVM虚拟机本身的机制保证了线程安全
2、并没有使用关键字synchronized,
synchronied虽然能保证线程安全,但是非常影响性能,因为只能有一个线程去读取里面的数据,这样读取实例的时候,另外的线程就不能读取。
而静态内部类就不一样,他可以同时读取实例,这样就提高了我们的性能。
3、SingletonHolder这个类是私有的
除了getInstance方法,没有其他的地方能访问他。
code:
package singleton;
public class StaticInnerSingleton {
private StaticInnerSingleton() {
}
public static StaticInnerSingleton getInstance() {
return SingletonHolder.sInstance;
}
//静态内部类
private static class SingletonHolder {
private static final StaticInnerSingleton sInstance = new StaticInnerSingleton();
}
}
利用了内部类的原理,在内部类中去创建对象的实例。这样只要我们的业务中不使用这个内部类StaticInnerSingleton,JVM虚拟机就不会去加载这个类,也就不会去创建我们所要创建的单例对象instance。从而这里的静态内部类就完成了懒汉式的延时加载,同时通过static内部类完成了线程安全。
第一次加载StaticInnerSingleton,不会初始化instance,只有第一次调用getInstance这个方法的时候,虚拟机才会加载SingletonHolder,并初始化我们的instance。这样不仅能确保线程安全,还能确保single类的唯一性。
所以这种写法又简单又能做到懒汉式的延迟加载,同时他也是线程安全的。所以面试的时候,可以不写饿汉,懒汉或者DCL,直接写静态内部类的形式。它实现的重要思想是利用了静态变量的唯一性。
6)枚举
枚举的概念在java5才出现。枚举的单例模式只能在java5之后才能出现
优点:写法简单/线程安全(如果不写自己的实例,枚举的实例默认是线程安全的)
code:
package singleton;
public enum EnumSingleton {
//定义一个枚举的元素,他就是Singleton的一个实例
INSTANCE;
public void doSomeThing() {
}
}
如果只写INSTANCE,那INSTANCE是线程安全的。要是在doSomeThing中写一些实例方法,一定要确保是线程安全的,因为他会影响其他对象的。
默认情况下枚举类是线程安全的,如果要在枚举类中添加自己的实例方法,一定要做好线程安全的操作。
枚举类最大的特点就是它的写法特别的简单,相较于静态内部类还要简单。
2、单例模式-Android中运用
1、Application
application是单例的,而且生命周期是整个Android应用程序中最长的,它的生命周期等同于整个程序的生命周期
code:获取全局的context
public class MusicAppUtils extends Application{
private static Context sContext;
@Override
public void onCreate() {
super.onCreate();
sContext = this;
}
public static Context getContext(){
return sContext;
}
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)