2.对象的共享

上一章我们知道,同步代码块和同步方法可以确保以原子的方式执行操作,但是,synchronized除了这个功能外,还有一个重要的功能:内存可见性。(当一个线程修改状态后,其他线程能够看到状态的变化)

 

1.可见性

单线程,写入一个数据再读出来,值不会变。多线程中,如果没有同步机制,读的线程不一定能看到其他线程写入的值。而且就算看到了,可能顺序跟真实的顺序不一致。这种现象叫做“重排序”。

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

但是,只要有数据在多个线程之间实现共享,就使用正确的同步

 

(1)失效数据

读线程读到的可能并不是其他写线程写入的值,就会是失效数据。

 

(2)非原子的64位操作

在没有同步的情况下读取变量可能会读到失效值,但是这个失效值不是一个随机值,可能是由之前某个线程设置的值。这种安全性保证也被称为最低安全性(虽然这个值不对,但是它只是失效的或者过期的值,而不是随随便便的一个值)

 

最低安全性适用于绝大多数变量,但是有一个例外:非volatile类型的64位数值变量(double和long)

java内存模型要求,变量的读操作和写操作都必须是原子操作,但是非volatile类型的long和double除外,JVM允许将64位的读操作或写操作分解成两个32位的操作。这就存在不是原子性的可能,高32位与低32位可能来自两个值,组合后会变成一个随机数,而非最低安全性的数值。除非用volatile或者锁

 

(3)加锁与可见性

内置锁可以确保某个线程以一种可预测的方式来查看另一个线程的执行结果。

上一章总是说访问共享且可变的变量时要求所有线程在同一个锁上同步,就是为了确保某个线程写入该变量的值对于其他线程来说是可见的。否则,可能是失效值。

 

加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步

 

(4)volatile变量

*1)volatile变量可以确保将变量的更新操作通知到其他线程。且该变量上的操作不会与其他操作一起重排序。不会被缓存到寄存器或其他处理器不可见的地方。因此,读取volatile变量总是返回最新写入的值

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

 

*2)线程A读取变量——线程A写入volatile变量——线程B读取变量:在线程B读取变量后,线程A可以看见的所有变量,线程B都可以看到。

所以,从内存可见性角度来看,写入volatile变量相当于退出同步代码块,读取volatile变量相当于进入同步代码块。volatile变量提供了可见性

 

*3)当且仅当满足以下条件,才使用volatile变量:

*对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值

*该变量不会与其他状态变量一起纳入不变形条件中

*在访问变量时不需要加锁

 

2.发布与逸出

发布:使对象能够在当前作用于之外的代码使用。

逸出:当某个不该发布的对象被发布

 

(1)发布

*1)发布对象的最简单方法是:将对象的引用保存到一个公有的静态变量中,以便任何类和线程都能看见该对象。

*2)当发布一个对象时,在该对象的非私有域中引用的所有对象都会被发布。

*3)发布一个内部的类实例,其实也隐含的发布了这和个类实例本身,因为这个内部类的实例中包含了对类实例的隐含引用。

 

(2)逸出

*1)this引用在构造函数中逸出。构造函数中启动一个线程,this引用会被共享出去,在对象尚未完全构造之前,新的线程可以看到它。

 

3.线程封闭

当访问共享的可变数据时,通常需要同步,一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术被称为线程封闭,它是实现线程安全性的最简单方式之一。当某个对象封闭在一个线程中时,这种用法将自动实现线程安全性,即使被封闭的对象本身不是线程安全的。

 

(1)Ad-hoc线程封闭

Ad-hoc线程封闭是指,维护线程封闭的职责完全由程序实现来承担。这很脆弱,没有任何语言可以将对象封闭到目标线程上,一般对线程封闭对象的引用通常保存在公有变量上。建议最好别用。

 

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

 

(2)栈封闭

栈封闭是线程封闭的一种特例,在栈封闭中,只有通过局部变量才能访问对象局部变量的固有属性之一就是封闭在执行线程中。他们位于执行线程的栈中,其他线程无法访问这个栈。栈封闭比Ad-hoc线程封闭更加健壮。

 

对于基本类型的局部变量,无论如何都不会破坏栈封闭性,由于任何方法都无法获得对基本类型的引用,因此java确保了基本类型的局部变量始终封闭在线程里。

 

在维持对象引用的栈封闭性时,如果引用被发布,那么封闭性被破坏。

在线程内部上下文中使用非线程安全的对象,那么该对象仍然是安全的。

 

(3)ThreadLocal类

*1)ThreadLocal类能使线程的某个值与保存值的对象关联起来。它提供了get、set方法,为每个使用该变量的线程都存有一份独立的副本,因此总是可以get到最新的值

