关于单例模式

栏目: 后端 · 发布时间: 7年前

内容简介:单例模式无论是在实际项目开发还是面试中,都是经常会涉及到,今天总结一下什么样的单例模式才是正确的。懒汉式,也是最想当然的单例方式,线程不安全,可以从以下运行结果看出,线程并发访问这种单例类时,会初始化多个实例,违反了单例类的原则,如果在两个线程start的代码中间加入线程休眠时间,这样后运行的线程才能拿到先运行线程创建的单例对象。通过将整个getInstance方法设为同步的,来保证每次只能有一个线程进入到创建/获取实例的方法内,虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只

单例模式无论是在实际项目开发还是面试中,都是经常会涉及到,今天总结一下什么样的单例模式才是正确的。

1. 存在问题的单例模式

1.1 线程不安全的懒汉式

/**
 * Created by zhoujunfu on 2016/8/24.
 * 线程不安全的懒汉式单例
 */
class SingletonLazyNonThreadSafe {

    private static SingletonLazyNonThreadSafe instance;

    private SingletonLazyNonThreadSafe() {
        System.out.println("初始化单例对象:" + this.hashCode());
    }

    public static SingletonLazyNonThreadSafe getInstance() {
        if (instance == null) {
            instance =  new SingletonLazyNonThreadSafe();
        }

        System.out.println("获取单例对象:" + instance.hashCode());
        return instance;
    }

}

class Runner implements Runnable {

    @Override
    public void run() {
        SingletonLazyNonThreadSafe.getInstance();
    }
}

public class SingletonDemo {

    public static void main(String[] args) throws InterruptedException {
        // 两个线程并发访问单例类创建实例
        Runner runnerOne = new Runner();
        Runner runnerTwo = new Runner();

        Thread threadOne = new Thread(runnerOne);
        Thread threadTwo = new Thread(runnerTwo);

        threadOne.start();
        threadTwo.start();
    }

}
复制代码

懒汉式,也是最想当然的单例方式,线程不安全,可以从以下运行结果看出,线程并发访问这种单例类时,会初始化多个实例,违反了单例类的原则,如果在两个线程start的代码中间加入线程休眠时间,这样后运行的线程才能拿到先运行线程创建的单例对象。

1.2 线程安全的懒汉式

/**
 * Created by zhoujunfu on 2016/8/24.
 * 懒汉式单例
 */
class SingletonLazyThreadSafe {

    private static SingletonLazyThreadSafe instance;

    private SingletonLazyThreadSafe() {
        System.out.println("初始化单例对象:" + this.hashCode());
    }

    public static synchronized SingletonLazyThreadSafe getInstance() {
        if (instance == null) {
            instance =  new SingletonLazyThreadSafe();
        }

        System.out.println("获取单例对象:" + instance.hashCode());
        return instance;
    }

}
class Runner implements Runnable {

    @Override
    public void run() {
        SingletonLazyThreadSafe.getInstance();
    }
}

public class TestSingleton {

    public static void main(String[] args) throws InterruptedException {
        // 两个线程并发访问单例类创建实例
         Runner runnerOne = new Runner();
        Runner runnerTwo = new Runner();

        Thread threadOne = new Thread(runnerOne);
        Thread threadTwo = new Thread(runnerTwo);

        threadOne.start();
        threadTwo.start();
    }

}
复制代码

通过将整个getInstance方法设为同步的,来保证每次只能有一个线程进入到创建/获取实例的方法内,虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。

1.3 双重检验锁

/**
 * Created by zhoujunfu on 2016/8/24.
 * 懒汉式双重检查锁
 */
class SingletonDoubleCheck {
    private SingletonDoubleCheck() {
        System.out.println("初始化单例对象:" + this.hashCode());
    }

    private static SingletonDoubleCheck instance;

    public static SingletonDoubleCheck getInstance() {
        if (instance == null) {
            synchronized (SingletonDoubleCheck.class) {
                if (instance == null) {
                    instance = new SingletonDoubleCheck();
                }
            }
        }
        return instance;
    }
}
复制代码

双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。

这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

1.给 instance 分配内存

2.调用 Singleton 的构造函数来初始化成员变量

3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

但是在 JVM 的即时编译器中存在指令重 排序 的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

