引入

单例模式同时解决了两个问题:

  1. 保证一个类只有一个实例
  2. 为该实例提供一个全局访问节点:单例模式允许在程序的任何地方访问特定对象。 但是它可以保护该实例不被其他代码覆盖。

单例模式的实现需要包含两个必要的步骤:

  • 将默认的构造函数设为私有,防止其他对象使用单例类的new来新建一个对象;
  • 新建一个静态构建方法来作为构造函数,该函数必要时会调用私有的类构造函数来创建对象并将其保存在一个静态成员函数变量中,此后对所有该函数的返回均返回这个静态成员函数变量;

无论什么时候调用静态构造方法,其返回值都是相同的对象。

一个典型的UML如下所示

design-pattern-singleton-1

使用场景

  1. 如果程序中某个类对于所有客户端都只有一个可用的实例,则可以使用单例模式
  2. 如果需要严格控制全局变量,可以使用单例模式

实现

饿汉模式

饿汉模式在类加载的时候就对实例进行创建,实例在整个程序周期都存在。因此不会存在多线程同步的问题,但是如果实例并未在程序中使用,则会浪费一定的内存

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Singleton1 {
    // 私有构造
    private Singleton1() {}

    private static Singleton1 single = new Singleton1();

    // 静态工厂方法
    public static Singleton1 getInstance() {
        return single;
    }
}

go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package singleton

type singleton struct {
}

var singletonInstance *singleton

func init() {
	singletonInstance = &singleton{}
}

func GetInstance() *singleton {
	return singletonInstance
}

test for go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func TestGetInstance(t *testing.T) {
	if GetInstance() != GetInstance() {
		t.Errorf("instance get failed")
	}
}

func BenchmarkGetInstance(b *testing.B) {
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			if GetInstance() != GetInstance() {
				b.Errorf("parallel get instance failed")
			}
		}
	})
}

懒汉模式

懒汉模式单例只有在需要的时候才去创建,但是存在线程安全的问题,需要考虑其他更安全的方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 懒汉式单例
public class Singleton2 {

    // 私有构造
    private Singleton2() {}

    private static Singleton2 single = null;

    public static Singleton2 getInstance() {
        if(single == null){
            single = new Singleton2();
        }
        return single;
    }
}

go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package singleton

import "sync"

var (
	lazySingleton *singleton
	once          = &sync.Once{}
)

func GetLazyInstance() *singleton {
	if lazySingleton == nil {
		once.Do(func() {
			lazySingleton = &singleton{}
		})
	}
	return lazySingleton
}

test for go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func TestGetLazyInstance(t *testing.T) {
	if GetLazyInstance() != GetLazyInstance() {
		t.Errorf("lazy instance get failed")
	}
}

func BenchmarkGetLazyInstance(b *testing.B) {
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			if GetLazyInstance() != GetLazyInstance() {
				b.Errorf("parallel get lazy failed")
			}
		}
	})
}

对比测试结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
PS E:\github\design_pattern_go\singleton> go test -benchmem -bench="." -v
=== RUN   TestGetInstance
--- PASS: TestGetInstance (0.00s)
=== RUN   TestGetLazyInstance
--- PASS: TestGetLazyInstance (0.00s)
goos: windows
goarch: amd64
pkg: design_pattern_go/singleton
cpu: AMD Ryzen 5 3500X 6-Core Processor
BenchmarkGetInstance
BenchmarkGetInstance-6          1000000000               0.08200 ns/op         0 B/op          0 allocs/op
BenchmarkGetLazyInstance
BenchmarkGetLazyInstance-6      1000000000               0.6276 ns/op          0 B/op          0 allocs/op
PASS
ok      design_pattern_go/singleton     0.836s

饿汉式的性能表现要远远好于懒汉式

线程安全懒汉模式

每次请求的时候,都通过getInstance获取对象,首先判断对象是否为null,如果为null,则先加一个同步锁,然后判断对象是否为null,如果为null则需要调用构造函数实例化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class Singleton4 {
    // 私有构造
    private Singleton4() {}

    private static Singleton4 single = null;

    // 双重检查
    public static Singleton4 getInstance() {
        if (single == null) {
            synchronized (Singleton4.class) {
                if (single == null) {
                    single = new Singleton4();
                }
            }
        }
        return single;
    }
}

问题:Java中存在指令重排优化(在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快)和Happens-Before规则(前一个操作的结果可以被后续的操作获取。这条规则规范了编译器对程序的重排序优化。),指令重排优化的存在,导致初始化Singleton和将对象地址赋给instance字段的顺序是不确定的。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是状态不正确的对象,程序就会出错。

可以通过Java提供的关键字volatile来解决

volatile关键字的作用主要有两个:

  • 多线程主要围绕可见性和原子性两个特性而展开,使用volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,一定是最新的数据;
  • 使用volatile则会对禁止语义重排序

volatile是一个特殊的修饰符,只有成员变量才能使用它。在Java并发程序缺少同步类的情况下,多线程对成员变量的操作对其它线程是透明的。volatile变量可以保证下一个读取操作会在前一个写操作之后发生。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class Singleton4 {
    // 私有构造
    private Singleton4() {}

    private volatile static Singleton4 single = null;

    // 双重检查
    public static Singleton4 getInstance() {
        if (single == null) {
            synchronized (Singleton4.class) {
                if (single == null) {
                    single = new Singleton4();
                }
            }
        }
        return single;
    }
}

静态内部类

静态内部类,序列化对象,默认方式是多例的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package org.mlinge.s06;
 
public class MySingleton {
	
	//内部类
	private static class MySingletonHandler{
		private static MySingleton instance = new MySingleton();
	} 
	
	private MySingleton(){}
	 
	public static MySingleton getInstance() { 
		return MySingletonHandler.instance;
	}
}

这种方式利用了类加载机制来保证只会创建一个instance实例,它与饿汉模式一样,也是利用了类加载机制,因此不存在多线程并发的问题。它与饿汉模式一样,也是利用了类加载机制,因此不存在多线程并发的问题。这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全。