单例概念
单例模式是一种对象创建模式,它用于产生一个对象的具体实例,它可以确保整个系统中一个类只产生一个实例
好处
1. 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销
2. 由于new操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻GC压力,缩短GC停顿时间
单例模式的核心在于通过一个接口返回唯一的对象实例,把创建实例的权利收回来,让类自身来创建自己的实例工作,然后由这个类提供外部可以访问这个类实例的方法
单例的六种写法和各自特点
1. 饿汉
特点:类在加载的时候就直接初始化了实例,所以饿汉式是线程安全的单例模式
缺点:无法实现延迟加载
2. 懒汉
特点:用到这个实例的时候才去实列化
在getInstance()里面判断instance有没有被实例化,在单线程下的确做到了延迟加载和单例,但在多线程环境下是有问题的,如果两个线程在同一时间都通过了判空,那么就会实例两次
缺点:没有同步,多线程环境下无法保证实例是唯一的
3. 懒汉线程安全模式
特点:用到这个实例的时候才去实例化,但是把整个方法都同步了,效率低下
在getInstance()方法上加上同步,使得同一时刻只有一个线程能够访问这个方法,那么只要有一个线程完成实例化以后,后面的线程都不会再次实例化,虽然它保证了多线程环境下只能后实例一次,但是由于synchronized的排他性,导致多线程环境下性能低下
缺点:每次只有一个线程读取实例,性能低下
4. DCL(double check lock)双重检查锁设计模式
特点:双重非空判断,new对象前加一次锁
单例模式的双重锁为什么要加volatile?
在Java多线程的开发中有三种特性:原子性、可见性和有序性。
原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行,就好比你做一件事,要么不做,要么做完。java提供了很多机制来保证原子性。我们举一个例子,比如说常见的a++就不满足原子性。这个操作实际是a = a + 1;是可分割的。在运行的时候可能做了一半不做了。所以不满足原子性。
为了解决a++出现的问题,java提供了很多其他的关键字和类,比如说AtomicInteger、AtomicLong、AtomicReference等。
可见性就是指当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改。
每一个线程都有一份自己的本地内存,所有线程共用一份主内存。如果一个线程对主内存中的数据进行了修改,而此时另外一个线程不知道是否已经发生了修改,就说此时是不可见的。
这种不可见的状况会带来一个问题,两个线程有可能会操作同一份但是值不一样的数据。这时候怎么办呢?可以使用volatile关键字。
volatile关键字的作用很简单,就是一个线程在对主内存的某一份数据进行更改时,改完之后会立刻刷新到主内存。并且会强制让缓存了该变量的线程中的数据清空,必须从主内存重新读取最新数据。这样一来就保证了可见性
程序执行的顺序按照代码的先后顺序执行就叫做有序性,但是有时候程序的执行并不会遵循,比如说下面的代码:
int i = 0;int j = 2;
这两行代码很简单,i=1,j=2,程序在运行的时候一定会先让i=1,然后j=2嘛?不一定,为什么会不一定,这是因为有可能会发生指令重排序
用到volatile关键字,主要是保证内存可见性和防止指令重排序
volatile不保证原子性。所以它是非线程安全的。
我们把test2 = new Test2()这行代码进行拆分。可以分解为3个步骤:
(1)分配内存
(2)初始化对象
(3)设置test2 指向刚分配的地址
如果没有volatile关键字,可能会发生指令重排序。在编译器运行时,从1-2-3 排序为1-3-2。此时两个线程同时进来的时候出现可见性问题,也就是说一个线程执行了1-3,另外一个线程一进来直接返回还未执行2的null对象。而我们的volatile关键之前已经说过了,可以很好地防止指令重排序。也就不会出现这个问题了。
这里使用了双重检查机制,当两个线程都通过了 instance的判空,进入下一个判断时,只有一个线程能抢到锁进入同步块而完成实例化,完成实例化以后,其他的线程都不能通过第一次 instance的判空,并不会像上一个模式一样,每次调用getInstance()方法时,都要进行同步,大大提高了多线程效率。
懒汉模式中的缺点:JVM的即时编译器中存在指令重排序的优化(volatile)优化
优化:静态内部类/枚举
5.静态内部类单例设计模式
优点:
1. jvm本身机制保证了线程安全,因为static/final 初始化之后无法被修改
2. 没有性能缺陷,因为没有使用synchronized关键字
3. 实现了懒汉式的延迟加载
首先通过static进行区块的初始化数据,保证了数据在内存中是唯一的;然后通过final初始化之后无法被修改。这就是JVM提供给我们的同步机制
利用JVM进行类加载的时候会保证数据同步,所以说就利用了内部类的原理,在内部类中去创建实例。这样做的好处是,假如我们的业务中不使用这个内部类,JVM就不会去加载这个内部类,也就不会去创建这个外部类的实例,这样静态内部类就完成了懒汉式的延迟加载
静态内部类单例实现思路中最重要的一点是利用了类中静态变量的唯一性;synchronized虽然能保证了线程的安全,但是非常耗性能,因为它每次只能有一个线程去读取数据,而静态内部类则不一样,它可以同时去读取;将静态内部类私有化,只能通过getInstance()去访问它
6.枚举单例设计模式(java5之后)
优点:写法简单/线程安全
直接通过Singleton.INSTANCE.doSomething()的方式调用即可
网友评论