一、介绍

单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。

本文给出了Java和Golang的代码实现,即使语言不同,思想是相同的,但考虑到不同语言的特性,会存在些许的差别。

二、Java实现单例模式

Java实现单例模式的方式主要有5种,分别是饿汉模式、懒汉模式、双重检测机制实现、静态内部类实现、枚举类实现。

2.1、饿汉模式

代码

public final class Singleton {
    private static Singleton instance = new Singleton("Foo");
    public String value;

    private Singleton(String value) {
        this.value = value;
    }

    public static Singleton getInstance() {
        return instance;
    }
}

在类加载的时候就对实例进行初始化,没有线程安全问题;获取实例的静态方法没有使用同步,调用效率高;但是没有使用懒加载,如果该实例从始至终都没被使用过,则会造成内存浪费。

总结:线程安全、非懒加载、效率高。

是否推荐:可以使用,但不推荐。

2.2、懒汉模式

2.2.1、非线程安全

代码

public final class Singleton {
    private static Singleton instance;
    public String value;

    private Singleton(String value) {
        this.value = value;
    }

    public static Singleton getInstance(String value) {
        if (instance == null) {
            instance = new Singleton(value);
        }
        return instance;
    }
}

这种实现方式在第一次使用的时候才进行初始化,达到了懒加载的效果,较为节省内存资源。但如果遇到并发情况,会出现线程安全问题。

总结:懒加载,效率低

是否推荐:不推荐。

2.2.2、线程安全

代码

public final class Singleton {
    private static Singleton instance;
    public String value;

    private Singleton(String value) {
        this.value = value;
    }

    public static synchronized Singleton getInstance(String value) {
        if (instance == null) {
            instance = new Singleton(value);
        }
        return instance;
    }
}

线程安全的懒汉模式是比较常见的一种写法。由于获取实例的静态方法用synchronized修饰,所以也没有线程安全的问题;但是,这种写法每次获取实例都要进行同步(加锁),因此效率较低,并且可能很多同步都是没必要的。

总结:线程安全、懒加载、效率低。

是否推荐:可以使用,但不推荐。

2.3、双重检测机制

代码

public final class Singleton {
    private static volatile Singleton instance; //注意volatile修饰符
    public String value;

    private Singleton(String value) {
        this.value = value;
    }

    public static Singleton getInstance(String value) {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(value);
                }
            }
        }
        return instance;
    }
}

在第一次使用的时候才进行初始化,达到了懒加载的效果;

在进行初始化的时候会进行同步(加锁),没有线程安全问题;并且只有第一次进行初始化才进行同步,因此效率较高。

总结:线程安全、懒加载、效率高。

是否推荐:可以使用。

memory = allocate(); // 1、分配对象的内存空间
ctorInstance(memory);// 2、初始化对象
instance=memory;	 // 3、设置instance指向刚才分配的内存地址
memory = allocate(); // 1、分配对象的内存空间
instance=memory;	 // 3、设置instance指向刚才分配的内存地址
ctorInstance(memory);// 2、初始化对象

2.4、静态内部类

代码

public final class Singleton {
    //静态内部类
    private static class SingletonHolder {
        private static final Singleton instance = new Singleton("FOO");
    }

    public String value;

    private Singleton(String value) {
        this.value = value;
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}
SingletonHolder
getInstanceSingletonHolderSingleton

总结:线程安全、懒加载、效率高。

是否推荐:推荐使用

注:《Java Concurrency in Practice》作者Brian Goetz推荐使用的方式

2.5、枚举类

代码

public enum Singleton {
    INSTANCE("FOO");
    public String value;

    private Singleton(String val) {
        this.value = val;
    }
}
enum

枚举类实现是很简洁的一种实现方式,提供了序列化机制,保证线程安全,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。

总结:线程安全、非懒加载、效率高。

是否推荐:推荐使用

注:《Effective Java》作者Joshua Bloch推荐使用的方式。

三、Golang实现单例模式

Golang并不是纯粹意义上的面向对象语言,在实现单例模式上与Java并不完全相同。

实现方式也可分为饿汉式、懒汉式、双重检测,除此之外,也可以利用Golang的sync包实现。

3.1、饿汉模式

这种实现方式在大多数情况下并不推荐使用。

3.1.1、普通实现

代码

package singleton

type singleton struct {
   Val string
}

var instance = &singleton{"FOO"}

func NewInstance() *singleton {
   return instance
}

这种方式的单例对象是在包加载时立即被创建。

3.1.1、init函数实现

代码

type singleton struct {
   Val string
}

var instance *singleton

func NewInstance() *singleton {
   return instance
}

func init() {
   instance = &singleton{"FOO"}
}
init
init

3.2、懒汉模式

3.2.1、非协程安全:

package singleton

type singleton struct {
   Val string
}

var instance *singleton

func NewInstance(v string) *singleton {
   if instance == nil{
      instance = &singleton{Val: v}
   }
   return instance
}

懒汉模式,能够懒加载,但会造成协程不安全。

不推荐使用。

3.2.2、协程安全:

package singleton

type singleton struct {
   val string
}

var (
   instance *singleton
   m        sync.Mutex
)

func NewInstance(v string) *singleton {
   m.Lock()
   defer m.Unlock()
   if instance == nil {
      instance = &singleton{Val: v}
   }
   return instance
}

协程安全,但加锁操作会降低效率。

不推荐使用。

3.3、双重检测

package singleton

type singleton struct {
   Val string
}

var (
   instance *singleton
   m        sync.Mutex
)

func NewInstance(v string) *singleton {
   if instance == nil {
      m.Lock()
      defer m.Unlock()
      if instance == nil {
         instance = &singleton{Val: v}
      }
   }
   return instance
}

在进行初始化的时候会进行加锁,没有线程安全问题,并且只有第一次进行初始化才进行同步,因此效率较高。

可以使用。

3.4、使用sync.once

package singleton

type singleton struct {
	Val string
}

var (
	instance *singleton
	once     sync.Once
)

func NewInstance(v string) *singleton {
	once.Do(func() {
		instance = &singleton{Val: v}
	})
	return instance
}

Once 是一个结构体,在执行 Do 方法的内部通过 atomic 操作和加锁机制来保证并发安全,且 once.Do 能够保证多个 goroutine 同时执行时 &singleton {} 只被创建一次。

其实 Once 并不神秘,其内部实现跟上面使用的双重锁定机制非常类似,只不过把 instance == nil 换成了 atomic 操作,感兴趣的同学可以查看下其对应源码。

推荐使用。

四、总结

方式优点缺点
饿汉式线程安全、效率高非懒加载
非线程安全懒汉式懒加载、效率高非线程安全
线程安全懒汉式线程安全、懒加载效率低
双重检测线程安全、懒加载、效率高
静态内部类(Java)线程安全、懒加载、效率高
枚举(Java)线程安全、效率高非懒加载
使用sync.once (Golang)线程安全、效率高、懒加载

参考