起源
- 在内存中只有一个对象,节省内存空间。避免频繁的创建销毁对象,可以提高性能。避免对共享资源的多重占用。可以全局访问。
- 确保一个类只有一个实例,自行实例化并向系统提供这个实例
问题
- 线程安全问题
- 资源使用问题
实现
方式1:饿汉式
优点:在未调用getInstance() 之前,实例就已经创建了,天生线程安全
缺点:如果一直没有调用getInstance() , 但是已经创建了实例,造成了资源浪费。
//JAVA
public class Singleton {
private static Singleton instance = new Singleton() ;
private Singleton(){
}
public static Singleton getInstance() {
return instance ;
}
}
//C++
class Singleton
{
private:
Singleton(){};
static Singleton* p;
public:
static Singleton* getInstance();
};
Singleton* Singleton::p = new Singleton();
Singleton* Singleton::getInstance()
{
return p;
}
方式2:懒汉式
优点:get() 方法被调用的时候,才创建实例,节省资源。
缺点:线程不安全。
//JAVA
public class Person {
private static Person person ;
private Person(){
}
public static Person get(){
if ( person == null ) {
person = new Person() ;
}
return person ;
}
}
//C++
class Singleton
{
private:
Singleton(){};
static Singleton* p;
public:
static Singleton* getInstance();
};
Singleton* Singleton::p = NULL;
Singleton* Singleton::getInstance(){
if(p == NULL)
p = new Singleton();
return p;
}
-
注意:
这种模式,可以做到单例模式,但是只是在单线程中是单例的,如果在多线程中操作,可能出现多个实例
-
造成的原因:
线程A希望使用Person,调用get()方法。因为是第一次调用,A就发现 person 是null的,于是它开始创建实例,就在这个时候,CPU发生时间片切换,线程B开始执行,它要使用 Person ,调用get()方法,同样检测到 person 是null——注意,这是在A检测完之后切换的,也就是说A并没有来得及创建对象——因此B开始创建。B创建完成后,切换到A继续执行,因为它已经检测完了,所以A不会再检测一遍,它会直接创建对象。这样,线程A和B各自拥有一个 Person 的对象——单例失败!
-
总结:
可以实现单线程单例
多线单例无法保证
-
改进:
加锁
改进:加锁同步(JAVA)
优点:满足单线程的单例,满足多线程的单例
缺点:性能差
//JAVA
//已经可以满足多线程的安全问题了,synchronized修饰的同步块可是要比一般的代码段慢上几倍的!如果存在很多次get()的调用,那性能问题就不得不考虑了!
优点:
public class Person {
private static Person person ;
private Person(){
}
public synchronized static Person get(){
if ( person == null ) {
person = new Person() ;
}
return person ;
}
}
改进:双重校验
优点:满足单线程单例,满足多线程单例,性能问题得以优化
缺点:第一次加载时反应不快,由于JAVA内存模型一些原因偶尔失败
//JAVA
public class Person {
private static Person person ;
private Person(){
}
public static Person get(){
if ( person == null ) {
synchronized ( Person.class ){
if (person == null) {
person = new Person();
}
}
}
return person ;
}
}
//C++
class Singleton{
private:
Singleton()
{
pthread_mutex_init(&mutex);
};
static Singleton* p;
public:
static pthread_mutex_t mutex;
static Singleton* getInstance();
};
Singleton* Singleton::p = NULL;
pthread_mutex_t Singleton::mutex;
Singleton* Singleton::getInstance()
{
if(p == NULL)//多线程性能优化
{
pthread_mutex_lock(&mutex);
if(p == NULL)
p = new Singleton();
pthread_mutex_unlock(&mutex);
}
return p;
}
改进:volatile 关键字
假设没有关键字volatile的情况下,两个线程A、B,都是第一次调用该单例方法,线程A先执行 person = new Person(),该构造方法是一个非原子操作,编译后生成多条字节码指令,由于JAVA的指令重排序,可能会先执行 person 的赋值操作,该操作实际只是在内存中开辟一片存储对象的区域后直接返回内存的引用,之后 person 便不为空了,但是实际的初始化操作却还没有执行,如果就在此时线程B进入,就会看到一个不为空的但是不完整 (没有完成初始化)的 Person对象,所以需要加入volatile关键字,禁止指令重排序优化,从而安全的实现单例。
public class Person {
private static volatile Person person = null ;
private Person(){
}
public static Person getInstance(){
if ( person == null ) {
synchronized ( Person.class ){
if ( person == null ) {
person = new Person() ;
}
}
}
return person ;
}
}
方式3:静态内部类
优点:资源利用率高,不执行getInstance()不被实例,可以执行该类其他静态方法
//JAVA
public class Person {
private Person(){
}
private static class PersonHolder{
/**
* 静态初始化器,由JVM来保证线程安全
*/
private static Person instance = new Person();
}
public static Person getInstance() {
return PersonHolder.instance;
}
}
//C++
class Singleton{
private:
Singleton(){};
public:
static Singleton* getInstance();
};
Singleton* Singleton::getInstance()
{
static Singleton p;
return &p;
}
方式4:枚举类实现单例
获取实例对象:Singleton.INSTANCE
调用其他方法:Singleton.INSTANCE.show();
//JAVA
public enum Singleton {
INSTANCE ;
public void show(){
// Do you need to do things
}
}
public class A1 {
public static void main(String[] args) {
for ( int i = 0 ; i < 20 ; i ++ ) {
new Thread( new Runnable() {
@Override
public void run() {
System.out.println( Singleton.INSTANCE.hashCode() ) ;
}
}).start(); ;
}
}
}
总结
- 上面的方法,都实现了某种程度的单例,各有利弊,根据使用的场景不同,需要满足的特性不同,选取合适的单例方法才是正道。
- 对线程要求严格,对资源要求不严格的推荐使用:饿汉式
- 对线程要求不严格,对资源要求严格的推荐使用:懒汉式
- 对线程要求稍微严格,对资源要求严格的推荐使用:双重加锁
- 同时对线程、资源要求非常严格的推荐使用:volatile 关键字、静态内部类