1、什么是单例呢?顾名思义单例就是单个实例,单个对象的意思。
2、单例设计模式的功能:可以保证一个类只生成一个类的实例,也就是说在整个程序空间里只存在一个该类的实例。
一、案例引申
1、栗子
如上图:首先我们创建了一个Person类,紧接着我们写个测试类 调用他 打印其对象的内存地址。
2、思考-------设计单例
观察结果发现三个内存地址都不同,此时你会发现我们每次都 “new” 了下,这样导致每次都会生成新的对象,他们为Person类的不同实例。然而这不符合我们单例的需求嘿嘿,如果我们能够控制类的new就行了,让他就new一次。
思考:
java还向我们开发者提供了几种权限修饰符 public 、private、 protected 而平时我们所使用的成员一般都设为了private,为什么我们不把构造函数也设置为私有的呢?这样Person类的new操作就由我们控制了,我们可以在Person类的内部提供一个public 的方法向外部只提供一个此类的实例。就给他提供一个此类的实例。
然而问题又来了如何使类的实例只有一份呢?我们知道java 提供了static修饰符,此修饰符修饰的成员为类所有 在内存中只存在一份,而平时我们不加static修饰的成员为对象所有。(如下图)
上图中a 为Person类所有 在Person类 被程序执行加载时就初始化了。而b为对象所有,在你 new Person 时才被初始化。
二、整理单例写法
写法:
1、私有构造函数
2、提供本类类型的私有静态对象
3、提供方法暴露对象
单例的写法分类:
- 饿汉式
- 懒汉式
- DCL(双重检测)
- 静态内部类方式(java并发实战推荐)
1、饿汉式
package pattern_singleton;
/**
* Create by SunnyDay on 2019/04/08
*/
public class Person {
// 提供私有静态对象
private static Person person = new Person();
// 私有构造函数
private Person() {
}
// 暴露方法 提供对象
public static Person getInstance() {
return person;
}
}
这样的写法就简单的完成了单例设计模式,其实这种方式叫饿汉式。
经过一系列的分析 我们会简单的书写设计模式了 但是你会发现 饿汉式的单例有一个问题,由于我们直接在声明静态成员的时候就给他赋了初始值这样会在类加载时候他就初始化了。然而我们如果想这样做:在我们第一次调用getInstance 这个方法时才被初始化,所以懒汉单例就出来了。(如下)
2、懒汉式
package pattern_singleton.lanhan;
/**
* Create by SunnyDay on 2019/04/08
*/
public class Person {
private static Person person;
private Person() {}
public static Person getInstance() {
if (person==null) {
person = new Person();
}
return person;
}
}
正两种方式看似差不多但是在多线程中就出现问题了(解释如下图)
饿汉式线程安全
懒汉式非线程安全
懒汉:
饿汉:
解释:
我们知道引发线程安全问题的原因:
1、存在两个或者两个以上的线程对象共享同一个资源
2、多线程操作共享资源代码有多个语句
看到这结合图我们就知道为啥懒汉引发线程安全了吧。
解决方案:方法锁或者同步代码块
使用方法锁:
简单极啦,一个关键字解决。
思考弊端:
当线程1,2,3 争夺资源 假如1 获得了使用权,有了进入方法操作资源的权限,这时2,3 等等更多的线程只有等待线程1执行晚锁中的方法,这时再一起争夺CPU使用权,也就是说一次只有一个线程操作资源,安全是安全了,但是其他线程都必须等待,这样造成了资源等待。降低了性能。
这时我们该怎么解决呢?使用锁代码块 +双重检测技术。(如下)
3、DCL双检查锁机制(DCL:double checked locking)
我们使用代码块是不会如上图这样(红圈圈的)吧整块锁完,而是只锁new的那一句,这样能够减少资源的等待。于是加锁代码块:
分析:此时线程1,2,3来到了if判断,假如1,2,同时间隔时间很小,通过了判断,1,2 开始争夺锁代码的使用权,此时1 获得了使用权,进入申请实例,返回了新的对象,由于1,2,几乎同时经过了锁外面的判断,线程1执行完方法后 线程2 假如正好争夺到使用权,开始进入锁代码块,由于2和1一样经过了if 判断 不为null 故也会申请个实例,我操,又不安全了,好吧 放大招 双重检测。
双重检测:
package pattern_singleton.lanhan;
/**
* Create by SunnyDay on 2019/04/08
*/
public class Person {
private volatile static Person person;
private Person() {
}
public static synchronized Person getInstance() {
if (person == null) {
synchronized ("锁") {
if (person == null) {
person = new Person();
}
}
}
return person;
}
}
分析DCL:
还是三个线程 我们模拟下运行.(ps:上图少加了volatile关键字 我们源码加了这里更正下)
假如还是1,2 通过了锁代码块外层判断,假如1获得了cpu使用权,进入操作代码块,里面还有判断,判了下还是null,申请实例,刚刚执行完new代码,2来了结果1还持有锁,2等待,1又获得了使用权,继续执行然后return 对象,正好此时2获得了使用权,进入所代码块,结果又碰见了if哎,判断此时person不为null了,哎只有使用存在的实例了。
4、静态内部类方式的单例
package pattern_singleton;
/**
* Create by SunnyDay on 2019/04/08
*/
public class SingletonDemo3 {
// 单利设计模式 静态内部类单利 (推荐使用)
private SingletonDemo3(){}
public static class InnerClass{
private static SingletonDemo3 singletonDemo3 = new SingletonDemo3();
public static SingletonDemo3 getInstance(){
return singletonDemo3;
}
}
}
三、补充:上锁技巧
上锁的技巧:锁对象必须惟一。
1、 同步函数的锁对象 :
如果为静态的方法,锁对象为当前方法所属类的字节码文件(class 对象),如果为非静态方法,锁对象是this对象。
可见方法锁要慎用。一般设置方法为静态。这样才能保证唯一。
2、 锁代码块的使用:
synchronized(obj){}
obj 为锁对象 一般我们设为字符串而且使用“xxx” 二不是new String(“xxx”),“xxx”存在字符串常量池中,保证内存中只有一份,对象惟一,任何对象都可以设置为锁对象。但是建议使用“xxx”,不容易出错哈!
四、小结
简单的总结一下
更多设计模式阅读