我们只需要将 instance 变量声明成 volatile 就可以了。有些人认为使用 volatile 的原因是可见性,也就是可以保证线程在本地不会存有 instance 的副本,每次都是去主内存中读取。但其实是不对的。使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。

但是特别注意在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。

2. 不存在问题的单例模式

2.1 饿汉式(非懒加载)

class SingletonHungry {
    private SingletonHungry() {
        System.out.println("初始化单例对象:" + this.hashCode());
    }

    private static SingletonHungry instance = new SingletonHungry();

    public SingletonHungry getInstance() {
        return instance;
    }
}
复制代码

这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。 这种写法如果完美的话,就没必要在啰嗦那么多双检锁的问题了。缺点是它不是一种懒加载模式(lazy initialization),单例会在加载类后一开始就被初始化,即使客户端没有调用 getInstance()方法。饿汉式的创建方式在一些场景中将无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。

2.2 饿汉式(懒加载)

class SingletonStaticNestedClass {
    private SingletonStaticNestedClass() {
    }

    private static class Holder {
        private static final SingletonStaticNestedClass instance = new SingletonStaticNestedClass();
    }

    public SingletonStaticNestedClass getInstance() {
        return Holder.instance;
    }
}
复制代码

这种写法仍然使用JVM本身机制保证了线程安全问题;由于 Holder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本,但反序列化时会出现问题。

2.3 枚举式(终极方法)

enum SingletonByEnum {
    INSTANCE;
}
复制代码

我们可以通过EasySingleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。但是还是很少看到有人这样写,可能是因为不太熟悉吧。 网络上很多关于单例类的文章都介绍了用枚举法实现单例,但仅仅靠上述的例子还无法知道具体的使用方法,下面以一个具体的例子来说明如何通过枚举实现单例类。

//Example 1
public enum MyDataBaseSource {
    DATASOURCE;
    private ComboPooledDataSource cpds = null;

