第3章-对象的共享

1.分析清单3-1
2.syn是否禁止指令重排序
3.volatile修饰引用类型
4.java中的不变性条件指什么
1.连接池回收线程时是否清除Thread对象中的值?

要编写正确的并发程序,关键问题在于:在访问共享的可变状态时进行正确的管理。

第二章介绍了如何通过避免多个线程在同一时刻访问相同的数据,而本章将介绍如何共享和发布对象,从而使他们能够安全地由多个线程同时访问。

synchronized 1.原子性 2.内存可见性(memory visibility)3.互斥性

在没有同步的情况下,编译器处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得出正确结论。

在缺乏同步的程序中,可能会出现一种产生错误结果的情况:失效数据

出现失效数据的可能 :

  1. 不能保证原子性
  2. 不能保证可见性
  3. 存在指令重排序

失效数据可能会导致一些严重的安全问题或者活跃性问题,比如

  1. 输出错误的值
  2. 使程序无法结束,无限循环
  3. 意料之外的异常
  4. 被破坏的数据结构
  5. 不精确的计算

当线程在没有同步的情况下读取变量时,可能会得到一个失效值,但至少这个值是由之前某个线程设置的值,而不是一个随机值,这种安全性保证也被称为最低安全性(out-of-thin-airsafety)

最低安全性适用于绝大多数变量,但是存在一个例外:非volatile类型的64位数值变量(double和long)。java内存模型要求,变量的读取操作和写入操作都必须是原子操作,但对于非volatile类型的long和double变量,jvm允许将64位的读操作或写操作分解为两个32位的操作。当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值的低32位。因此,即使不考虑失效数据,在多线程程序中使用共享且可变的long和double等类型变量也是不安全的,(这里是指既保证不了最低安全性,也保证不了原子性 。。。)除非

  1. 用关键字volatile来声明它们
  2. 用锁保护起来

内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行情况

java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,(因此不能将局部变量声明为volatile)因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型变量时总会返回最新的值

在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。

在当前大多数处理器架构上,读取volatile变量的开销只比读取非volatile变量的开销略高一些。

volatile变量的正确使用方式包括:

  1. 确保他们自身状态的可见性
  2. 确保他们所引用对象状态的可见性
  3. 标识一些重要的程序生命周期事件的发生(例如,初始化或关闭)

volatile变量通常用作某个操作完成、发生中断或者状态的标志

volatile的语义不足以确保递增操作(count++)的原子性,除非你能确保只有一个线程对变量执行 操作

当且仅当满足以下所有条件时,才应该使用volatile变量:

  1. 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
  2. 该变量不会与其他状态变量一起纳入不变性条件中。
  3. 在访问变量时不需要加锁

发布(Publish)一个对象的意思是指,使对象能够在当前作用域之外的代码中使用。比如:

  1. 将一个指向该对象的引用保存到其他代码可以访问的地方(公有静态变量)
  2. 在某一个私有的方法中返回改引用
  3. 将引用传递到其他类的方法中(方法传递,即alien方法,发布一个内部类的实例)

在许多情况中,我们要确保对象及其内部状态不被发布。而在某些情况下,我们又需要发布某个对象,但如果发布时要确保线程安全性,则可能需要同步。发布内部状态可能会破坏封装行,并且使程序难以维持不变性条件。如果在对象构造完成之前就发布该对象,就会破坏线程安全性

当某个不应该发布的对象被发布时,这种情况就被称为逸出(Escape)。

当发布某个对象时,可能会间接地发布其他对象。

当发布一个对象时,在该对象的非私有域中引用的所有对象同样会被发布。

假定有一个类C,对于C来说,“外部方法(alien)” 是指行为并不完全由C来规定的方法,包括

  1. 其他类中定义的方法
  2. 类C中可以被改写的方法(既不是私有方法也不是final方法)

当把一个对象传递给某个外部方法时,就相当于发布了这个对象。

发布内部类实例时,也隐含地发布了外部类实例本身,因为在这个内部类的实例中包含了外部类实例的隐含引用。

不安全(正确)的对象构造过程有可能导致this逸出,比如

  1. 在构造函数中发布一个匿名内部类
  2. 在构造函数中启动一个线程
  3. 在构造函数中调用一个可改写的实例方法(alien方法)

当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术被称为线程封闭(Thread Confinement)

在java语言中并没有强制规定某个变量必须由锁来保护,同样在java语言中也无法强制将对象封闭在某个线程中。(因为无论怎样都有对象逸出的可能性)

java线程封闭类型 :

  1. ad-hoc封闭
  2. 栈封闭
  3. ThreadLocal类

ad-hoc线程封闭是指,维护线程封闭性的职责完全由程序实现来承担。共享的公有变量,脆弱性,尽量少用它。

在volatile变量上存在一种特殊的线程封闭。只要你能确保只有单个线程对共享的volatile变量执行写入操作,那么就可以安全地在这些共享的volatile变量上执行“读取修改写入”的操作。在这种情况下,相当于将修改操作封闭在单个线程中以防止发生竞态条件,并且volatile变量的可见性保证还确保了其他线程能看到最新的值。

栈封闭,只有通过局部变量才能访问对象。
对于基本类型的局部变量,无论如何都不会破坏封装性。由于任何方法都无法获得对基本类型的引用,因此java语言的这种语义就确保了基本类型的局部变量始终封闭在线程内。
对于引用类型的基本变量,要防止逸出。

如果在线程内部(Within-Thread) 上下文中使用非线程安全的对象,那么该对象仍然是线程安全的。(因为没共享)

