对象的共享

Posted 老人与JAVA

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了对象的共享相关的知识,希望对你有一定的参考价值。

3 对象的共享

3.1 可见性

我们不仅希望防止某个线程正在使用对象状态而另一个线程在同时修改该状态,而且希望确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。因此就需要通过显式的同步或者类库中内置的同步来保证对象被安全地发布。

3.1.1 失效数据

3.1.2 非原子的64位操作

JVM允许将64位的读操作或写操作分解为两个32位的操作。当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值的低32位。因此,即使不考虑失效数据问题,在多线程程序中使用共享且可变的long和double等类型的变量也是不安全的,除非用关键字volatile来声明它们,或者用锁保护起来。

 

3.1.3 加锁与可见性

内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果。即后进入锁的线程能看到先进去的操作结果。体现了加锁的两个特征:互斥和可见。

 

3.1.4 Volatile变量

特殊域变量,每次重新计算。不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写人的值。

仅当Volatile能够简化代码实现和对同步策略的验证时才使用。他的正确使用包括:确保它们自身状态的可见性,确保它引用状态的可见性,以及标识一些重要程序的生命周期。

加锁机制既能保证可见性又能保证原子性,Volatile只能保证可见性。

 

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

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

B:该变量不会与其他状态变量一起纳人不变性条件中。

C:在访问变量时不需要加锁。

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

volatile boolean asleep;

while(!asleep)

countSomeSheep();

如果在验证正确性时,还需要对可见性进行复杂的判断,就不要使用了。

 

 

3.2 发布与溢出

发布(Publish):使对象能够在当前作用域之外的代码中使用。

对象发布的方式:

A:将对象的引用保存在其它代码能访问的地方

B:在一个非私有的方法中返回对象的引用

C:将对象的引用传递到其它类的方法中做参数。

 

发布对象:最简单方法是将对象的引用保存到一个公有的静态变量中:

如果将一个Secret对象添加到集合knownSecrets中,那么同样会间接发布这个对象。

 

溢出:当对象发布的时候,状态出现安全性,则称为溢出。

如果按照上述方式来发布states,就会出现问题,因为任何调用者都能修改这个数组的内

容。在这个示例中,数组states已经逸出了它所在的作用域,因为这个本应是私有的变量已经被发布了。

 

使用封装的最主要原因:

封装能够使得对程序的正确性进行分析变得可能,并使得无意中破坏设计约束条件变得更难。

3.2.1 this溢出

最后一种发布对象或其内部状态的机制就是发布一个内部的类实例

A: 在构造函数中new一个对象

当ThisEscape发布EventListener(new出的EventListener对象被保存)时,也隐含地发布了ThisEscape实例本身。因为在这个内部类的实例中包含了对ThisEscape实例的隐含引用。

在ThisEscape中给出了逸出的一个特殊示例,即this引用在构造函数中逸出。当内部的EventListener实例发布时,在外部封装的ThisEscape实例也逸出了。当且仅当对象的构造函数返回时,对象才处于可预测的和一致的状态(在构造函数未返回的这段时间ThisEscape都是溢出状态)。因此,当从对象的构造函数中发布对象时,只是发布了一个尚未构造完成的对象。即使发布对象的语句位于构造函数的最后一行也是如此。如果this引用在构造过程中逸出,那么这种对象就被认为是不正确构造。

 

B:在构造函数中调用一个可改写的实例方法时(既不是私有方法,也不是终结方法),同样会导致this引用在构造过程中逸出。

如果想在构造函数中注册一个事件监听器或启动线程,那么可以使用一个私有的构造函

数和一个公共的工厂方法(Factory Method),从而避免不正确的构造过程(只有当构造函数返回时,this引用才应该从线程中逸出。构造函数可以将this引用保存到某个地方,只要其他线程不会在构造函数完成之前使用它。)例如:

 

3.3 线程封闭

如果仅在单线程内访问共享的可变数据,就不需要同步。这种技术被称为线程封闭。它是实现线程安全性的最简单方式之一。

线程封闭技术的另一种常见应用是JDBC(Java Database Connectivity)的Connection对象。

JDBC规范并不要求Connection对象必须是线程安全的。

 

3.3.1 Ad-hoc多线程封闭(volatile)

维护线程封闭性的职责完全由程序实现来承担。Ad-hoc线程封闭是非常脆弱的,因为没有任何一种语言特性,例如可见性修饰符或局部变量,能将对象封闭到目标线程上。事实上,对线程封闭对象的引用通常保存在公有变量中。

当决定使用线程封闭技术时,通常是因为要将某个特定的子系统实现为一个单线程子系

统。在某些情况下,单线程子系统提供的简便性要胜过Ad-hoc线程封闭技术的脆弱性。 因此在程序中尽量少用它,在可能的情况下,应该使用更强的线程封闭技术(例如,栈封闭或ThreadLocal类)。

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

 

3.3.2 栈封闭

:存放8种基本类型的变量数据和对象的引用。

栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象(也被称为线程内部使用或者线程局部使用)。比Ad-hoc线程封闭更易于维护,也更加健壮。局部变量的固有属性之一就是封闭在执行线程中。它们位于执行线程的栈中,其他线程无法访问这个栈。

