单例设计模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点
特点:
1、单例类构造方法私有化(private修饰),只能有一个实例;
2、单例类必须自己创建自己的实例化对象(private static类型);
3、单例类必须给其他类提供这个实例(定义一个静态方法)
优点:
1、避免实例化对象的重复创建,不仅减少每次创建对象的时间开销,还可以节约内存空间;
2、避免由于创建多个实例导致某些值不一致的情况
适用情况:
在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。
1、饿汉式
public class Singleton1 {
//将自身的实例对象设置成一个static final属性
private static final Singleton1 instance = new Singleton1();
//私有构造方法
private Singleton1(){
}
//公共静态方法返回该类的实例
public static Singleton1 getInstance(){
return instance;
}
}
在类加载的时候就创建实例,实例在整个程序周期都存在(直到类卸载)
优点:只在类加载的时候创建一次实例,不会存在多个线程创建多个实例的情况,避免多个线程同步的问题
缺点:类加载之后就会被创建,即使没有用到。线程安全,但效率比较低
饿汉模式都是线程安全的
2、懒汉式
public class Singleton2 {
private static Singleton2 instance;
private Singleton2(){
}
public static Singleton2 getInstance(){
if(instance == null){
instance = new Singleton2();
}
return instance;
}
}
类加载的时候对象不存在,延迟加载方式,需要时再创建。
优点:节约内存,按需创建
缺点:并发环境下可能出现多个实例,线程不安全
3、懒汉式的简单实现
public class Singleton3 {
private static Singleton3 instance;
private Singleton3(){
}
//在getInstance方法上加synchronized同步,但是有开销
public static synchronized Singleton3 getInstance(){
if(instance == null){
instance = new Singleton3();
}
return instance;
}
}
优点:使用synchronized关键字避免多线程访问时,出现多个实例。
缺点:同步方法频繁调用时,效率略低。每次都要同步,影响性能。
4、双重检查锁定
public class Singleton4 {
private static Singleton4 instance;
private Singleton4(){
}
public static Singleton4 getInstance(){
if(instance == null){//1
//只需要在初始化的时候同步,正常的代码执行路径不需要同步
synchronized(Singleton4.class){
if(instance == null){//2第二层校验
instance = new Singleton4();
}
}
}
return instance;
}
}
假如两个线程A、B,A执行了代码1,if (instance == null)语句,它会认为单例对象没有创建,此时还没来得及创建对象,线程切到B也执行了同样的语句,B也认为单例对象没有创建,然后两个线程依次执行同步代码块,并分别创建了一个单例对象。为了解决这个问题,还需要在同步代码块中增加if (instance == null)语句,也就是上面看到的代码【第二层校验】。
只需要在初始化的时候同步,正常的代码执行路径不需要同步。
缺点:会出现指令重排的情况。
指令重排:
在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。JVM中并没有规定编译器优化相关的内容,也就是说JVM可以自由的进行指令重排序的优化。
为了优化指令,提高程序运行效率。指令重排序包括编译器重排序和运行时重排序。
* JVM规范规定,指令重排序可以在不影响单线程程序执行结果前提下进行
5、双重检查锁定+volatile
public class Singleton5 {
private static volatile Singleton5 instance;
private Singleton5(){
}
public static Singleton5 getInstance(){
if(instance == null){
synchronized(Singleton5.class){
if(instance == null){
instance = new Singleton5();
}
}
}
return instance;
}
}
volatile禁止指令重排。volatile的另一个语义是保证变量修改的可见性。
双重检查锁定不是线程安全的,如果要用这种方式,需要使用volatile关键字。
使用volatile保证了多线程访问时instance变量的可见性,避免了instance初始化时其他变量属性还没赋值完时,被另外线程调用
6、静态内部类(延迟初始化占位类模式)
public class Singleton6 {
private static class InstanceHolder{
public static Singleton6 instance = new Singleton6();
}
private Singleton6(){
}
public static Singleton6 getInstance(){
return InstanceHolder.instance;
}
}
该模式引进了一个静态内部类(占位类),在内部类中提前初始化实例,既保证了Singleton实例的延迟初始化,又保证了同步。
这是一种提前初始化(恶汉式)和延迟初始化(懒汉式)的综合模式。
不常用设计模式:
登记式单例:登记式单例实际上维护了一组单例类的实例,将这些实例存放在一个Map(登记薄)中,对于已经登记过的实例,则从Map直接返回,对于没有登记的,则先登记,然后返回。
正确的单例模式有三种:
1、饿汉式---方式一
2、双重检查锁定+volatile---方法五
3、静态内部类(延迟初始化占位类模式)---方法六
线程安全:
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
或者说:一个类或者程序所提供的接口对于线程来说是原子操作,或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题,那就是线程安全的。
双重检查锁定的线程安全
双重检查锁定不是线程安全的,如果要用这种方式,需要使用volatile关键字。
假设没有关键字volatile的情况下,两个线程A、B,都是第一次调用该单例方法,线程A先执行instance = new Instance(),该构造方法是一个非原子操作,编译后生成多条字节码指令,由于JAVA的指令重排序,可能会先执行instance的赋值操作,该操作实际只是在内存中开辟一片存储对象的区域后直接返回内存的引用,之后instance便不为空了,但是实际的初始化操作却还没有执行,如果就在此时线程B进入,就会看到一个不为空的但是不完整(没有完成初始化)的Instance对象,所以需要加入volatile关键字,禁止指令重排序优化,从而安全的实现单例。