ThreadLocal类能使线程中的某个值与保存值的对象关联起来。
ThreadLocal对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。

ThreadLocal中特定线程的值保存在了自身Thread对象中,当线程终止时,这些值会作为垃圾回收。

满足同步需求的另一种方法是使用不可变对象(Immutable Object)。

如果某个对象在被创建后其状态就不能被修改(不可变对象状态一个,并且该状态由构造函数来控制),那么这个对象就称为不可变对象。线程安全性是不可变对象的固有属性之一,他们的不变性条件是由构造函数创建的,只要他们的状态不改变,那么这些不变性条件就能得以维持。

不可变对象一定是线程安全的。

即使对象中所有的域都是final类型的,这个对象也仍有可能是可变的,因为在final类型的域中可以保存对可变对象的引用

当满足以下条件时,对象才是不可变的:

  1. 对象创建以后其状态就不能修改(对象的状态为其下所有域状态之和(递归))
  2. 对象的所有域都是final类型(不递归)
  3. 对象是正确创建的(在对象的创建期间,this引用没有逸出;singleton不能有多个线程同时构造)

例如String对象被创建后,其内部状态不会被修改,为不可变对象。

从技术上来看,不可变对象并不需要将其所有的域都声明为final类型,例如String就是这种情况。这就要对类的良性数据竞争(Benign Data Race)情况做精确分析,因此需要深入理解java内存模型(注意:String会将散列值的计算推迟到第一次调用hash Code时进行,并将计算得到的散列值缓存到非final类型的域中,但这种方式之所以可行,是因为这个域有一个非默认的值,并且在每次计算中都得到相同的结果[因为基于一个不可变的状态]自己在编写代码时不要这么做)。

在不可变对象的内部仍可以使用可变对象来管理他们的状态。如以下程序清单。

@Immutable
public final class ThreeStooges{
    private final Set<String> stooges = new HashSet<String>();
    public ThreeStooges(){
        stooges.add("Moe");
        stooges.add("Larry");
        stooges.add("Curly");
    }
    public boolean isStooge(String name){
        return stooges.contains(name);
    }
}

尽管保存姓名的Set对象是可变的,但从ThreeStooges的设计中可以看到,在Set对象构造完成后无法对其进行修改(1)。stooges是一个final类型final类型的引用变量,因此所有的对象状态都通过一个final域来访问(2) 。最后一个要求是“正确地构造对象”,这个要求很容易满足,因为构造函数能使该引用由除了构造函数及其调用者之外的代码来访问。

关键字final可以视为c++中const机制的一种受限版本,用于构造不可变性对象。final类型的域是不能修改的。

在java内存模型中,final域还有着特殊的语义。final域能确保初始化的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无须同步。

正如“除非需要更高的可见性,否则应将所有的域都声明为私有域”[EJ Item 12]是一个良好的编程习惯,“除非需要某个域是可变的,否则应将其声明为final域”也是一个良好的编程习惯。

不可变对象能够提供一种弱形式的原子性。

通过使用包含多个状态变量的容器对象来维持不变性条件,并使用一个volatile类型的引用来确保安全性,使得类在没有显式地使用锁的情况下仍然是线程安全的。

要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全的发布:

  1. 在静态初始化函数中初始化一个个对象引用。
  2. 将对象的引用保存到volatile类型的域或者AtomicReference对象中。
  3. 将对象的引用保存到某个正确构造对象的final类型域中。
  4. 将对象的引用保存到一个由锁保护的域中。

在线程安全容器内部的同步意味着,在将对象放到某个容器中,例如Vector或synchronizedList时,将满足上述最后一条需求。如果线程A将对象X放入一个线程安全的容器,随后线程B读取这个对象,那么可以确保B看到A设置的X状态,即便在这段读/写X的应用程序代码中没有包含显式的同步。尽管Javadoc在这个主题上没有给出很清晰的说明,但线程安全库中的容器类提供了以下的安全发布保证:

  1. 通过将一个键或者值放入HashTable、synchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程(无论是直接访问还是通过迭代器访问)
  2. 通过将某个元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或synchronizedSet中,可以将该元素安全地发布到任何从这些容器中访问该元素的线程。
  3. 通过将某个元素放入BlockingQueue或者ConcurrentLinkedQueue中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程。

通常,要发布一个静态构造的对象,最简单和最安全的方式是使用静态的初始化器。

public static Holder holder = new Holder(42);

静态初始化器由jvm在类的初始化阶段执行。由于在jvm内部存在着同步机制,因此通过这种方式初始化的任何对象都可以被安全地发布。

如果对象从技术上来看是可变的,但其状态在发布后不会再改变,那么把这种对象称为“事实不可变对象(Effectively Immutable Object)”。

如果对象在构造后可以修改,那么安全发布只能确保“发布当时”状态的可见性。对于可变对象,不仅在发布对象时需要使用同步,而且在每次对象访问时同样需要使用同步来确保后续修改操作的可见性。

对象的发布需求取决于它的可变性:

  1. 不可变对象可以通过任意机制来发布
  2. 事实不可变对象必须通过安全方式来发布
  3. 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来。

在并发程序中使用和共享对象时,可以使用一些实用的策略:

  1. 线程封闭。线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改
  2. 只读共享。在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
  3. 线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。
  4. 保护对象。被保护的对象只能通过持有特定的锁访问。保护对象包括封装在其他线程安全中的对象,以及已发布的并且由特定锁保护的对象。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值