(因此方法中的局部变量需要保证线程安全的,一定要写上注解防止人为溢出。)

如果发布了对集合animals(或者该对象中的任何内部数据)的引用,那么封闭性将被破坏,并导致对象animals的逸出。

 

3.3.3 ThreadLocal类

局部变量ThreadLocal,是以空间换时间。 维持线程封闭性的一种更规范方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。这些特定于线程的值保存在Thread对象中,当线程终止后,这些值会作为垃圾回收。假设你需要将一个单线程应用程序移植到多线程环境中,通过将共享的全局变量转换为ThreadLocal对象(如果全局变量的语义允许),可以维持线程安全性。

ThreadLocal对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。

当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每次执行时都重新分配该临时对象,就可以使用这项技术。

 

 

3.4不变性

满足同步需求的另一种方法是使用不可变对象。如果某个对象在被创建后其状态就不能被修改,那么这个对象就称为不可变对象。不可变对象一定是线程安全的。

 

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

A:创建之后,状态不能修改

B:对象所有域都是final类型

C:创建对象时没有this溢出

 

在"不可变的对象"与"不可变的对象引用"之间存在着差异。保存在不可变对象中的程序状态仍然可以更新,即通过将一个保存新状态的实例来"替换"原有的不可变对象。

 

3.4.1 final域

Final类型的域是不能修改的(但如果final域所引用的对象是可变的,那么这些被引用的对象是可以修改的。在Java内存模型中,final域还有着特殊的语义。final域能确保初始化过程的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无须同步。

除非需要某个域有更高的可见性,否则应该声明为final域。

 

3.4.2 使用Volatile类型来发布不可变对象

OneValueCache是不可变的,并且在每条相应的代码路径中只会访问它一次。通过使用包含多个状态变量的容器对象来维持不变性条件,并使用一个volatile类型的引用来确保可见性,使得Volatile Cached Factorizer在没有显式地使用锁的情况下仍然是线程安全的。

 

3.5 安全发布

3.5.1 正确的对象被破坏。

发布之后,不同的线程构造出Holder之后看到的n可能不一样。

 

3.5.2 不可变对象与初始化安全性

因此Java内存模型为不可变对象的共享提供了一种特殊的初始化安全性保证。即使某个对象的引用对其他线程是可见的,也并不意味着对象状态对于使用该对象的线程来说一定是可见的。

这种保证还将延伸到被正确创建对象中所有final类型的域。在没有额外同步的情况下,也可以安全地访问final类型的域。然而,如果final类型的域所指向的是可变对象,那么在访问这些域所指向的对象的状态时仍然需要同步。

 

3.5.3 安全发布的常用模式

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

A:在静态初始化函数中初始化一个对象引用

B: 将时象的引用保存到volatile类型的域或者AtomicReferance对象中。

C: 将对象的引用保存到某个正确构造对象的final类型域中。

D: 将对象的引用保存到一个由领保护的域中。

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

 

线程安全库中的容器类提供了以下的安全发布保证:

A: 通过将一个键或者值放入Hashtable, synchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程(无论是直接访问还是通过迭代器访问)。

B: 通过将某个元素放人Vector, CopyOnWriteAzrayList, CopyOnWriteArraySet, synchronizedList或synclLronizedSet中,可以将该元素安全地发布到任何从这些容器中访问该元素的线程。

C: 通过将某个元素放入BlockingQueue或者ConcurrentLinkedQueue中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程。

 

3.5.4 事实不可变对象

如果对象从技术上来看是可变的,但其状态在发布后不会再改变,那么把这种对象称为"事实不可变对象(Effectively Immutable Object)"。在没有额外的同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对孰。例如:

public Map<String, Date> lastLOgin=

Collections.synchronizedMap(new HashMap<String, Date>());

如果Date对象的值在被放入Map后就不会再改变,那么synchronizedMap中的同步机制就足以使Date值被安全地发布,并且在访问这些Date值时不需要额外的同步。

 

3.5.5 可变对象

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

 

对象的发布需求决定了它的可见性:

A:不可变对象,可以通过任意机制发布

B:事实不可变对象必须安全发布

C:可变对象必须通过安全方式发布,并且必须是线程安全的或者由锁保护。

3.5.6 安全地共享对象

当获得对象的一个引用时,你需要知道在这个引用上可以执行哪些操作。在使用它之前是否需要获得一个锁?是否可以修改它的状态,或者只能读取它? 当发布一个对象时,必须明确地说明对象的访问方式。

在并发编程使用共享对象的时候,可以使用一些策略:

A:线程封闭:被封装在线程中的对象只能被这个线程拥有和修改。

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

C:线程安全共享:线程安全的对象在其内部实现同步,多个线程可以通过对象的公有方法访问而不需要额外的同步

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

 

以上是关于对象的共享的主要内容,如果未能解决你的问题,请参考以下文章

将共享对象链接到其他共享对象 C++ 项目

从另一个共享对象动态加载共享对象时的约束?

对象的使用和共享:ThreadLocal

在进程之间共享一个复杂的对象?

指向共享对象的指针在拥有它的不同对象中是不同的

多个线程访问共享对象和数据的方式