单例模式
# 单例模式
保证在内存中只有一个实例。
# 饿汉式
类加载到内存之后,就实例化一个单例,JVM 保证线程安全。
简单实用,推荐使用
唯一缺点:不管用到与否,类装载时就完成实例化
public class Sg1 {
// 定义一个不可变的静态的实例
private static final Sg1 INSTANCE = new Sg1();
// 代表外界无法使用new
private Sg1() {}
public static Sg1 getInstance() {
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
Sg1 s1 = Sg1.getInstance();
Sg1 s2 = Sg1.getInstance();
// 验证
System.out.println(s1 == s2); // true
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
提示
JVM 保证线程安全:JVM 保证每个类加载到内存只会一次,static
修饰的变量在类load
之后马上就进行初始化,它也保证只初始化一次。
换一种写法,本质上和上面没区别
public class Sg2 {
private static final Sg2 INSTANCE;
static {
INSTANCE = new Sg2();
}
private Sg2() {}
public static Sg2 getInstance() {
return INSTANCE;
}
public static void main(String[] args) {
Sg2 s1 = Sg2.getInstance();
Sg2 s2 = Sg2.getInstance();
System.out.println(s1 == s2);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 懒汉式(lazy loading)
“什么时候我用,什么时候进行初始化”。
即:按需初始化,却带来线程不安全的问题
public class Sg3 {
private static Sg3 INSTANCE;
private Sg3() {
}
public static Sg3 getInstance() {
if (INSTANCE == null) {
/* 测试线程用
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
*/
INSTANCE = new Sg3();
}
return INSTANCE;
}
public static void main(String[] args) {
// 使用多线程进行验证
for (int i = 0; i < 100; i++) {
// lambda 表达式 -> 对只有一个方法的匿名内部类(接口),一个没有名字的对象
new Thread(() -> {
// 同一个类的不同的对象的哈希码是不一样的
// 或者直接打印它的内存地址
System.out.println(Sg3.getInstance().hashCode());
}).start();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
不安全分析
存在一个概率性问题,所以我筛选出一点有问题的结果
1304359947
1013737077
1736549747
414391319 # 这里就不一样了
1270959224
475006753 # 这里也不一样了
1013737077
1013737077
1013737077
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
如果是大多数情况相同的,是因为线程执行的速度太快,所以加了一个线程测试的代码。
# 通过synchronized
来解决懒汉式的问题
注意
但是,使用synchronized
会带来效率下降的问题
public class Sg4 {
private static Sg4 INSTANCE;
private Sg4() {
}
// 锁定的是 Sg4的对象 因为加了static
public static synchronized Sg4 getInstance() {
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Sg4();
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Sg4.getInstance().hashCode());
}).start();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
进一步调整
public class Sg5 {
private static Sg5 INSTANCE;
private Sg5() {
}
public static Sg5 getInstance() {
if (INSTANCE == null) {
// 妄图通过减小同步代码块的方式提高效率,还是不可行
synchronized(Sg5.class) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Sg5();
}
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Sg5.getInstance().hashCode());
}).start();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
这也不能保证线程是安全的
提示
此方法,等于没有加锁,效率还降低了,还是和上面的不安全的差不多的案例,synchronized
块判断等于被多个线程进行拿锁解锁,实例化。
进一步优化,是比较完美的一个,进行双重校验
public class Sg6 {
// volatile JIT compiler 汇编的时候语句重排不加会产生问题,就会在没有初始化的时候就返回 INSTANCE
private static volatile Sg6 INSTANCE;
private Sg6() {
}
public static Sg6 getInstance() {
// 防止多余加锁
if (INSTANCE == null) {
// 双重检查
synchronized(Sg6.class) {
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Sg6();
}
}
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Sg6.getInstance().hashCode());
}).start();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 静态内部类方式(完美的写法之一)
JVM 保证单例
加载外部类时不会加载内部类,这样可以实现懒加载
public class Sg7 { // JVM保证类只加载一次
private Sg7() {}
// 静态的内部类
private static class Sg7Holder { // 这也只加载一次
private final static Sg7 INSTANCE = new Sg7();
}
public static Sg7 getInstance() {
// 返回静态内部类里的 INSTANCE
// 静态内部类在这里才会加载
return Sg7Holder.INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Sg7.getInstance().hashCode());
}).start();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 使用 enum,枚举单例(完美中的完美!) -> EffectiveJava 作者大佬写的
public enum Sg8 {
INSTANCE;
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Sg8.INSTANCE.hashCode());
}).start();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
提示
掌握饿汉式和最后一个可以了,实际选一个用,哪种合适用哪个。
警告
写单例,都会注意一个序列化和反序列化,java 的Class
类通过反射机制可以把类加载到内存,然后进行实例化,可以获取到你的所有东西,这样一来,只有最后一个枚举单例是完美的。
枚举单例为什么是安全的:因为枚举类本身没有构造方法,就算拿到它的类,也没有办法来构造对象;它反序列化后返回的值和单例创造的对象是同一个对象,所以严格来说,枚举单例是最完美的。
编辑 (opens new window)
上次更新: 2021/11/29, 23:47:13