1、基本介绍:
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
2、用途:
应用中某个实例对象需要频繁的被访问。
应用中每次启动只会存在一个实例。如账号系统,数据库系统。
3、实现方式:
3.1、lazy instantiaze 懒加载
public class Demo1 {
private Demo1() { }
private static Demo1 demo = null;
public static Demo1 getInstance() {
if (demo == null) { //@1
demo = new Demo1(); //@2
}
return demo;
}
}
- 优点:延迟加载(需要的时候才会加载)
- 缺点:多线程容易出现不同步的问题。
假设这样的场景:两个线程并发调用Demo1.getInstance(),假设线程一先判断完demo是否为null,既代码中的
@1
进入到@2
的位置。刚刚判断完毕后,JVM将CPU资源切换给线程二,由于线程一还没执行@2
,所以demo仍然是空的,因此线程二执行了new
Demo1()操作。片刻之后,线程一被重新唤醒,它执行的仍然是new Demo1()操作。
3.2、饿加载:
public class Demo2 {
private Demo2(){}
private static Demo2 demo2 = new Demo2();
public static Demo2 getInstance() {
return demo2;
}
}
- 优点:不会出现多线程访问问题,依赖JVM在加载这个类的时候马上创建唯一的单例实例,JVM保证在任何线程访问单例变量之前,一定先创建此单例。
- 缺点:
1.如果实例开销较大,而且程序中未使用,性能损耗。
2.如实例的创建是依赖参数或者配置文件的,在getInstance()之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。
3.3、同步锁synchronized:
public class Demo3 {
private Demo3() {
}
private static Demo3 demo3 =null;
public synchronized static Demo3 getInstance() {
if (demo3 == null) {
demo3 = new Demo3();
}
return demo3;
}
}
- synchronized :保证了并发编程的3大特性,迫使每个线程在进入这个方法之前,要先等到别的线程离开该方法,不会有2个线程同时进入到这个方法中。【详细用法见synchronized章节】
- 优点:解决了多线程并发访问的问题。
- 缺点:性能问题。 只有第一次执行此方法的时候才需要同步,一旦设置了demo3变量,就不需要同步这个方法。此后的每次操作都会消耗在同步上。
3.4、双重加锁机制:【最复杂,最难理解,需要提前了解java中的内存模型、Volatile跟synchronized机制】
public class Demo4 {
private Demo4() { }
private volatile static Demo4 demo4 = null;
public static Demo4 getInstance() {
if (demo4 == null) {
synchronized (Demo4.class) {
if (demo4 == null) {
demo4 = new Demo4(); //@1
}
}
}
return demo4;
}
}
- 优点:多线程访问不会出现不同步问题,并且减缓了3.3同步锁单例的性能弊端。
- 难点1:synchronized能保证可见性,为什么单例模式还需要加volatile关键字?
- 解答1:
这里加锁确实能够保证这个对象不会被new两次,但不能保证对象的创建过程不被重排序:
demo4 = new Demo4(); //实例化类@1并不是一个原子操作,这一行代码可以分解为如下的三行伪代码:
memory = allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
demo4 = memory; //3:设置demo4变量指向刚分配的内存地址
上面三行伪代码中的2和3之间,可能会被重排序,造成2中的构造方法操作有可能没有执行完,3中的demo4就拿到了这个值,从而出现异常问题。
假想一个场景:线程1进入到@1位置进行开始实例化操作,由于编译器的指令重排序,它是按照1、3、2步骤进行的;但是在执行完第3步还没有执行到第2步时,demo4变量已经拿到值,指向了堆内存中的地址,此时由于锁的可见性,线程2工作内存中的demo4备份值已经更改,会去主存重新读取demo4的值,并且返回了有值的demo4,但是此时实际上Demo4类并没有完成实例化,Demo4对象并没有生成,所以此时会出现问题。
当声明对象的引用为volatile后,“问题的根源”的三行伪代码中的2和3之间的重排序,在多线程环境中将会被禁止,类在实例化过程中会严格按照1、2、3顺序执行下去。
4、注意点:
• 单例模式不会被jvm垃圾回收的
• 实现单例模式需要私有构造器、一个静态变量、一个静态方法
• 使用多个类加载器时,可能导致单例失效,产生多个实例,解决:自行指定类加载器,并指定同一个类加载器。