单例模式有两种实现方式,一种是饿汉式,一种是懒汉式。
饿汉式:类加载到内存后,就实例化一个单例,JVM保证线程安全,简单实用,推荐使用!唯一缺点,不管用到与否,类装载时就完成实例化,也就是Class.forName("")加载到内存就会实例化。(不过话又说回来,你如果不用它,你要装载它干啥)。
懒汉式:类加载到内容后,不会实例化一个单例,而是在需要时才实例化,但是实现这个方式需要考虑到一些问题,下面我们来分析。
一、直接初始化
public class MgrTest01 {
private static final MgrTest01 INSTANCE = new MgrTest01();
private MgrTest01() {};
public static MgrTest01 getInstance(){
return INSTANCE;
}
public static void main(String[] args) {
MgrTest01 mgrTest011 = MgrTest01.getInstance();
MgrTest01 mgrTest012 = MgrTest01.getInstance();
System.out.println(mgrTest011 == mgrTest012);
}
}
执行结果:true
二、使用静态语句初始化(本质上和直接初始化没有什么区别)
public class MgrTest02 {
private static final MgrTest02 INSTANCE;
static {
INSTANCE = new MgrTest02();
}
private MgrTest02() {};
public static MgrTest02 getInstance(){
return INSTANCE;
}
public static void main(String[] args) {
MgrTest02 mgrTest011 = MgrTest02.getInstance();
MgrTest02 mgrTest012 = MgrTest02.getInstance();
System.out.println(mgrTest011 == mgrTest012);
}
}
结果:true
一、按需初始化
public class MgrTest03 {
private static MgrTest03 INSTANCE;
private MgrTest03() {};
public static MgrTest03 getInstance() {
if(null == INSTANCE){
try {
Thread.sleep(10);//延迟10毫秒、让问题出现的可能性增大
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new MgrTest03();
}
return INSTANCE;
}
public static void main(String[] args) {
for(int i=0;i<5;i++){
new Thread(() -> System.out.println(MgrTest03.getInstance())).start();
}
try {
Thread.sleep(3000);//延迟3秒、模拟单例INSTANCE已经有值之后再调用getInstance
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<5;i++){
new Thread(() -> System.out.println(MgrTest03.getInstance())).start();
}
}
}
执行结果:获取的实例对象可能会不同(虽然达到了按需初始化的目的,但是却带来了线程不安全的问题)
问题原因:因为在对象还没有创建之前。多个线程同时调用getInstance方法获取实例的时候,可能存在第一个线程进入了if语句,但是还没有来的及执行实例化对象,后面线程也进入了if语句。等到第一个线程实例化之后,虽然这个时候再有线程调用getInstance,不会再进入if语句直接拿对象,但是已经进入if语句的线程又创建了新的对象。(注意:我们可以看到前五次的对象可能不是同一个,但是后五次肯定是同一个了(中间加入延迟是模拟在对象创建之后再调用getInstance的场景),所以这个问题是在对象还没有创建之前,然后有多个线程同时调用getInstance方法可能出现的问题)
二、可以通过synchronized来解决上个问题
public class MgrTest04 {
private static MgrTest04 INSTANCE;
private MgrTest04() {};
public static synchronized MgrTest04 getInstance() {
if(null == INSTANCE){
try {
Thread.sleep(10);//延迟10毫秒、让问题出现的可能性增大
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new MgrTest04();
}
return INSTANCE;
}
public static void main(String[] args) {
for(int i=0;i<5;i++){
new Thread(() -> System.out.println(MgrTest04.getInstance())).start();
}
try {
Thread.sleep(3000);//延迟3秒、模拟单例INSTANCE已经有值之后再调用getInstance
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<5;i++){
new Thread(() -> System.out.println(MgrTest04.getInstance())).start();
}
}
}
执行结果:获取的实例对象都是同一个(可以实现懒加载,并且不会有线程安全的问题,但是效率下降)
但是又引发了另一个问题,每次调用getInstance方法的时候都要加锁,因为调用加了synchronized的方法每次都要去判断有没有申请到这把锁,执行效率就降低了。本来我们只是解决在INSTANCE还没有实例化的时候线程安全问题,而INSTANCE初始化之后调用getInstance方法是不会有线程安全问题的,所以我们只需要在INSTANCE为空的时候才需要加锁获取,已经不为空了就没有必要还加锁获取。
三、通过减小同步代码快的方式提高效率,但是不可行(需要注意)
public class MgrTest05 {
private static MgrTest05 INSTANCE;
private MgrTest05() {};
public static MgrTest05 getInstance() {
if(null == INSTANCE){
synchronized (MgrTest05.class){
try {
Thread.sleep(10);//延迟10毫秒、让问题出现的可能性增大
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new MgrTest05();
}
}
return INSTANCE;
}
public static void main(String[] args) {
for(int i=0;i<5;i++){
new Thread(() -> System.out.println(MgrTest05.getInstance())).start();
}
try {
Thread.sleep(3000);//延迟3秒、模拟单例INSTANCE已经有值之后再调用getInstance
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<5;i++){
new Thread(() -> System.out.println(MgrTest05.getInstance())).start();
}
}
}
执行结果:获取的实例对象可能会不同(虽然减少了同步代码块,但是出现了线程安全问题)
问题原因:和上面讲的懒加载第一种方式问题类似,虽然把实例化INSTANCE对象的代码同步了,但是还是有可能存在第一个线程进入了if语句,然后进入了同步代码块上锁了,但是还没有来的及执行实例化对象,后面线程也进入了if语句,只是被锁在实例化对象语句外面,等到第一个进入同步代码的线程出来后,被锁在外面的线程还是可以进入,然后实例化了新的对象,就出现了上面类似的线程安全问题。
四、通过双重检查来解决上一个问题
public class MgrTest06 {
//做JIT优化的时候会指令重排 加上volatile关键之阻止编译时和运行时的指令重排
private static volatile MgrTest06 INSTANCE;
private MgrTest06() {};
public static MgrTest06 getInstance() {
if(null == INSTANCE){
synchronized (MgrTest06.class){
//双重检查
if(null == INSTANCE){
try {
Thread.sleep(10);//延迟10毫秒、让问题出现的可能性增大
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new MgrTest06();
}
}
}
return INSTANCE;
}
public static void main(String[] args) {
for(int i=0;i<5;i++){
new Thread(() -> System.out.println(MgrTest06.getInstance())).start();
}
try {
Thread.sleep(3000);//延迟3秒、模拟单例INSTANCE已经有值之后再调用getInstance
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<5;i++){
new Thread(() -> System.out.println(MgrTest06.getInstance())).start();
}
}
}
执行结果:获取的实例对象都是同一个(可以实现懒加载,并且不会有线程安全的问题,同时也解决了效率问题)
这样实现了只在INSTANCE为空的时候才需要加锁获取实例,已经不为空了再调用getInstance方法就判断不为空,然后直接获取。
五、静态内部类单例
public class MgrTest07 {
private MgrTest07() {};
private static class MgrTest07Holder{
private static final MgrTest07 INSTANCE = new MgrTest07();
}
public static MgrTest07 getInstance(){
return MgrTest07Holder.INSTANCE;
}
public static void main(String[] args) {
for(int i=0;i<5;i++){
new Thread(() -> System.out.println(MgrTest07.getInstance())).start();
}
try {
Thread.sleep(3000);//延迟3秒、模拟单例INSTANCE已经有值之后再调用getInstance
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<5;i++){
new Thread(() -> System.out.println(MgrTest07.getInstance())).start();
}
}
}
执行结果:(JVM保证单例,虚拟机加载类的时候只加载一次,所以INSTANCE也只会加载一次,同时实现了懒加载,因为加载外部类时不会加载内部类)
六、枚举单例(不仅可以解决线程同步,还可以防止反序列化)
public enum MgrTest08 {
INSTANCE;
public static void main(String[] args){
for(int i=0;i<5;i++){
new Thread(() -> System.out.println(MgrTest08.INSTANCE.hashCode())).start();
}
try {
Thread.sleep(3000);//延迟3秒、模拟单例INSTANCE已经有值之后再调用getInstance
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<5;i++){
new Thread(() -> System.out.println(MgrTest08.INSTANCE.hashCode())).start();
}
}
}
执行结果:
总结:一般使用直接初始化单例的和静态内部类单例的方式就可以了,不过使用枚举单例的方式更好,因为只有枚举单例,不仅可以解决线程安全问题,还可以防止反序列化,主要看实际开发中需不需要考虑到这些问题,来选择哪种方式实现单例就可以了。
序列化的问题:
为什么在做单例的时候要防止这一点?
因为java的反射是通过一个class文件,然后把整个class加载到内存,再把它创建一个实例出来,而除了枚举方式,其它的都可以找到class文件通过反序列化的方式(反射)再创建一个实例出来,如果想让它不能被反序列化需要设置一些变量,过程比较复杂。
枚举单例为什么可以防止反序列化?
因为枚举类没有构造方法(java规定没有构造方法),就算拿到class文件也没有办法实例化一个对象出来,它反序列化只是一个INSTANCE值(当前案例),然后根据这个值来找对象的话,找到的是和单例创建的同一个对象。