单例模式有八种写法,不是说设计模式是表明了最佳的实践吗,这一下冒出八种写法,何谈最佳?java
每一种单例的写法基本上均可以破坏其单例的属性,这就带来了安全隐患,因此每一种写法都是在以前的基础上进行增强,可是比消此涨,这会增长空间复杂度或者时间复杂度,没有最优的用法,只有最合适的用法。设计模式
这八种里面有些方法是重复的,只是写法稍变一下,重复的就不讲解了,每种方法经过多线程并发进行测试,经过构造方法执行的次数查看建立对象的数量安全
饿汉单例,顾名思义,饿汉就是比饿,比较饥渴,比较冲动,一上来就进入主题,建立对象。markdown
- 在类加载时就建立了单例对象
- 只建立一个实例,绝对线程安全,在线程还没出现之前就是实例化了,不可能存在访问安全问题,JVM保证线程安全
- 优势:没有加任何锁,执行效率高,线程安全
- 缺点:无论你是否用到,类装载的时候就完成实例化,浪费内存空间(空间换时间)
public class Hungry {
// 私有构造器
private Hungry(){
System.out.println(Thread.currentThread().getName());
}
// 指向本身实例的私有静态引用
private static final Hungry hungry = new Hungry();
// 以本身实例为返回值的静态的公有方法
public static Hungry getInstance(){
return hungry;
}
}
// 多线程并发测试
@Test
public void TestMain1(){
for (int i = 0;i < 10;i++){
new Thread(() -> {
Hungry.getInstance();
}).start();
}
}
复制代码
懒汉模式,就是比较懒,作事比较拖拉,在调用实例方法的时候才去实例化对象,一开始我就不给你 new 对象,你来找我,我再给你建立一个对象多线程
- 在调用实例方法的时候才去实例化对象
- 在
getInstance
方法中线程不安全,会建立了多个实例- 优势:一直没人用的话,就不会建立实例,节约内存空间
- 缺点:浪费判断时间,下降效率(时间换空间),线程不安全
public class LazyMan {
// 私有构造器
private LazyMan(){
System.out.println(Thread.currentThread().getName());
}
// 指向本身实例的私有静态引用
private static LazyMan lazyMan = null;
// 以本身实例为返回值的静态的公有方法
public static LazyMan getInstance(){
if(lazyMan == null){
lazyMan = new LazyMan();
}
return lazyMan;
}
}
// 多线程并发测试
@Test
public void TestMain2(){
for (int i = 0;i < 10;i++){
new Thread(() -> {
LazyMan.getInstance();
}).start();
}
}
复制代码
方法二存在线程不安全的隐患,咱们能够经过加锁来进行增强,方法声明上加上 synchronized,同步方法并发
- 和方法二同样,在调用实例方法的时候才去实例化对象
- 在方法声明上加锁,保证线程安全
- 优势:一直没人用的话,就不会建立实例,节约内存空间,且线程安全
- 缺点:每次建立实例都要判断锁,浪费判断时间,下降效率(时间换空间)
public class LazyManSyn {
// 私有构造器
private LazyManSyn(){}
// 私有静态引用建立对象
private static LazyManSyn lazyManSyn = null;
// 同步方法建立实例对象
public static synchronized LazyManSyn getInstance(){
if (lazyManSyn == null){
lazyManSyn = new LazyManSyn();
}
return lazyManSyn;
}
}
// 多线程并发测试
@Test
public void TestMain3(){
for (int i = 0; i < 10; i++) {
new Thread(()->{
System.out.println(LazyManSyn.getInstance().hashCode());
}).start();
}
}
复制代码
方法三虽然线程安全,但每次建立实例都须要判断锁,效率低,咱们能够经过双重锁来进行增强,让已经有实例对象的时候不进行锁判断,这种方式称之为DCL懒汉式性能
- 和方法二同样,在调用实例方法的时候才去实例化对象
- 经过双重锁来防止多实例(双重判断和锁)
- 第一个 if 判断:提升执行效率,若是对象不为空,就直接不执行下面的程序
- 第二个 if 判断:在第一个 if 判断和 synchronized 锁之间,能够进来多个线程,若是没有第二个 if 判断,一个线程拿到锁 new 对象后释放锁,第二个线程又可以拿到锁 new 对象,经过第二个 if 则能够避免建立多个对象。
- synchronized 锁:经过同步代码块
- 优势:减小了锁的判断,相对于方法三提高了效率,使用双重锁,暂且认为线程安全(后面再说)
- 缺点:其实这个方法仍是存在线程不安全的隐患,在
lazyManDCL = new LazyManDCL();
建立对象的时候,不是一个原子性操做,会出现指令重排的可能,所以也可能建立多个实例
public class LazyManDCL {
// 私有构造器
private LazyManDCL(){}
// 指向本身实例的私有静态引用
private static LazyManDCL lazyManDCL = null;
// 双重检查锁模式
public static LazyManDCL getInstance(){
if(lazyManDCL == null){
synchronized (LazyManDCL.class){
if(lazyManDCL == null){
lazyManDCL = new LazyManDCL();
}
}
}
return lazyManDCL;
}
}
// 多线程并发测试
@Test
public void TestMain3(){
for (int i = 0;i < 10;i++){
new Thread(() -> {
System.out.println(LazyManDCL.getInstance().hashCode());
}).start();
}
}
复制代码
在DCL单例模式中,虽然可以经过双重锁保证必定的线程安全性,可是在 new 对象的时候,非原子性操做形成指令重排,执行
new LazyManDCL();
的过程以下:测试
- 分配内存空间
- 执行构造方法
- 把这个对象指向这个空间
执行代码时,为了提升性能,编译器和处理器每每会对指令进行重排序,上面的执行顺序多是 1—>2—>3,也多是 1—>3—>2,当执行顺序为 1—>3—>2 时,就会出现以下问题:spa
- A线程执行 1—>3 ,分配了内存空间,把这个对象指向这个空间
- 此时B线程进来,因为A线程指向了这个空间,形成第一个 if 判断为 false,从而直接 return 对象,因为没有初始化对象,就会报错
所以咱们要防止指令重排的现象发生,即:使用
volatile
关键字,代码以下,就是在方法四的基础上加了一个volatile
关键字线程
- 优势:线程安全
- 缺点:多重判断,浪费时间,下降效率
public class LazyManDCL {
// 私有构造器
private LazyManDCL(){}
// 指向本身实例的私有静态引用,使用volatile防止指令重排
private static volatile LazyManDCL lazyManDCL = null;
// 双重检查锁模式
public static LazyManDCL getInstance(){
if(lazyManDCL == null){
synchronized (LazyManDCL.class){
if(lazyManDCL == null){
lazyManDCL = new LazyManDCL();
}
}
}
return lazyManDCL;
}
}
// 多线程并发测试
@Test
public void TestMain4(){
for (int i = 0;i < 10;i++){
new Thread(() -> {
System.out.println(LazyManDCL.getInstance().hashCode());
}).start();
}
}
复制代码
经过静态内部类来建立实例,静态内部类单例模式的核心原理为对于一个类,JVM在仅用一个类加载器加载它时,静态变量的赋值在全局只会执行一次!,
- 优势1:外部类加载时,不会当即加载内部类,所以不占内存
- 优势2:不像DCL那样须要进行多重判断,提高了效率
- 优势3:第一次调用
getInstance()
方法时,虚拟机才加载SingleOne
类,既能保证线程安全,又能保证明例的惟一性,同时也延迟了单例的实例化
public class SingleOne {
// 私有构造方法
private SingleOne(){}
// 经过静态内部类建立实例
private static class InnerClass{
private static final SingleOne SINGLE_ONE = new SingleOne();
}
// 经过内部类返回对象实例
public static SingleOne getInstance(){
return InnerClass.SINGLE_ONE;
}
}
// 多线程并发测试
@Test
public void TestMain5(){
for (int i = 0; i < 10; i++) {
new Thread(()->{
System.out.println(SingleOne.getInstance().hashCode());
}).start();
}
}
复制代码
上面的方法都是在忽略反射的状况下,咱们都知道,反射能够破坏类,可以无视私有构造器,所以,上面的单例均可以使用反射进行破坏,为了解决反射破坏,咱们可使用枚举单例。
- 枚举的特性自己就是单例,在任何状况下都是一个单例
- 直接经过
EnumSingle.INVALID
进行调用- 让JVM来帮咱们保证线程安全和单一实例问题
public enum EnumSingle {
INVALID;
public void doSomething(){}
}
// 多线程并发测试
@Test
public void TestMain6(){
for (int i = 0; i < 10; i++) {
new Thread(()->{
System.out.println(EnumSingle.INVALID.hashCode());
}).start();
}
}
复制代码
单例模式写法有不少种,稍微改动一下可能又是一种,不过最完美的仍是方法七的枚举单例,可是用的最多的仍是第一种,由于简单,易于理解,更适合开发者。其实咱们没有必要拘泥于完美,最合适的才是最好的,用什么方式解决实际问题更合适就用什么方式,不要追求那些没必要要的完美。就像两我的在一块儿,可能他(她)足够完美,你很喜欢,然而却由于种种缘由不是那么合适,喜欢是乍见之欢,久处仍怦然,合适是你来我往,听得懂和聊得来,你是用心动的方法仍是用本身可以信手拈来的方法呢?固然,即喜欢又合适是最好的,不管多难,你也总会遇到即喜欢有合适的,毕竟地球是圆的,只是时间迟早的问题!