昨天看到一篇文章阐述技术类资料的"等级",看完以后颇有共鸣。再加上最近在工做中愈加以为线程安全性的重要性和难以捉摸,又掏出了《Java并发编程实战》研读一番,这本书应该是属于为“JAVA 多线程做注解”的一本书,那我就为书中关于对象安全发布的内容做一些注解,做为今年的第一篇博文。html
我读的是中文版,确实感受书中有些地方的描述晦涩难懂,也没有去拿英文原文来对照,就按中文版描述,再配上一些示例代码记录个人一些理解吧。java
发布是个动词,是去发布对象。而对象,通俗的理解是:JAVA里面经过 new 关键字 建立一个对象。编程
发布一个对象的意思是:使对象在当前做用域以外的代码中使用。好比下面knowSecrets指向的HashSet类型的对象,由static修饰,是一个类变量。当前做用域为PublishExample类。缓存
import java.util.HashSet; import java.util.Set; /** * @author psj * @date 2019/03/10 */ public class PublishExample { public static Set<Secret> knowSecrets; public void initialize() { knowSecrets = new HashSet<>(); } }
public修饰引用knowSecrets,致使 在其余类中也能访问到这个HashSet对象,好比向HashSet添加元素或者删除元素。所以,也就发布了这个对象。安全
public class UsingSecret { public static void main(String[] args) { PublishExample.knowSecrets.add(new Secret()); PublishExample.knowSecrets.remove(new Secret()); } }
另外,值得注意的是:添加到HashSet集合中的Secret对象也被发布了。多线程
由于对象通常是在构造函数里面初始化的(不讨论反射),当 new 一个对象时,会为这个对象的属性赋值,当前时刻对象各个属性拥有的值 称为对象的状态。并发
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")
改变了对象的状态。app
建立对象的目的是使用它,而要用它,就要把它发布出去。同时,也引出了一个重要问题,咱们是在哪些地方用到这个对象呢?好比:只在一个线程里面访问这个对象,仍是有可能多个线程并发访问该对象?ide
对象被发布后,是没法知道其余线程对已发布的对象执行何种操做的,这也是致使线程安全问题的缘由。函数
先看一个不安全发布的示例----this引用逸出。参考《Java并发编程实战》第3章程序清单3-7
当我第一次看到"this引用逸出"时,是懵逼的。后来在理解了“发生在先”原则、“初始化过程安全性”、"volatile关键字的做用"以后才慢慢理解了。这些东西后面再说。
外部类ThisEscape和它的内部类EventListener
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; }
如今要建立一个ThisEscape对象,因而执行ThisEscape的构造方法,构造方法里面有 new EventListener对象,因而EventListener对象就隐式地持有外部类ThisEscape对象的引用。
那若是能在其余地方访问到EventListner对象,就意味着"隐式"地发布了ThisEscape对象,而此时ThisEscape对象可能还还没有初始化完成,所以ThisEscape对象就是一个还没有构造完成的对象,这就致使只能看到ThisEscape对象的部分状态!
看下面示例:我故意让EventSource对象持有EventListener对象的引用,也意味着:隐式地持有ThisEscape对象的引用了,这就是this引用逸出。
public class EventSource { ThisEscape.EventListener listener;//EventSource对象 持有外部类ThisEscape的 内部类EventListener 的引用 public ThisEscape.EventListener getListener() { return listener; } public void registerListener(ThisEscape.EventListener listener) { this.listener = listener; } }
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关系,用来判断程序执行顺序的问题。这个概念仍是太抽象,下面会用具体的示例说明。在我写代码的过程当中,发现有四个规则对判断多线程下程序执行顺序很是有帮助:
程序顺序规则:
若是程序中操做A在操做B以前(即:写的代码语句的顺序),那么在单个线程执行中A操做将在B操做以前执行。
监视器规则:
这个规则是关于锁的,定义是:在监视器锁上的解锁操做必须在同一个监视器锁上的加锁操做以前。咋一看,没啥用。我这里扩展一下,以下图:
在线程A内部的全部操做都按照它们在源程序中的前后顺序来排序,在线程B内部的操做也是如此。(这就是程序顺序规则)
因为A释放了锁,而B得到了锁,所以A中全部在释放锁以前的操做 位于 B中请求锁以后的全部操做以前。这句话:它的意思就是:在线程A解锁M以前的全部操做,对于线程B加锁M以后的全部操做都是可见的。这样,在线程B中就能看到:线程A对 变量x 、变量y的所写入的值了。
再扩展一下:为了在线程之间传递数据,咱们常常用到BlockingQueue,一个线程调用put方法添加元素,另外一个线程调用take方法获取元素,这些操做都知足发生在先关系。线程B不只仅是拿到了一个元素,并且还能看到线程A修改的一些对象的状态(这就是可见性)
总结一下:
同步操做,好比锁的释放和获取、volatile变量的读写,不只知足发生在先关系(偏序),并且还知足全序关系。总之:要想保证执行操做B的线程看到操做A的结果(无论操做A、操做B 是否在同一个线程中执行),操做A、操做B 之间必须知足发生在先关系
volatile变量规则:对volatile变量的写入操做必须在该变量的读取操做以前执行。这条规则帮助理解:为何在声明类的实例变量时用了volatile修饰,做者的意图是什么?
传递性:若是操做A在操做B以前执行,操做B在操做C以前执行,那么操做A必须在操做C以前执行。在你看到一大段代码,这个线程里面调用了synchronized修饰的方法、那个线程又向阻塞队列put了一个元素、另外一个线程又读取了一个volatile修饰的变量…从这些发生在先规则里面 使用 传递性 就能大体推断整个代码的执行流程了。
扯了这么多,看一个不安全发布的示例。
public class UnsafeLazyInitialization { private static Resource resource; public static Resource getResource() { if (resource == null) { resource = new Resource();//不安全的发布 } return resource } }
这段代码没有应用到前面提到的任何一个发生在先规则,代码在执行过程当中发生的指令重排致使了不安全的发布。
在建立对象、发布对象时,隐藏了不少操做的。new Resource对象时须要给Resource对象的各个属性赋值,赋值完了以后,在堆中对象的地址要赋值给 静态变量resource。在整个过程当中就有可能存在指令重排,看图:
相似地,双重检查加锁也会致使不安全的发布。
public class EagerInitialization { private static Resource resource = new Resource(); public static Resource getResource() { return resource; } }
在声明静态变量时同时初始化,由JVM来保证初始化过程的安全性。static修饰说明是类变量,于是符合单例模式。
初始化安全性是一种保证:正确构造的对象在没有同步的状况下也能安全地在多个线程之间共享,而无论它是如何被发布的。换句话说:对于被正确构造的对象,全部线程都能看到由构造函数为对象各个final域设置的正确值。
再换句话说:对于含有final域的对象,初始化安全性能够防止对象的初始引用被重排序到构造过程以前。这句话已经点破了关键了。看上一幅图,线程A在赋值到半路,太累了,休息了一下,抽了一根烟。而后继续开始了它的赋值,这些赋值操做,就是对象的构造过程。而在赋值的中间,存在着一个指令重排---将还没有构造完成的对象的堆地址写入到初始引用中去了,而若是这个时候刚好有其余线程拿着这个初始引用去访问对象(好比访问该对象的某个属性),但这个对象还未初始化完成啊,就会致使bug。
哈哈哈哈……是否是仍是看不懂、很抽象?这就是 经。经书级别的经,难念的经。咱用代码来讲明一下:
public class Resource { private int x;//没有用final修饰 private String y;//没有用final修饰 public Resource(int x, String y) { this.x = x; this.y = y; } }
而若是,这两个属性都用final修饰的话,那么就知足初始化安全的保证,就没有指令重排了。
这就是final关键字所起的做用。
另外,你是否是注意到,若是用final修饰实例变量时,IDEA会提示你还没有给final修饰的实例变量赋初始值?哈哈……
总结一下:
构造函数对final域的全部写入操做,以及对经过这些域能够到达的任何变量的写入操做,都将被“冻结”,而且任何得到该对象引用的线程都至少能确保看到被冻结的值。对于经过final域可到达的初始变量的写入操做,将不会与构造过程后的操做一块儿被重排序。
因此:若是Resouce是一个不可变对象,那么UnsafeLazyInitialization就是安全的了。
//不可变 public class Resource { private final int x; private final String y; public Resource(){x=10;y="hello"} public Resource(int x, String y) { this.x = x; this.y = y; } } //UnsafeLazyInitialization 不只是安全的发布,并且在多线程访问中也是线程安全的。 //由于Resource的属性x、y 都是不可变的。 public class UnsafeLazyInitialization { private static Resource resource; public static Resource getResource() { if (resource == null) { resource = new Resource();//安全的发布! } return resource; } }
关于初始化安全性,只能保证 final 域修饰的属性在构造过程完成时的可见性。若是,构造的对象存在非final域修饰的属性,或者在构造完成后,在程序中其余地方可以修改属性的值,那么必须采用同步来保证可见性(必须采用同步保证线程安全),示例以下:
import java.util.HashMap; import java.util.Map; /** * @author psj * @date 2019/03/10 */ public class UnSafeStates { /** * UnSafeStates 惟一的一个属性是由final修饰的,初始化安全性仍是存在的 * 即:其余线程能看到一个正确且 **构造完成** 的UnSafeStates对象 */ private final Map<String,String> states; public UnSafeStates() { states = new HashMap<>(); states.put("hello", "he"); states.put("world", "wo"); } public String getAbbreviation(String s) { return states.get(s); } /** * 这个方法可以修改 states 属性的值, UnSafeStates 再也不是一个线程安全的类了 * 若是多线程并发调用 setAbbreviation 方法, 就存在线程安全性问题. HashMap的循环引用了解一下?哈哈…… * @param key * @param value */ public void setAbbreviation(String key, String value) { states.put(key, value); } }
这个和final关键字中讨论的初始化安全性相似。只不过,volatile修饰的属性是知足发生在先关系的。
套用volatile变量规则:在volatile变量的写入操做必须在对该变量的读取操做以前执行,那volatile也能避免前面提到的指令重排了。由于,初始化到一半,而后好累,要休息一下,说明初始化过程还没有完成,也即:变量的写入操做还没有完全完成。那根据volatile变量规则:对该变量的访问也不能开始。这样就保证了安全发布。这也是为何DCL双重检查锁中定义的static变量 用volatile修饰就能安全发布的缘由。
在写代码过程当中,有时不太刻意地去关注安全发布,在声明一个类的属性时,有时就顺手给实例变量用一个final修饰。抑或是在考虑多线程访问到一个状态变量时,给它用个volatile修饰,并无真正地去思考总结final到底起做用在哪里了?
因此总结起来就是:final关键字在初始化过程当中防止了指令重排,保证了初始化完成后对象的安全发布。volatile则是经过JMM定义的发生在先关系,保证了变量的内存可见性。
最近在看ES源码过程当中,看别人写的代码,就好奇,哎,为何这里这个属性要用个final呢?为何那个属性加了volatile修饰呢?其实只有明白背后原理,才能更好地去理解别人的代码吧。
固然,上面写的全是本身的理解,有可能出错,由于我并无将源代码编译成字节码、甚至是从机器指令角度去分析 上面示例的执行流程,由于我看不懂那些汇编指令,哈哈哈哈哈哈……
《Java并发编程实战》第3章、第16章
这篇文章前先后后加起来竟然写了6个小时,没时间打球了…^:(^ ^:(^
原文:https://www.cnblogs.com/hapjin/p/10505337.html