发布与逸出
对象发布:
"发布"一个对象的意思是指,使对象能够在当前作用域之外的代码中使用.例如:将一个指向该对象的引用保存到其他代码可以访问的地方,或者在某一个非私有的方法中返回该引用,或者将引用传递到其他类的方法中.
发布一个对象最简单的方法是将对象的引用保存到一个公有的静态变量中,以便任何类和变量都能看见该对象.
public class PublishDemo {
public static Set<String> stringSet;
public void initialize(){
stringSet = new HashSet<>();
}
}
对象逸出:
一种错误的发布,当对象没有构造完成时,就使它被其他线程所见.或者对象的作用域被修改,也是逸出.
先看作用域逸出:
public class UnsafeStates {
private String[] states = new String[]{"AK","AL"};
public String[] getStates(){
return states;
}
}
这种方式发布对象,任何调用者都可以修改这个数组的内容,而原来我们声明states的作用域是私有的,那么这样就不符合我们的预期了.
初始化一半的对象:
package com.springcloud.server.springserver.thread;
public class ThisEscape {
//final常量会保证在构造器内完成初始化(但是仅限于未发生this逃逸的情况下)
final int i;
//尽管实例变量有初始值,但是还实例化完成
int j = 0;
static ThisEscape obj;
public ThisEscape() {
i = 1;
j = 1;
//将this逃逸抛出给线程B
obj = new ThisEscape();
}
public static void main(String[] args) {
//线程A:模拟构造器中this逃逸,将未构造完全对象引用抛出
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
obj = new ThisEscape();
}
});
//线程B:读取对象引用,访问i/j变量
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
//可能会发生初始化失败的情况解释:实例变量i的初始化被重排序到构造器外,此时1还未被初始化
ThisEscape objB = obj;
try {
System.out.println(objB.j);
} catch (NullPointerException e) {
System.out.println("发生空指针错误:普通变量j未被初始化");
}
try {
System.out.println(objB.i);
} catch (NullPointerException e) {
System.out.println("发生空指针错误:final变量i未被初始化");
}
}
});
threadA.start();
threadB.start();
}
}
这里是将对象ThisEscape的初始化交给另外一个线程,由于多线程运行的不确定性,这个初始化工作显然是可能会失败的.
输出结果:
另外一种对象逸出的方式是在构造函数中启动一个线程去进行对象的初始化:
package com.springcloud.server.springserver.thread;
import java.util.concurrent.TimeUnit;
public class ThisEscape2 {
final int i;
int j;
public ThisEscape2() {
i = 1;
j = 1;
new Thread(new RunablTest()).start();
}
//内部类实现Runnable:引用外部类
private class RunablTest implements Runnable{
@Override
public void run() {
try {
System.out.println(ThisEscape2.this.j);
} catch (NullPointerException e) {
System.out.println("发生空指针错误:普通变量j未被初始化");
}
try {
System.out.println(ThisEscape2.this.i);
} catch (NullPointerException e) {
System.out.println("发生空指针错误:final变量i未被初始化");
}
}
}
public static void main(String[] args) {
new ThisEscape2();
}
}
在这里ThisEscape2的构造方法里面起到了一个线程,并且在线程里面使用到了ThisEscape2的成员变量,有可能在ThisEscape2对象还未完全创建完成的时候,新的线程就一个开始访问了,那么这里就是不安全的,也叫做this逃逸.
正确发布对象的方法
线程封闭
使用线程封闭的一个最好的例子就是ThreadLocal类,这个类可以将线程的某个值与保存值的对象关联起来,防止全局变量的共享.此类的讨论暂时不放在本文里面.
不变性
不可变对象一定是线程安全的.
当满足以下条件是,对象才是不可变的:
- 对象创建之后其状态就不能修改.
- 对象的所有域都是final类型.
- 对象是正确创建的(在对象创建期间,this引用没有逸出).
任何线程都可以在不需要额外同步的情况下安全的访问不可变对象,即使在发布这些对象时没有使用同步.
除了不可变对象以外,使用被另外一个线程初始化的对象通常都是不安全的,除非对象的发布操作是在使用该对象的线程开始使用之前执行.
安全发布的常用模式:
可变对象必须通过安全的方式,要安全的发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见,一个正确的构造对象可以通过以下方式来安全的发布:
- 在静态初始化函数中初始化一个对象引用.
- 将对象的引用保存到volatile类型的域或者AtomicReference对象中.
- 将对象的引用保存到某个正确的构造对象的final类型域中.
- 将对象的引用保存到一个由锁保护的域中.
通常,要发布一个静态构造的对象,最简单和最安全的方式是使用静态的初始化器:
public static Holder holder = new Holder(42);
静态初始化器由JVM在类的初始化阶段执行.由于在JVM内部存在着同步机制,因此通过这种方式初始化任何对象都可以被安全的发布.
对象的发布需求取决于它的可变性:
- 不可变对象可以通过任意机制来发布.
- 事实不可变对象必须通过安全方式来发布.
- 可变对象必须通过安全方式来发布.并且必须是线程安全的或者由某个锁保护起来.
不安全的发布模式:
public class UnsafeLazyInitialization {
private static Resource resource;
public static Resource getInstance(){
if (resource == null){
resource = new Resource();
}
return resource;
}
}
假设有两个线程同时调用getInstance方法,A线程先初始化这个resource对象,并初始化里面的属性值.由于没有使用同步,B线程去调用这个方法时,可能看到一个被部分初始化的对象,,实例处于一个无效状态.
除了不可变对象以外,使用被另外一个线程初始化的对象通常都是不安全的,除非对象的发布操作是在使用该对象的线程开始使用之前执行.
安全初始化模式:
上述方法中,我们可以将getInstance方法声明为synchronized,可以解决并发安全的问题:
1. 使用synchronized进行加锁同步
public class SafeLazyInitialization {
private static Resource resource;
public static synchronized Resource getInstance(){
if (resource == null){
resource = new Resource();
}
return resource;
}
}
2.提前初始化
public class EagerInitialization {
private static Resource resource = new Resource();
public static Resource getResource(){
return resource;
}
}
静态初始化是JVM在类的初始化阶段执行,即在类被加载之后而且在线程使用之前.JVM在初始化执行期间将获取一个锁,并且每个线程都至少获取一次这个锁以确保这个类已经加载,因此在静态初始化期间,内存的写入操作对所有的线程可见.因此无论是在构造期间还是被引用时,静态初始化的对象都不需要显示的同步.
3.延迟初始化占位类模式
public class ResourceFactory {
private static class ResourceHolder{
public static Resource resource = new Resource();
}
public static Resource getResource(){
return ResourceHolder.resource;
}
}
上述代码使用了一个专门的ResourceHolder类来初始化Resource.JVM将推迟ResourceHolder的初始化操作,直到开始使用这个类时才初始化,并且由于通过一个静态初始化来初始化Resource,因此不需要额外的同步.
4.双重检查加锁
public class DoubleCheckedLocking {
private static volatile Resource resource;
public static Resource getInstance(){
if (resource == null){
synchronized (DoubleCheckedLocking.class){
if (resource == null){
resource = new Resource();
}
}
}
return resource;
}
}
双重检查加锁的原理是,首先检查是否在没有加锁的情况下需要初始化,如果Resource引用不为空,就直接使用它,否则就加锁同步并在此检查Resource是否被初始化,从而保证只有一个线程对共享的Resource进行初始化.
虽然上述方式也可以安全初始化一个类,但是方式略显复杂,建议使用延迟初始化占位类模式,这种方式更加简单,便于理解.