发布是一个动词,是去发布对象。而对象,通俗的理解是:JAVA里面通过 new 关键字 创建一个对象。
发布一个对象的意思是:使对象在当前作用域之外的代码中使用。比如下面knowSecrets指向的HashSet类型的对象,由static修饰,是一个类变量。当前作用域为PublishExample类。
Demo-1:
import java.util.HashSet;
import java.util.Set;
public class PublishExample {
public static Set<Secret> knowSecrets;
public void initialize() {
knowSecrets = new HashSet<>();
}
public修饰引用knowSecrets,导致在其他类中也能访问到这个HashSet对象,比如向HashSet添加元素或者删除元素。因此,也就发布了这个对象。Demo-2代码段相当于使用了被发布的
Demo-2:
public class UsingSecret {
public static void main(String[] args) {
PublishExample.knowSecrets.add(new Secret());
PublishExample.knowSecrets.remove(new Secret());
}
}
另一方面,值得注意的是:被发布对象中所存的引用类对象也会被发布,比如Demo-2代码段中添加的到HashSet集合中的Secret对象也被发布了。
因为对象一般是在构造函数里面初始化的(不讨论反射),当构造一个对象时,会为这个对象的属性赋值,当前时刻对象各个属性拥有的值称为对象的状态。给出一个常见的单线程代码例子:Demo-3
Demo-3:
public class Secret {
private String password;
private int length;
public Secret(){}
public Secret(String password, int length) {
this.password = password;
this.length = length;
}
public static void main(String[] args) {
//"current state" 5 组成了secObjCurrentState对象的当前状态
Secret secObjCurrentState = new Secret("current state", 5);
//改变 secObjCurrentState 对象的状态
secObjCurrentState.setPassword("state changed");
}
public void setPassword(String password) {
this.password = password;
}
}
Secret对象有两个状态(属性):password和length,利用构造方法进行状态的初始化。
利用secObjCurrentState.setPassword("state changed")
进行password状态的修改。
创建对象的目的是使用它,而要用它,就要把它发布出去。同时,也引出了一个重要问题,我们是在哪些地方用到这个对象呢?比如:只在一个线程里面访问这个对象,还是有可能多个线程并发访问该对象?然而在Demo-3代码段中显然没有考虑多线程的安全性问题。
对象被发布后,是无法知道其他线程对已发布的对象执行何种操作的,这也是导致线程安全问题的原因。
先看一个不安全发布的示例----this引用逸出。参考《Java并发编程实战》第3章程序清单3-7。
this逸出主要指我们仍未构造好对象(某些状态并未初始化,或并未按照构造函数指定初始化状态),却将对象发布出去了。在其中可能显式地用到了this关键字,也有可能隐式地发布了this。下面利用Dome-4来说明这个问题。
Demo-4:
public class ThisEscape {
private int intState;//外部类的属性,当构造一个外部类对象时,这些属性值就是外部类状态的一部分
private String stringState;
public ThisEscape(EventSource source) {
source.registerListener(new EventListener(){
@Override
public void onEvent(Event e) {
doSomething(e);
}
});
//执行到这里时,new 的EventListener就已经把ThisEscape对象隐式发布了,而ThisEscape对象尚未初始化完成
intState=10;//ThisEscape对象继续初始化....
stringState = "hello";//ThisEscape对象继续初始化....
//执行到这里时, ThisEscape对象才算初始化完成...
}
/**
* EventListener 是 ThisEscape的 非静态 内部类
*/
public abstract class EventListener {
public abstract void onEvent(Event e);
}
private void doSomething(Event e) {}
public int getIntState() {
return intState;
}
public void setIntState(int intState) {
this.intState = intState;
}
public String getStringState() {
return stringState;
}
public void setStringState(String stringState) {
this.stringState = stringState;
}
EventListener是ThisEscape类的内部类。
**创建内部类必须得要有外部类创建的实例。**现在要创建一个ThisEscape对象,于是执行ThisEscape的构造方法,构造方法里面有 new EventListener对象,于是EventListener对象就隐式地持有外部类ThisEscape对象的引用。
如果能在其他地方访问到EventListner对象,就意味着"隐式"地发布了ThisEscape对象,而此时ThisEscape对象可能还尚未初始化完成,因此ThisEscape对象就是一个尚未构造完成的对象,这就导致只能看到ThisEscape对象的部分状态!
看下面示例(Demo-5):其中让EventSource对象持有EventListener对象的引用,也意味着:隐式地持有ThisEscape对象的引用了,这就是this引用逸出。
Demo-5:
public class EventSource {
ThisEscape.EventListener listener;//EventSource对象 持有外部类ThisEscape的 内部类EventListener 的引用
public ThisEscape.EventListener getListener() {
return listener;
}
public void registerListener(ThisEscape.EventListener listener) {
this.listener = listener;
}
}
内部类对象隐式持有外部类对象,可能会发生内存泄漏问题。持有内部类的实例,其对应的外部类对象不一定能够安全地释放相关资源。用Demo6代码段来进行说明。
Demo-6:
public class ThisEscapeTest {
public static void main(String[] args) {
EventSource eventSource = new EventSource();
ThisEscape thisEscape = new ThisEscape(eventSource);
ThisEscape.EventListener listener = eventSource.getListener();//this引用逸出
thisEscape.setStringState("change thisEscape state...");
//--------演示一下内存泄漏---------//
thisEscape = null;//希望触发 GC 回收 thisEscape
consistentHold(listener);//但是在其他代码中长期持有listener引用
}
}
Happens Before 发生在先关系
深刻理解这个关系,对判断代码中是否存在线程安全性问题很有帮助。扯一下发生在先关系的来龙去脉。
为了加速代码的执行,底层硬件有寄存器、CPU本地缓存、CPU也有多个核支持多个线程并发执行、还有所谓的指令重排…那如何保证代码的正确运行?因此Java语言规范要求JVM:
JVM在线程中维护一种类似于串行的语义:只要程序的最终执行结果与在严格串行环境中执行的结果相同,那么寄存器、本地缓存、指令重排都是允许的,从而既保证了计算性能又保证了程序运行的正确性。
在多线程环境中,为了维护这种串行语义,比如说:操作A发生了,执行操作B的线程如何看到操作A的结果?
Java内存模型(JMM)定义了Happens-Before关系,用来判断程序执行顺序的问题。这个概念还是太抽象,下面会用具体的示例说明。
有四个规则对判断多线程下程序执行顺序非常有帮助
安全发布由4种常见模式,接下来单独介绍各自线程安全的原因(模式1和模式3)。
注意事项:安全发布并不能保证在对象发布后的相关操作是线程安全的。
通常,要发布一个静态构造的对象,最简单和最安全的方式是使用静态的初始化器。使用静态的工厂方法返回此实例
Demo-7
public class SecurityPublish {
public static void main(String[] args) {
Holder holder=Holder.holder;//静态的方法属于类,无需调用构造器
}
}
class Holder{
private int n;
private Holder(int n){//私有化构造函数的目的是为了单例模式下的线程安全性
this.n =n;
}
public static Holder holder = new Holder(42);
}
线程安全的实现途径:为了确保对象引用的安全性,如果不适用锁机制,还有另外一个方法就是使用静态初始化器(单例模式)。这是由于JVM的一个特性:静态初始化器由JVM在类的初始化阶段执行,JVM依靠其自身的同步机制,可以使初始化的任何对象都可以被安全地发布。私有化构造函数的目的是为了单例模式下的线程安全性,如果用public修饰构造方法,那么确保不了一个尚未完全创建的对象拥有完整性,如下面的代码:
Demo-8
public class SecurityPublish {
public static void main(String[] args) {
Holder holder=new Holder(42);
}
}
class Holder{
private int n;
public Holder(int n){//私有化构造函数的目的是为了单例模式下的线程安全性
this.n =n;
}
}
在volatile变量的写入操作必须在对该变量的读取操作之前执行,变量的写入操作尚未彻底完成。那根据volatile变量规则:对该变量的访问也不能开始,这样就保证了安全发布。
对于含有final域的对象,JVM必须保证对对象的初始引用在构造函数之后执行,不能乱序执行(out of order),也就是可以保证一旦你得到了引用,final域的值都是完成了初始化的工作。所以其能够进行对象的正确发布。