发布对象
发布对象:是指使一个对象能够被当前范围之外的代码所使用。
对象逸出:一种错误的发布。当一个对象还没有构造完成时,就使它被其他线程所见。
我们经常需要发布一些对象例如通过类的非私有方法返回对象的引用,或者通过共有静态变量发布对象。
简单例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 1@Slf4j
2@NotThreadSafe
3public class UnsafePublish {
4 private String[] states = {"a", "b", "c"};
5 public String[] getStates() {
6 return states;
7 }
8
9 public static void main(String[] args) {
10 UnsafePublish unsafePublish = new UnsafePublish();
11 log.info("{}", Arrays.toString(unsafePublish.getStates()));
12 unsafePublish.getStates()[0] = "d";
13 log.info("{}", Arrays.toString(unsafePublish.getStates()));
14 }
15}
16
17
输出:
1
2
3 115:42:12.937 [main] INFO com.vincent.example.publish.UnsafePublish - [a, b, c]
215:42:12.942 [main] INFO com.vincent.example.publish.UnsafePublish - [d, b, c]
3
为什么要做这个例子?UnsafePublish通过getState()方法发布了一个属性,在类的任何外部线程都可以访问这个属性,这样发布对象是不安全的,因为我们无法确定其他线程会不会修改这个属性,从而其他线程访问这个属性时,它的值是不确定的,这样发布的对象就是线程不安全的。
对象逸出
先看一个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 1@Slf4j
2@NotThreadSafe
3@NotRecommend
4public class Escape {
5 private int thisCannBeEscape = 0;
6 public Escape() {
7 new InnerClass();
8 }
9 private class InnerClass {
10 public InnerClass() {
11 log.info("{}",Escape.this.thisCannBeEscape);
12 }
13 }
14
15 public static void main(String[] args) {
16 new Escape();
17 }
18}
19
20
这个例子中定义了一个内部类InnerClass,这个内部类的中引用了外部类的一个引用,有可能在对象没有正确被构造之前就被发布,有可能有不安全的因素在里面。
安全的发布对象
- 在静态初始化函数中初始化一个对象引用
- 将对象的引用保存到volatile类型域或者AtomicReference对象中
- 将对象的引用保存到某个正确构造对象的final类型域中
- 将对象的引用保存到一个由锁保护的域中
懒汉模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 1/**
2 * 懒汉模式
3 * 单例的实例在第一次使用时进行创建
4 */
5public class SingletonExample1 {
6 //私有构造函数
7 private SingletonExample1(){
8
9 }
10 //单例对象
11 private static SingletonExample1 instance = null;
12 //静态工厂方法
13 public static SingletonExample1 getInstance(){
14 if(instance == null){
15 instance = new SingletonExample1();
16 }
17 return instance;
18 }
19}
20
上面这段代码在单线程的情况下没有问题,在多线程的情况下会有问题,问题出现在代码if(instance ==null){instance = new SingletonExample1();}return instance;部分。两个线程可以同时访问这段代码,因此这段代码有可能会被调用两次,这样两个线程拿到的实例可能是不一样的。这样写是线程不安全的。
懒汉模式-线程安全
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 1@ThreadSafe
2@NotRecommend
3public class SingletonExample3 {
4 //私有构造函数
5 private SingletonExample3(){
6
7 }
8 //单例对象
9 private static SingletonExample3 instance = null;
10 //静态工厂方法
11 public static synchronized SingletonExample3 getInstance(){
12 if(instance == null){
13 instance = new SingletonExample3();
14 }
15 return instance;
16 }
17}
18
这样的懒汉模式是线程安全,但是却带来了性能上的开销。而这个开销是我们不希望的。因此并不推荐这种写法。
懒汉模式-双重同步锁单例模式
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
35
36
37
38
39
40 1
2/**
3 * 懒汉模式 -> 双重同步锁单例模式
4 * 单例的实例在第一次使用时进行创建
5 */
6@NotThreadSafe
7@ThreadSafe
8@NotRecommend
9public class SingletonExample4 {
10 //私有构造函数
11 private SingletonExample4(){
12
13 }
14 //单例对象
15 private static SingletonExample4 instance = null;
16
17 //正常的执行步骤
18 //1.memory = allcate() 分配对象的内存空间
19 //2.ctorInstance()初始化对象
20 //3.instance = memory 设置instance指向刚分配的内存
21
22 // JVM 和CPU优化,发生了指令重排
23 //1.memory = allcate() 分配对象的内存空间
24 //3.instance = memory 设置instance指向刚分配的内存
25 //2.ctorInstance()初始化对象
26
27 //静态工厂方法
28 public static SingletonExample4 getInstance(){
29 if(instance == null){ //双重检测机制
30 synchronized (SingletonExample4.class) { //同步锁
31 if (instance == null) {
32 instance = new SingletonExample4();
33 }
34 }
35 }
36 return instance;
37 }
38}
39
40
1
2
3
4
5
6
7
8
9 1正常的执行步骤
21.memory = allcate() 分配对象的内存空间
32.ctorInstance()初始化对象
43.instance = memory 设置instance指向刚分配的内存
5 上面的步骤在单线程的情况下没有问题,而在多线程情况下JVM 和CPU优化,发生了指令重排
61.memory = allcate() 分配对象的内存空间
73.instance = memory 设置instance指向刚分配的内存
82.ctorInstance()初始化对象
9
发生指令重排导致双重检测机制线程不安全。因此可以限制不让CPU发生指令重排。可以使用volatile关键字限制指令重排。
1
2 1private static volatile SingletonExample5 instance = null;
2
这样就可以实现线程安全了。
因此volatile关键字有两个使用场景。1.状态标示量。2.双重检测。这里就是一个双重检测的应用。
饿汉模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 1/**
2 * 饿汉模式
3 * 单例的实例在类装载时进行创建
4 */
5@ThreadSafe
6public class SingletonExample2 {
7 //私有构造函数
8 private SingletonExample2(){
9
10 }
11 //单例对象
12 private static SingletonExample2 instance = new SingletonExample2();
13 //静态工厂方法
14 public static SingletonExample2 getInstance(){
15 return instance;
16 }
17}
18
如果单例类的构造函数中,没有过多的操作处理,饿汉模式还可以接受。饿汉模式有什么不足呢?如果构造函数中存在过多的处理,会导致这个类在加载的过程中特别慢,因此可能存在性能问题,如果使用饿汉模式的话只进行类的加载却没有实际的调用的话,会造成资源的浪费。因此使用饿汉模式时一定要考虑两个问题,1.私有构造函数中没有太多的处理。2.这个类肯定会被使用,不会带来资源的浪费。饿汉模式属于线程安全的。
饿汉模式2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 1@ThreadSafe
2public class SingletonExample6 {
3 //私有构造函数
4 private SingletonExample6(){
5
6 }
7 static {
8 instance = new SingletonExample6();
9 }
10 //单例对象
11 private static SingletonExample6 instance = null;
12 //静态工厂方法
13 public static SingletonExample6 getInstance(){
14 return instance;
15 }
16
17 public static void main(String[] args) {
18 System.out.println(getInstance());
19 System.out.println(getInstance());
20 }
21}
22
这时打印出来的值为null
需要把private static SingletonExample6 instance = null;代码写在static语句块之前:
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 1/**
2 * 饿汉模式
3 * 单例的实例在类装载时进行创建
4 */
5@ThreadSafe
6public class SingletonExample6 {
7 //私有构造函数
8 private SingletonExample6(){
9
10 }
11 //单例对象
12 private static SingletonExample6 instance = null;
13 static {
14 instance = new SingletonExample6();
15 }
16
17 //静态工厂方法
18 public static SingletonExample6 getInstance(){
19 return instance;
20 }
21
22 public static void main(String[] args) {
23 System.out.println(getInstance());
24 System.out.println(getInstance());
25 }
26}
27
当我们在写静态属性和静态代码块时要注意顺序。
枚举模式
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 1/**
2 * 饿汉模式
3 * 单例的实例在类装载时进行创建
4 */
5@ThreadSafe
6@Recommend
7public class SingletonExample7 {
8 //私有构造函数
9 private SingletonExample7(){
10
11 }
12 public static SingletonExample7 getInstance(){
13 return Singleton.INSTANCE.getInstance();
14 }
15 private enum Singleton {
16 INSTANCE;
17 private SingletonExample7 singleton;
18
19 // JVM保证这个构造方法绝对只调用一次
20 Singleton() {
21 singleton = new SingletonExample7();
22 }
23 public SingletonExample7 getInstance() {
24 return singleton;
25 }
26
27 }
28
29}
30
枚举模式相比于懒汉模式在安全性方面更容易保证,其次相比于饿汉模式可以在实际调用时才初始化,而在后续使用时也可以取到里面的值。不会造成资源浪费。