单例模式
介绍
单例模式,是采取一定的方法保证在整个软件系统中,对某个类只能存在一个对象实例,
并且该类只提供一个取得其对象的静态方法。
单例模式实现
饿汉式
- 优点:
- 写法简单,在类装载时完成实例化。避免线程同步问题
- 缺点:
- 没有懒加载效果。当实例一直未使用时,会造成内存浪费
- 下面的两种方法都是使用到了类加载(class loader)的机制,避免了多线程的同步问题。由于导致类加载的原因有多种,
无法确定是否有其他方式导致了类加载,此时如果直接初始化instance,就无法达到懒加载的目的。通俗点说就是没使用getInstance()
方法,但是也使用了类,就会导致该类已经加载了,但是并未在此时获取instance实例。 - 结论:饿汉式的两种(如下),可以使用,但是不推荐,因为可能造成内存浪费。
静态变量实现
/**
* 方法1:使用静态变量的饿汉式单例模式
*/
class Singleton{
//1、私有化构造参数,防止外界使用new初始化
private Singleton() {
}
//2、使用静态变量方式初始化对象实例
private final static Singleton instance = new Singleton();
//3、提供方法供外部获取单例instance
public static Singleton getInstance(){
return instance;
}
}
静态代码块实现
/**
* 方法二:使用静态代码块的饿汉模式
*/
class Singleton {
//1、私有化构造方法
private Singleton() {
}
//2、定义对象实例常量
private final static Singleton instance;
//3、使用静态代码块初始化常量
static {
instance = new Singleton();
}
//4、返回对象实例
public static Singleton getInstance() {
return instance;
}
}
懒汉式
线程不安全
其实,我个人说到单例模式的第一感觉就是使用这个线程不安全的方式去实现,毕竟简单嘛。但是,这种方式有一个严重的问题就是
线程不安全,当线程A进入到if语句内,还未执行new语句的时候,另外一个线程B判断此时的instance为null,也进入了if语句。
此时就会产生多个实例。
- 结论:虽然达到了懒加载的效果,但是只能单线程使用(开发中一般都是多线程的,不能埋下隐患),所以不推荐使用。
//方法3:懒汉模式--线程不安全
class Singleton {
//1、定义类对象
private static Singleton instance;
private Singleton() {
}
//2、懒加载类对象实例
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
同步方法
在上一个方法的基础上,在实例获取getInstance()方法中添加synchronized关键字,同步方法。
- 解决了线程安全问题
- 效率过低(因此不推荐使用):由于获取该类实例时都会进行同步,但是实例化却只需要一次,因此,
后续获取实例都会因为同步导致时间浪费,效率低。
//方法4:懒汉模式--同步方法实现--线程安全
class Singleton {
private static Singleton instance;
private Singleton() {
}
//使用同步方法
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
同步代码块
同上一个
//方法5:懒汉模式--同步代码块--线程安全
class Singleton {
private static Singleton instance;
private Singleton() {
}
//使用同步代码块
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {//静态方法使用Class作为同步锁
instance = new Singleton();
}
}
return instance;
}
}
双重检查
在看到这个方法的时候,由于看漏了volatile关键字,内心便产生了如下疑惑:好像也会出现不安全的问题?
这里先解释下Java的volatile关键字:
当 volatile 用于一个作用域时,Java保证如下:(适用于Java所有版本)读和写一个 volatile 变量有全局的排序。
也就是说每个线程访问一个 volatile 作用域时会在继续执行之前读取它的当前值,而不是(可能)使用一个缓存的值。
(但是并不保证经常读写 volatile 作用域时读和写的相对顺序,也就是说通常这并不是有用的线程构建)。
(适用于Java5及其之后的版本) volatile 的读和写创建了一个happens-before关系,类似于申请和释放一个互斥锁。
使用volatile会比使用锁更快,但是在一些情况下它不能工作。
volatile使用范围在Java5中得到了扩展,特别是双重检查锁定现在能够正确工作。
这里,我们需要知道volatile比使用锁更快(效率高)并且保证写instance总在读instance之前(线程安全)即可。
优缺点:
- 线程安全(进行类两次if检查且instance用volatile声明)
- 第一次实例化后,后续访问if(instance ==null)时直接返回实例化对象,避免反复进行方法同步。
- 兼具效率、安全、懒加载(推荐使用)
//方法6:双重检查--线程安全
class Singleton {
//使用volatile关键字
private static volatile Singleton instance;
private Singleton() {
}
//双重检查机制,在解决线程安全的同时,解决了懒加载,或许这就是双赢吧
//不对,效率也有了保证(volatile),三赢
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
静态内部类实现
同样,先解释下静态内部类作用:
由于静态内部类是不暴露在外面的,所以使用静态内部类时,我们唯一提供静态内部类类加载的
地方也就是getInstance()入口了,并且静态内部类中的INSTANCE使用了final static进行修饰,
只会在第一次类加载时初始化,因此保证了线程安全。
总结:
- 巧妙使用了jvm中类加载以及静态变量的特性,兼具线程安全与懒加载、效率高
- 推荐使用
//方法7:静态内部类实现
class Singleton{
private Singleton() {
}
private static class SingleInstance{
private final static Singleton INSTANCE = new Singleton();
}
public static synchronized Singleton getInstance(){
return SingleInstance.INSTANCE;
}
}
枚举实现
最后一种方法,也是《effective java》作者推荐的方法,使用了枚举类实现单例模式(太巧妙了吧)。
这里,枚举类型是线程安全的,只会装载一次,不会被破坏(其他方法可以通过反射以及反序列化,破坏单例模式,由于这一块并不是十分熟悉,
暂时就不做说明了)。
总结:
- 可以避免线程同步、防止反序列化重新创建新的对象
- 推荐使用
//方法8:枚举类实现
//使用枚举
enum Singleton {
INSTANCE;//属性
public void sayOK() {
System.out.println("ok----");
}
}
说明
- 单例模式保证了系统内存中只存在此类的一个对象,可以节省系统资源,
在需要频繁创建销毁对象时,可以使用单例模式提高性能。 - 需要实例化单例类的时候,需要使用相应的如getInstance()方法获取对象,
而不应该使用new来实例化对象。 - 单例模式运用在需要频繁创建与销毁对象或者是在对象耗时多和资源消耗大时,又
需要频繁使用类对象、工具类对象、数据库或者文件访问对象的场景(常见的有数据源)。 - 这篇文章更多是为了加深了解而写的,如果有错误或者表述不清之处可以评论提醒哦,后续了解了更多jvm底层会进行更新的。