《java并发编程实践》第三章学习笔记

系统 1610 0

第三章主要讲的共享对象,这章有些内容比较抽象,我理解其中的一些东西费了一些周折。所以把这些理解记录下来,以免以后遗忘,有些内容是个人的理解,如果您对我的理解有异议,请提出来共同讨论。

 

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的构造函数还没有完成)

  
《java并发编程实践》第三章学习笔记
    下面的实践中,会有对这种情况的改进方法。

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绑定
 }
}
  

   上面的代码是不是更好理解了?因为类的责任分开了,更具备面向对象的特征,更好理解。

   要注意这里的代码和上面代码在访问权限修饰符上的不同。(*****为访问权限不同的地方)

   上面代码的类图如下:

  
《java并发编程实践》第三章学习笔记

 

 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)使用和共享对象的一些最有效的策略:

  • 线程限制
  • 共享只读
  • 共享线程安全:有自己的同步代码,客户端代码无需写额外同步代码
  • 被守护的:即加锁访问。最一般的措施...

 

《java并发编程实践》第三章学习笔记


更多文章、技术交流、商务合作、联系博主

微信扫码或搜索:z360901061

微信扫一扫加我为好友

QQ号联系: 360901061

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描下面二维码支持博主2元、5元、10元、20元等您想捐的金额吧,狠狠点击下面给点支持吧,站长非常感激您!手机微信长按不能支付解决办法:请将微信支付二维码保存到相册,切换到微信,然后点击微信右上角扫一扫功能,选择支付二维码完成支付。

【本文对您有帮助就好】

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描上面二维码支持博主2元、5元、10元、自定义金额等您想捐的金额吧,站长会非常 感谢您的哦!!!

发表我的评论
最新评论 总共0条评论