*2)ThreadLocal类通常用于防止对可变的单实例变量或全局变量进行共享。例如JDBC里的Connection对象可以用ThreadLocal类。

*3)ThreadLocal类可以用于:当某个频繁执行的操作需要一个临时对象(比如缓冲区),而同时又希望避免在每次执行时都重新分配该临时对象,就可以用ThreadLocal类。

*4)假设将一个单线程应用程序移植到多线程,通过将共享的全局变量转换为ThreadLocal对象,可以维持线程安全性。

而如果将应用程序范围内的缓存转换为线程局部的缓存,就不会有太大作用。

 

4.不变性

如果某个对象在被创建后,其状态就不能被修改,那么这个对象就被成为不可变对象。不可变对象一定是线程安全的。

不可变对象的状态由构造函数来控制。

 

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

(1)对象在创建以后其状态不能修改

(2)对象的所有域都是final类型

(3)对象是正确创建的(即创建期间,this引用没有逸出)

 

在不可变对象的内部仍可以使用可变对象来管理它们的状态,但是构造完成后无法修改。

 

(1)Final域

final用于构造不可变对象。然而,在java内存模型中,final域能确保初始化过程的安全性,即可以不受限制的访问对象,且共享对象时无需同步

即使对象是可变的,加了final,可以限制对象只能在限定的几种状态中变化,而不会随意变化。

 

(2)使用volatile类型来发布不可变对象

我们可以定义一个volatile类型的引用确保可见性,并通过使用包含多个状态变量的容器对象来维持不变性,这样可以保证线程安全。

 

5.安全发布

有时候我们希望在多个线程间共享对象,有可能你的对象,有的线程看到的是构造成功的,有的线程看到的是未构造完成的。

 

(1)不正确的发布:正确的对象被破坏

我们不能指望未被完全创建的对象拥有完整性

 

如果没有使用同步,那么对象对其他线程来说可能会有两个问题:

*1)除了发布对象的线程外,其他线程可能看到对象域是一个失效值,因此可能是空引用或者旧值

*2)其他线程看到的对象引用的值是最新的,但是对象的状态却是失效的。

 

(2)不可变对象与初始化安全性

这边我们需要知道,就算某个对象的引用对其他线程是可见的,但是并不意味着对象状态对其他线程一定是可见的。为了确保一致,就需要同步

另外,即使发布不可变对象的引用时没有使用同步,仍然还可以安全的访问该对象

在没有额外同步的情况下,也可以安全的访问final类型的域,但是如果final类型的域所指向的是可变对象,那么在访问这些域所指向的对象时仍需要同步

 

(3)安全发布的常用模式

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

*1)在静态初始化函数中初始化一个对象引用

public static Holder holder = new Holder(42);

*2)将对象的引用保存到volatile类型的域或者AtomicReferance对象

*3)将对象的引用保存到某个正确构造对象的final类型域

*4)将对象的引用保存到一个由锁保护的域

        ^1)通过将一个键或者值放入Hashtable、synchronizedMap、ConcurrentMap中,可以安全的将它发布给任何从这些容器中访问它的线程

        ^2)通过将元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList、synchronizedSet中,可以安全的将它发布给任何从这些容器中访问它的线程

        ^3)通过将元素放入BlockQueue、ConcurrentLinkedQueue中,可以安全的将它发布给任何从这些容器中访问它的线程

        ^4)其他数据传递机制(Future、Exchanger)同样能实现安全发布。

 

(4)事实不可变对象

如果对象在发布后不会被修改,那么安全发布是足够的。

所有的安全发布机制都能确保,当对象的引用对所有访问该对象的线程可见时,对象发布时的状态对于所有线程也将是可见的,并且如果对象状态不会再变,那么就足以确保任何访问都是安全的。

如果对象从技术上可变,但是其状态在发布后不会再变化,这个对象就是“事实不可变对象”。在没有额外的同步的情况下,任何线程都可以安全的使用被安全发布的世事不可变对象。

 

(5)可变对象

对象的发布绣球取决于其可变性:

*1)不可变对象可以通过任意机制发布

*2)事实不可变对象必须通过安全方式发布

*3)可变对象必须通过安全方式发布,并且必须是线程安全的或者 由某个锁保护起来

 

(6)安全地共享对象

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

*1)线程封闭:线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改

*2)只读共享:在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但是任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象

*3)线程安全共享:线程安全的对象在其内部实现同步,因此多个线程可以通过对象的共有接口来进行访问而不需要进一步同步

*4)保护对象:被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

鹏哥哥啊Aaaa

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值