第三章主要讲的共享对象,这章有些内容比较抽象,我理解其中的一些东西费了一些周折。所以把这些理解记录下来,以免以后遗忘,有些内容是个人的理解,如果您对我的理解有异议,请提出来共同讨论。
3.1 可见性
这里提到了“重排序”,指的是操作系统对线程分片后,针对不同线程的调度是没有特定顺序的。
3.1.1 过期数据
貌似没有什么可说的...
3.1.2 非原子的64位操作
这里指的是对double和long类型64位的变量。对于这种数据编写多线程程序的时候最好要加volatile标示。因为现在很多cpu是不对64位操作支持的,64位的数据会分成两个32位的数据,两部分会分别读取、操作,这样就会有两个存储数据的地方:内存和cpu缓存。volatile关键字指的是强制可见,即内存和cpu缓存数据强制保持一致。这里内存是可以存64位数,但是32位的cpu只能存32位。举个例子:假设我们有个64位的数,前32位是H 00 00 00 01,后32位是H 00 00 00 10,现在有两个线程,分别对前32位的数加3和加1,那么最后的结果应该是对前32位加4.列举如下:
最后的结果:H 00 00 00 05 00 00 00 10
中间结果(加3):H 00 00 00 03 00 00 00 10
中间结果(加1):H 00 00 00 02 00 00 00 10
按照一般的逻辑,如果没有同步策略,那么最后看到的结果只有这三种,其实结果是很乱的,很有可能是这种:
H 00 00 00 01 10 01 11 11 10
为何会出现这种结果呢?原因在于前面所说的,32位的CPU是把64位的数切开分别做的,那么在低位累加到高位的过程中,会出现重叠,从而造成混乱。
如果不想使用volatile关键字,使用锁策略的话也可以保证64位数的正确操作
3.1.3 锁和可见性
其实大意在于锁与可见性是密切想关的,锁不仅保证了同步与互斥,也保证了内存可见性。
3.1.4 volatile变量
这里主要说了volatile变量是一种轻量级别的锁,不过说的太粗了,以后的章节会有详细分析
比较重要的是以下几点:
(1) 主要用于确保对象状态的可见性,或者标识重要的生命周期的发生
(2) volatile的语义不足以使自增操作(++)原子化。这句给了个重要的启示,那就是说你的算法针对这个变量必须是无状态的时候才可选用volatile
(3)开发和测试阶段启动JVM的时候请开启 -server选项,这样JVM会做优化,比如把循环中的没有改变的判定变量提到循环外面,循环改为无限循环。
比如
volatile boolean asleep: ... while(!asleep)//JVM优化器不会优化该判断 doSomething();
这里加了volatile,所以是不会做优化,因为JVM认为你这是多线程要使用的变量,但是如果没有加,很可能就会做上述优化,那么这个时候你做多线程,开发阶段可能正确,部署阶段和生产阶段可能就会出问题。
3.2 发布和溢出
这一章比较抽象,以往多线程的书重点在同步和互斥,发布和溢出问题我还是第一次看到这么深入讨论的。
先来看看概念:
发布(publishing)一个对象的意思是使它能够被当前范围之外的代码所使用;
逸出(escape):一个对象在尚未准备好时就将它发布,这种清况称作逸出。
接下来举了一些逸出的例子:
(1)发布到公有区域,一个对象没有准备好就将一些东西发布到公有区域。
(2)从私有区域获取(允许内部可变的数据逸出)
class UnsafeStates{ private String[] states = new String[]{"SK","AL"...} public String[] getStates(){ return states;//直接把private数据发布了出去... } }
这样完全允许私有对象获取,违背了其私有性质。 我写过的代码好像有过这种情况....
"发布一个对象,同样也发布了该对象所有非私有域所引用的对象。更一般的,在一个已经发布的对象中,那些非私有域的引用链,和方法调用链中的可获得对象也都会被发布。" 这句话很重要,要结合"非私有域"和"方法调用链"来理解。也就是说非私有域是可被调用或者可被覆盖的,要注意。
(3)内部类实例发布。这个发布很抽象,结合代码来看看
public class ThisEscape{ public ThisEscape(EventSource source){//构造和发布绑定,造成this泄露 source.registerListener( new EventListener(){ public void onEvent(Event e){ doSomething(2); } } ); } }
这段代码是一个很常见的匿名内部类的创建,不过要注意,这里发生的一切是在构造函数中进行的。在构造函数中这么做会有什么问题呢?
问题在于source会调用EventListener,而EventListener是ThisEscape的匿名内部类,它持有对ThisEscape对象实例的引用。source不仅会调用EventListener,实际上source是持有EventListener的引用,那么同时source也就持有了ThisEscape的引用。如果ThisEscape还没做好实例化的情况下,另外一个线程通过source访问ThisEscape,这样就造成了逸出。
我把上面的代码理解为"愚蠢的黑社会老大模式",这里有三个角色:老大(ThisEscape),小弟(EventListener),警察(EventSource)。老大犯了事,小弟去顶,这个时候警察要来问小弟问题。 关键点就在这里:老大应当知道警察一定也会来问他(EventSource也会找到ThisEscape的实例),但是他却没有编好理由去应对(ThisEscape的构造函数还没有完成)
3.2.1安全的构建实践
上面的例子指出,一个未完成构造的对象不能被发布出去。
更具体的说,不要让this引用在构造期间逸出。
比如在构造函数中起线程,会造成this的逸出,这样this就会被新线程共享,由于this的构造函数还未完成,其他线程会获取this的不正确的状态。
这里要特别说明的是,上面的意思是:在构造函数中创建线程并没有问题,但最好不要再构造函数中启动它。
还有一点,在构造函数中调用一个可覆盖的实例方法(非private、final)同样会造成this在构造期间逸出为什么这么说呢?书上没有给出解释,但是我简单想了一下,可以给出如下的例子:
public class ThisEscape{ public ThisEscape(EventSource source){ checkSomeThing(); } public void checkSomeThing(){ System.out.println("这个代码没有问题,没有this的逸出"); } }
这个是你自己的代码,如果自己用,没问题,你可以保证你的构造函数引用其他函数的时候没有包含,但如果你写的是jdk代码会怎么样?使用jdk的客户端程序员这样搞一下:
public class ShitJDK extends ThisEscape{ public void checkSomeThing(){//这里覆盖了父类的checkSomeThing this.......//这里可以调用this,造成this逸出 } }
这样客户端程序员就很郁闷了。所以作为类库的开发者首要任务是考虑好封装,否则问题会很多。
那么怎么解决上面问题呢?
首先要分析一下,上面的代码犯了一个错误,就是构造和发布耦合。构造函数是构造的地方,并不是发布的地方,所以,改造的方法就是构造和发布相分离。
于是我们有三个基本需求:
(1)构造函数中只包含初始化相关内容。
(2)至少有两个步骤,一个是构造,一个是发布。
(3)客户端不关心黑老大,也就是说它只需要一个小弟,黑老大封装(最严格的封装方式是构造函数为private,这样只能通过本类的静态方法初始化本类,其他地方根本就不会初始化本类)。
书上给出的例子如下:
public class SafeListener{ private final EventListener listener;//老大含有小弟 private SafeListener(){//构造函数私有,并且只包含初始化相关,不包含发布 listener = new EventListener(){ public void onEvent(Event e){ doSomeThing(e); } } } public static SafeListener newInstance(EventSource source){ SafeListener safe = new SafeListener();//这里是调用构造 source.registerListener(safe.listener);//这里是发布,构造与发布相分离 return safe;//这里为什么要return 一个safe?推断是因为safe还有其他代 //码,不只一个注册功能。这个safe从命中初始化开始就和一个listener绑定 } }
是不是觉得有点复杂,抽象不好理解?这里是因为除了构造与发布相分离,这里还做了一件事就是封装了构造与发布相分离的过程。也就是说类的责任没有完全分开
这里有个很有意思的矛盾点,即:
既要构造与发布分离,又要构造与发布绑定(一起调用)。
第一个点很容易做到,就是分为两个步骤即可。
第二个点,这里要将两个步骤合并到一个函数中,以共同调用。 第二个点其实可以理解为工厂模式的封装方法,这里这么封装是为了SafeListener发布出去的时候就是已经注册好了的, 这段代码其实写的不好,因为它将工厂和工厂生产的东西混在一起了,可以加一个工厂,以更深刻的理解:
class SafeListener{//*****本类只负责构造,注意这里没有了public,是包访问权限,对内的 private final EventListener listener;//老大含有小弟,注意这里private SafeListener(){//*****构造函数包内可见 listener = new EventListener(){ public void onEvent(Event e){ doSomeThing(e); } } }
EventListener getEventListenerListener(){//*****同样注意是包访问权限,对内的
return listener;} ...//其他方法 } public class SafeListenerFactory{//工厂类负责发布任务和封装任务,和SafeListener在同一个包中,是对外的 public static SafeListener newInstance(EventSource source){ SafeListener safe = new SafeListener();//这里是调用构造 source.registerListener(safe.getEventListenerListener());//这里是发布,构造与发布相分离,注意与上面区分开 return safe;//这里为什么要return 一个safe?推断是因为safe还有其他代 //码,不只一个注册功能。这个safe从命中初始化开始就和一个listener绑定 } }
上面的代码是不是更好理解了?因为类的责任分开了,更具备面向对象的特征,更好理解。
要注意这里的代码和上面代码在访问权限修饰符上的不同。(*****为访问权限不同的地方)
上面代码的类图如下:
3.3 线程封闭
线程封闭是指将对象封闭在一个线程当中。这样就避免了线程共享数据,自动成为线程安全。
这里举了两个例子:
(1)Swing的线程封闭技术--事件分发线程
这里简单介绍了事件分发线程,讲的不是很清楚,有兴趣的可以看看这里:
http://space.itpub.net/13685345/viewspace-374940
这篇文章对于Swing的事件分发线程作了详细介绍,主要思想是:不要EDT里面做出了图形相关的任何事情。
同时,我很佩服Swing的简单的设计。
(2)JDBC应用池
这里主要讲一个Connection对应一个线程。
3.3.1 Ad-hoc线程限制
这里不要被Ad-hoc吓到,Ad-hoc(译为:非正式的。好抽象....)主要意思如同mashup一样大众化,大意是我的类库不管线程同步问题,所有同步问题交给类库的使用者解决。这里作者主要要告诉大家不要用Ad-hoc...
3.3.2 栈限制
这里主要指把变量限制在方法中,即尽量把不需要共享的数据放入方法内部。不过真要共享呢?加锁同步,或者更简单的本地线程变量(ThreadLocal)。
3.3.3 ThreadLocal
这个玩意就是给每个线程一个存储空间,这样直接避免了同步问题。但是真要线程共享数据呢?加锁同步...
这里介绍了一个ThreadLocal的使用场景,即用一个ThreadLocal变量持有事务上下文,这样不用在每次函数调用的时候都传递这个上下文,是不是很方便?是的,哈哈!
3.4 不可变性
这里先介绍了不可变对象,先看概念:
创建后状态不能被修改的对象叫做不可变对象。
其实很好理解,本身不可变意味着其他线程不能改变其数据
3.4.1 Final域
没啥,不过这里给出了两个编程的最佳实践:
将所有的域声明为私有,除非它们具有更高的可见性;
将所有的域声明为final,除非它们是可变的。
3.4.2 示例 : 使用vloatile发布不可变对象
这里主要给出了两个代码,第二个代码使用第一个类的时候加了vloatile修饰符,主要因为第一个类为不可变对象,不需要同步,但是有可见性问题,所以一定要加volatile,以保证两个变量同时可见。代码就不贴了,感兴趣的可以看书。
3.5 安全发布
这里分了几小节,把里面的好的思想提出来如下:
(1)final总是可以安全的用于任意线程(除了容器类)
(2)安全发布的类型:
- 通过静态初始化器初始化对象的引用(上面的黑老大例子)或者最简单的:
public static Holder holder = new Holder();//这里静态初始化器由JVM在类的初始阶段执行,也就是说客户
//写的线程目前还都没有启动。
- 将它的引用存储到volatile域或者AtomicReference
- 将它的引用存储到正确创建对象的final域中
- 将它的引用存储到由锁正确保护的域中
(3)高效不可变对象
概念:一个对象在技术上是不可变的,但是它的状态不会再发布后改变。
从这里看,与其叫"高效不可变对象",不如叫"伪不可变对象"更合适....
(4)总结了发布对象的必要条件依赖于对象的可变性:
- 不可变对象可以通过任意机制发布
- 高效不可变对象必须要安全发布
- 可变对象必须要安全发布,同时必须要线程安全或者被锁保护
(5)使用和共享对象的一些最有效的策略:
- 线程限制
- 共享只读
- 共享线程安全:有自己的同步代码,客户端代码无需写额外同步代码
- 被守护的:即加锁访问。最一般的措施...