君临-行者无界

导航

单例模式

  在学习单例模式前,我们首先要了解两个问题。

  1、单例模式有哪些作用

  第一、控制资源的使用,通过线程同步来控制资源的并发访问;第二、控制实例产生的数量,达到节约资源的目的。第三、作为通信媒介使用,也就是数据共享,它可以在不建立直接关联的条件下,让多个不相关的两个线程或者进程之间实现通信。

  2、什么时候需要使用单例

  一个类在应用中如果有两个或者两个以上的实例会引起错误,或者某个类在整个应用中,同一时刻,有且只能有一种状态,则需要被设计为单例。在日常应用中使用的比较多就是配置类,还有我们常用的数据库连接池等都是使用了单例模式。

  java中单例模式的几种的方式

  1、饿汉式

package singleton;

public class HungerSingleton {

    private static HungerSingleton simpleSingleton = new HungerSingleton();
    private HungerSingleton(){};
    public static HungerSingleton getSingleton() {
        return simpleSingleton;
    }
}

  优点:线程安全,访问时速度快,因为项目启动时已经创建好;缺点:项目启动慢,初始化需要占用空间即便没有用到

  2、简单懒汉式

package singleton;

public class SimpleSingleton {

    private static SimpleSingleton simpleSingleton;
    private SimpleSingleton(){};
    
    public static SimpleSingleton getSingleton() {
        if(simpleSingleton== null) {
            simpleSingleton = new SimpleSingleton();
        }
        return simpleSingleton;
    }
}

  优点:使用时才会创建,节省空间,启动速度快,缺点:多线程下容易创建多个实例,造成莫名的错误,通过下面一个简单的例子可以验证

package singleton;

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Client {
    
    private volatile boolean flag;
    public boolean isFlag() {
        return flag;
    }
    public void setFlag(boolean flag) {
        this.flag = flag;
    }


    public static void main(String[] args) throws InterruptedException {
        Set<String> set = new HashSet<>();
        ExecutorService executorService = Executors.newCachedThreadPool();
        Client client = new Client();
        client.setFlag(false);
        for(int i=0;i<100;i++) {
            executorService.execute(new Runnable() {
                
                @Override
                public void run() {
                    while(true) {
                        if(client.isFlag()) {
                            SimpleSingleton s = SimpleSingleton.getSingleton();
                            set.add(s.toString());
                            break;
                        }
                    }
                }
            });
        }
        Thread.sleep(5000);
        client.setFlag(true);
        Thread.sleep(5000);
        System.out.println("并发情况下获取的实例情况");
        for (String string : set) {
            System.out.println(string);
        }
        executorService.shutdown();
    }
}

  运行结果如下:

  3、同步懒汉式

package singleton;

public class SynSingleton {

    private static SynSingleton simpleSingleton;
    private SynSingleton(){};
    
    public static synchronized  SynSingleton getSingleton() {
        if(simpleSingleton== null) {
            simpleSingleton = new SynSingleton();
        }
        return simpleSingleton;
    }
}

  优点:线程安全,启动快,初始化时不占用空间,缺点:同步代码较多,容易造成线程等待

  4、双重检查懒汉式

package singleton;

public class DoubleCheckSingleton {

    private static DoubleCheckSingleton simpleSingleton;
    private DoubleCheckSingleton(){};
    
    public static   DoubleCheckSingleton getSingleton() {
        if(simpleSingleton== null) {
            synchronized(DoubleCheckSingleton.class) {
                if(simpleSingleton== null) {
                    simpleSingleton = new DoubleCheckSingleton();
                }
            }
        }
        return simpleSingleton;
    }
}

  优点:线程安全,启动快,节省初始化空间,效率高,缺点:代码复杂,可能出现未知错误,至于为什么会出现未知错误,那我们就要先了解jvm创建对象的过程。

  jvm创建对象分为三个步骤:1.分配内存2.初始化构造器3.将对象指向分配的内存的地址,这种顺序在上述双重加锁的方式是没有问题的,因为这种情况下JVM是完成了整个对象的构造才将内存的地址交给了对象。但是如果2和3步骤是相反的(2和3可能是相反的是因为JVM会针对字节码进行调优,而其中的一项调优便是调整指令的执行顺序),就会出现问题了,因为这时将会先将内存地址赋给对象,针对上述的双重加锁,就是说先将分配好的内存地址指给synchronizedSingleton,然后再进行初始化构造器,这时候后面的线程去请求getInstance方法时,会认为synchronizedSingleton对象已经实例化了,直接返回一个引用。如果在初始化构造器之前,这个线程使用了synchronizedSingleton,就会产生莫名的错误。

  5、双重检查 + volatile

  这种就是在双重检查的基础上给 simpleSingleton 属性加上volatile关键字,这样便可以禁止jvm指定重排序,即避免上双重检查出现未知错误的情况。

  6、最常用的单例模式,静态内部类

package singleton;

public class StandardSingleton {

    private StandardSingleton(){};
    
    public static StandardSingleton getSingleton() {
        
        return InnerSingleton.standardSingleton;
    }
    
    private static class InnerSingleton{
        static StandardSingleton standardSingleton = new StandardSingleton();
    }
}

  关于这种方式,有人会问会不会和双重检查有一样的问题,答案是不会,因为jvm在加载静态属性是内部实现了同步,所以不用考虑多线程下的问题。

 

posted on 2018-11-27 10:18  请叫我西毒  阅读(292)  评论(0编辑  收藏  举报