    private MyDataBaseSource() {
        try {

            /*--------获取properties文件内容------------*/
            // 方法一:
            /*
             * InputStream is =
             * MyDBSource.class.getClassLoader().getResourceAsStream("jdbc.properties");
             * Properties p = new Properties(); p.load(is);
             * System.out.println(p.getProperty("driverClass") );
             */

            // 方法二:(不需要properties的后缀)
            /*
             * ResourceBundle rb = PropertyResourceBundle.getBundle("jdbc") ;
             * System.out.println(rb.getString("driverClass"));
             */

            // 方法三:(不需要properties的后缀)
            ResourceBundle rs = ResourceBundle.getBundle("jdbc");
            cpds = new ComboPooledDataSource();
            cpds = new ComboPooledDataSource();
            cpds.setDriverClass(rs.getString("driverClass"));
            cpds.setJdbcUrl(rs.getString("jdbcUrl"));
            cpds.setUser(rs.getString("user"));
            cpds.setPassword(rs.getString("password"));
            cpds.setMaxPoolSize(Integer.parseInt(rs.getString("maxPoolSize")));
            cpds.setMinPoolSize(Integer.parseInt(rs.getString("minPoolSize")));
            System.out.println("-----调用了构造方法------");
            ;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public Connection getConnection() {
        try {
            return cpds.getConnection();
        } catch (SQLException e) {
            return null;
        }
    }

}
public class Test {
    public static void main(String[] args) {
        MyDataBaseSource.DATASOURCE.getConnection() ;
        MyDataBaseSource.DATASOURCE.getConnection() ;
        MyDataBaseSource.DATASOURCE.getConnection() ;
    }
}
//Example 2
public enum UserActivity {
    INSTANCE;

    private DataSource _dataSource;
    private JdbcTemplate _jdbcTemplate;

    private UserActivity() {
        this._dataSource = MysqlDb.getInstance().getDataSource();
        this._jdbcTemplate = new JdbcTemplate(this._dataSource);
    }

    public void dostuff() {
     ...
    }
}

// use it as ...
UserActivity.INSTANCE.doStuff();
复制代码

Tips: 关于枚举

先看一下枚举类型的实质: 我们定义一个代表不同颜色的枚举类型Color,

public enum Color {
    RED, BLUE, GREEN;
}
复制代码

除了以上的定义方式,我们还可以如下定义,

public enum Color {
    RED(), BLUE(), GREEN();
}
复制代码

到这里你就会觉得迷茫(如果你是初学者的话),为什么这样子也可以?其实,枚举的成员就是枚举对象,只不过他们是静态常量而已。使用 javap 命令(javap 文件名<没有后缀.class>)可以反编译 class 文件,如下

我们可以使用普通类来模拟枚举,下面定义一个 Color 类。

public class Color {
    private static final Color RED = new Color();
    private static final Color GREEN = new Color();
    private static final Color BLUE = new Color();
}
复制代码

对比一下,你就明白了。如果按照这个逻辑,是否还可以为其添加另外的构造方法?答案是肯定的!

public enum Color {
    RED("red color", 0), BLUE("blue color", 1), GREEN("green color", 2);

    Color(String desc, int value) {
        this.desc = desc;
        this.value = value;
    }

    String desc;
    int value;
}
复制代码

为 Color 声明了两个成员变量,并为其构造带参数的构造器。如果你这样创建一个枚举

public enum Color {
    RED("red color", 0), BLUE("blue color", 1), GREEN("green color", 2);
}
复制代码

编译器就会报错,因为没有对应的构造函数。 对于类来讲,最好将其成员变量私有化,然后,为成员变量提供 get、set 方法。按照这个原则,可以进一步写好 enum Color.

public enum Color {
    RED("red color", 0), BLUE("blue color", 1), GREEN("green color", 2);

    Color(String desc, int value) {
        this.desc = desc;
        this.value = value;
    }

    private String desc;
    private int value;

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}
复制代码

但是,java 设计 enum 的目的是提供一组常量,方便用户设计。如果我们冒然的提供 set 方法(外界可以改变其成员属性),好像是有点违背了设计的初衷。那么,我们应该舍弃 set 方法,保留 get 方法。

public enum Color {
    RED("red color", 0), BLUE("blue color", 1), GREEN("green color", 2);

    Color(String desc, int value) {
        this.desc = desc;
        this.value = value;
    }

    private String desc;
    private int value;

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}
复制代码

普通类,我们可以将其实例化,那么,能否实例化枚举呢?在回答这个问题之前,先来看看,反编译之后的 Color.class 文件

public enum Color {
    RED("red color", 0), BLUE("blue color", 1), GREEN("green color", 2);

    private Color(String desc, int value) {
        this.desc = desc;
        this.value = value;
    }

    private String desc;
    private int value;

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}
复制代码

可以看出,编译器淘气的为其构造方法加上了 private,那么也就是说,我们无法实例化枚举。所有枚举类都继承了 Enum 类的方法,包括 toString 、equals、hashcode 等方法。因为 equals、hashcode 方法是 final 的,所以不可以被枚举重写(只可以继承)。但是,可以重写 toString 方法。 那么,使用 Java 的不同类来模拟一下枚举,大概是这个样子

public class Color {
    private static final Color RED = new Color("red color", 0);
    private static final Color GREEN = new Color("green color", 1);
    private static final Color BLUE = new Color("blue color", 2);
    private static final Color YELLOW = new Color("yellow color", 3);

    private final String _name;
    private final int _id;

    private Color(String name, int id) {
        _name = name;
        _id = id;
    }

    public String getName() {
        return _name;
    }

    public int getId() {
        return _id;
    }

    public static List<Color> values() {
        List<Color> list = new ArrayList<Color>();
        list.add(RED);
        list.add(GREEN);
        list.add(BLUE);
        list.add(YELLOW);
        return list;
    }

    @Override
    public String toString() {
        return "the color _name=" + _name + ", _id=" + _id;
    }

}
复制代码

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Practical JavaScript, DOM Scripting and Ajax Projects

Practical JavaScript, DOM Scripting and Ajax Projects

Frank Zammetti / Apress / April 16, 2007 / $44.99

http://www.amazon.com/exec/obidos/tg/detail/-/1590598164/ Book Description Practical JavaScript, DOM, and Ajax Projects is ideal for web developers already experienced in JavaScript who want to ......一起来看看 《Practical JavaScript, DOM Scripting and Ajax Projects》 这本书的介绍吧!

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具