java并发编程(2)--对象的共享

其实除了上述的表达,我们还需要了解一些其他的方面,同步不仅仅是上面的部分,还需要考虑“可见性”。的问题,也就是常见的对象的共享问题。
      这里我们引用了:
     《java并发编程实战》
 
     为了能让开发人员安全正确地在Java存储模型上编程,JVM提供了一个happens-before原则,有人整理得非常好,我摘抄如下:
  • 在程序顺序中, 线程中的每一个操作, 发生在当前操作后面将要出现的每一个操作之前.
  • 对象监视器的解锁发生在等待获取对象锁的线程之前.
  • 对volitile关键字修饰的变量写入操作, 发生在对该变量的读取之前.
  • 对一个线程的 Thread.start() 调用 发生在启动的线程中的所有操作之前.
  • 线程中的所有操作 发生在从这个线程的 Thread.join()成功返回的所有其他线程之前.

有了原则还不够,Java提供了以下工具和方法来保证变量的可见性和安全发布:

  • 使用 synchronized来同步变量初始化。此方式会立马把工作内存中的变量同步到主内存中
  • 使用 volatile关键字来标示变量。此方式会直接把变量存在主存中而不是工作内存中
  • final变量。常量内也是存于主存中
 
另外,一定要明确只有共享变量才会有以上那些问题,如果变量只是这个线程自己使用,就不用担心那么多问题了。
 
我们这里来讨论下第一章所提出的的三个主要的解决线程安全性的办法:
 
  •           不在线程之间共享该状态变量
     采用这种方法可以避免同步,因为不共享数据。也就是说我仅在单线程内访问数据,就不需要同步,这种技术就被称为线程封闭,它是实现线程安全性的最简单的方式之一。
  1.      Ad-hoc线程封闭:维护线程封闭性的职责完全由程序实现来承担。但是该技术非常脆弱,因此在程序中尽量少使用。
  2.      栈封闭:这是一个线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。局部变量的固有属性之一就是封闭在执行线程中。它们位于执行线程的栈中,其他线程无法访问这个栈。需要注意的是这种方式一定要明确留下文档,否则后续维护人员很容易错误的使对象逸出。
  3.      ThreadLocal类:这个类可以使线程中的某个值与保存值得对象关联起来。threadLocal提供了get与set等访问接口和方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。通常threadlocal对象用于防止对可变的单实例变量(singleton)或者全局变量进行共享。
          ThreadLocal很容易让人望文生义,想当然地认为是一个“本地线程”。其实,ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
线程的角度看,目标变量就像是线程的本地变量,这也是类名中“Local”所要表达的意思。
线程局部变量并不是Java的新发明,很多语言(如IBM IBM XL FORTRAN)在语法层面就提供线程局部变量。在Java中没有提供在语言级支持,而是变相地通过ThreadLocal的类提供支持。
所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,因此造成线程局部变量没有在Java开发者中得到很好的普及。
  •           将状态变量改为不可变的变量
     线程安全性时不可变对象的固有属性之一,它们的不变性条件是由构造函数创建的。不可变对象一定是线程安全的
 
     当满足下面的条件,对象才是不可变的:
  1. 对象创建以后其状态就不能拿修改。
  2. 对象的所有域都是final类型。
  3. 对象时正确创建的(在对象的创建期间,this引用没有逸出)。
     发布不可变的对象常见有两种方式,一种是final类型,另外一种是volatile类型。前者很好理解,后者是发布volatile类型的一个不可变对象,其实还是使用了final类型,后者也可以说是volatile与final的合作来发布对象。
  •           在访问状态变量的时候使用同步
     可变对象必须通过安全的方式来发布,这通常意味着在发布和使用该对象的线程时都必须使用同步。现在,我们将重点介绍如果确保使用对象的线程能够看到该对象处于已发布的状态。
     要安全的发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全的发布:
  1. 在静态初始化函数中初始化一个对象引用。
  2. 将对象的引用保存到volatile类型的域或者atomicReferance对象中。
  3. 将对象的引用保存到某个正确构造对象的final类型域中。
  4. 将对象的引用保存到一个由锁保护的域中。
     对于第四条,Java平台为了解决这个问题,提供了很多线程安全的类和并发工具,通过这些类和工具就能更简便地写线程安全的代码。归纳一下有以下几种,具体的介绍请参见引用的第三个文章:
  • 同步容器类
  • 并发容器类
  • 生产者和消费者模式
  • 阻塞和可中断方法
  • Synchronizer
总结:
     在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:
  • 线程封闭:线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。
  • 只读共享:在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
  • 线程安全共享:线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。
  • 保护对象:被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已经发布的并且由某个特定锁保护的对象。
 
附注:
1.volitle关键字
 
非原子的64位操作:
     当线程在没用同步的情况下读取变量时,可能会得到一个失效值,但是至少这个值是由之前某个线程设置的值,而不是一个随机值。这种安全性也被称为最低安全性。最低安全性适用于绝大多数变量,但是存在一个例外:就是非volitle类型的64位数值变量:long和double。 java的内存模型要求,变量的操作和写入操作必须是原子操作,对于非volitle类型的long和double变量,jvm允许将64位的读操作或者写入操作分解为两个32位的操作。
 
     volitle变量是一种稍弱的同步机制,用来确保将变量的更新操作通知到其他进程。当把变量声明为volatile类型后,就不会讲该变量上的操作与其他内存操作一起重排序,并且在当前大多数的处理器架构上,读取volatile变量的开销只比读取非volatile变量的开销略高一点,是一种比sychronized关键字更轻量级的同步机制。
 
     但是一般只是用于做某个操作完成,发生中断或者状态的标志,虽然也可以用作表示其他的状态信息,但是使用时要很小心,例如,volatile的语义不足以确保递增操作(count++)的原子性。也就是说:加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性
 
2.final变量
 
     final是用来构造不可变的对象,final类型的域是不可以变的,但是如果final域所引用的对象是可变的,那么这些被引用的对象时可以修改的。在java的内存模型中,final域能够确保初始化过程的安全性,从而可以不受限制的访问不可变对象,并且共享这些对象时无需同步。
 
     除非需要更高的可见性,否则应该将所有的域都声明为私有域,除非某个域式可变的,否则应该将其声明为final域
 
 
3.this引用逸出
     在构造过程中使this引用逸出的一个常见错误时,在构造函数中启动一个线程。当对象在其构造函数中创建一个线程时,无论是显式创建(通过将它传给构造函数)还是隐式创建(由于thread或runnable是该对象的一个内部类),this引用都会被新创建的线程共享。在对象尚未完全构造之前,新的线程就可以看见它。在构造函数中创建线程没有错误,但是最好不要启动它,而是通过一个start或者initialize方法来启动。
 
4.事实不可变对象
 
     如果对象在发布后不会被修改,那么对于其他在没有额外同步的情况下安全的访问这些对象的线程来说,安全发布时足够的。如果对象从技术上来看是可变的,但是其状态在发布后不会再改变,那么把这种对象称为事实不可变对象。
     对象的发布需求取决于它的可变现:
  • 不可变对象可以通过任意的机制来发布
  • 事实不可变对象必须通过安全方式来发布
  • 可变对象必须通过安全方式 来发布,并且必须是线程安全或者由某个锁保护起来。
posted @ 2013-04-21 21:14  editice  阅读(378)  评论(0)    收藏  举报