对象的共享

来源:互联网 发布:英文动画下载软件 编辑:程序博客网 时间:2024/05/21 08:41

本文主要介绍如何共享和发布对象

1.Synchronized同步代码块不仅是确保以原子的方式执行操作,而且保证内存的可见性。我们不仅希望防止某个线程正在使用对象而另一个线程在同时修改对象状态,而且希望确保党一个线程对对象状态修改了以后,其他线程能看到发生的状态的变化。

2.可见性

在没有的同步情况下,编译器处理器以及运行时都有可能对操作的顺序进行一些意想不到的调整(重排序),因此在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断几乎得不到正确的结论。因此一种简单的处理即只要数据在多个线程之间共享,就使用正确的同步。

(1)失效数据

在缺乏同步的程序中可能会产生的错误结果的一种情况:失效数据。失效数据的值是由之前某个线程设置的值,而不是一个随机值,这种安全性称作是最低安全性。

(2)非原子的64位操作

最低安全性适应于绝大多数的变量,除了非volatile类型的64位数值变量。Java内存要求变量的读取和写入必须是原子操作的,但是对于非volatile类型的long和double变量,jvm允许把64位的读写操作分解为两个32位的操作,当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值的低32位。因此,即使不考虑失效数据问题,在多线程程序中使用共享且可变的long和double等类型的变量也是不安全的,除非使用volatile来声明他们或者用锁保护起来。

(3)加锁与可见性

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

(4)volatile变量

volatile变量只能保证内存的可见性。把变量声明为volatile后,编译器和运行时都会注意到这个变量是共享的,因此不会把该变量上的操作和其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者其他队处理器不可见的地方,因此在读取volatile类型的变量时总是返回的是该变量的最新写入的值。

☆ volatile变量对可见性的影响比volatile变量本身更重要。线程A写入一个volatile变量并且线程B随后读取该变量,在写入volatile变量之前对A可见的所有变量的值,在B读取了volatile变量后,对B也是可见的。即在写入volatile变量相当于退出同步代码块,读取volatile变量相当于进入同步代码块。

☆ 不能过分的依赖volatile变量提供的可见性。volatile变量的典型用法是检查某个状态标记以判断是否退出循环。


☆volatile变量通常用作某个操作完成、发生中断或者状态的标志。当满足以下条件时才使用volatile变量(1)对变量的写入操作依赖于变量的当前值即只有单个线程更新变量的值(2)该变量不会与其他状态变量一起纳入不变性的条件中(3)访问变量时不需要加锁

3.发布与逸出

发布:发布对象指的是对象能够在当前作用域之外的代码中使用。许多情况中,我们要确保对象及其内部状态不被发布,发布内部状态有可能会破坏封装性。

逸出:当某个不应该发布的对象被发布时,这种情况就是逸出。

(1)当发布某个对象时,可能会间接的发布某个对象,比如List<Person> people=new ArrayList<Person>(),在发布people时,同样会把Person对象发布了。

(2)下面代码中的states虽然是个私有变量,但是任何调用者都能修改这个数组内的内容,数组states已经逸出了它所在的作用域,因为本应该是私有的变量已经被发布了。

当发布一个对象时,在该对象的非私有域中引用的所有对象同样会被发布。当某个对象逸出以后,必须假设有某个类或者线程会误用该对象。

(3)发布一个内部类:隐式的使this引用逸出

Source.java

public  class Source { public void show(Event event){ event.show1(); }}
Event.java

public interface Event {public void show1();}
Test1.java
/* * 验证隐式this溢出 * 会使Test1初始化还没有完成,就使用了,因此会导致错误 */public class Test1 {private  String name=null;public Test1(Source source) {// TODO Auto-generated constructor stubsource.show(new Event() {@Overridepublic void show1() {// TODO Auto-generated method stubTest1_show();//System.out.println("test");}});name="ltt";}public void Test1_show(){System.out.println(name.toString());}public static void main(String[] args){Source source=new Source();new Test1(source);}}
结果:


this引用在构造函数中逸出,即从对象的构造函数中发布对象时,只是发布了一个尚未构造完成的对象,只有当对象的构造函数返回时,对象才处于可预测的和一致的状态。如果this引用在构造器中逸出则该对象被认为是不正确的构造。因此不要在构造器中让this引用逸出。

使用工厂方式防止this逸出(使用一个私有的构造函数,一个公有的工厂方法):

/* * 工厂方式来防止this不被溢出 */public class Test2 {private String name=null;private static Event event=null;private Test2() {// TODO Auto-generated constructor stubevent=new Event() {@Overridepublic void show1() {// TODO Auto-generated method stubTest2_show();}};name="ltt";}public static Test2 getTest2(Source source){Test2 t2=new Test2();source.show(event);return t2;}public void Test2_show(){System.out.println(name.toString());}public static void main(String[] args){Source source=new Source();Test2.getTest2(source);}}
4.线程关闭

线程关闭即不共享数据。当某个对象封闭在一个线程中,这种用法将自动的实现线程安全性,即使被封闭的对象本身不是线程安全的。

(1)Ad-hoc技术脆弱,基本不使用。

(2)栈封闭,只能通过局部变量才能访问对象。局部变量就是封闭在执行线程中,它们位于执行线程的栈中,其他线程无法访问这个栈。对于基本类型的局部变量是线程安全的,无论如何都不会破坏栈封闭性,对于引用类型,为了维持栈的封闭性要确保饮用对象不会逸出。

(3)ThreadLocal类,这个类使得线程中某个值与保存值得对象关联起来,具体可以看我以前的文章ThreadLocal详解

5.不变性

(1)满足同步要求的另一种方法是使用不可变对象。如果某个对象被创建以后其状态不能被修改,则这个对象就被成为是不可变对象,不可变对象一定是线程安全的。不可变对象只有一种状态,并且该状态是由构造函数来控制的。

(2)当满足这些条件时,对象才是不可变的。(1)对象创建以后其状态不能修改(2)对象的所有域是final类型(3)对象是正确创建的,即构造函数中没有this逸出。

(3)在不可变对象的内部仍然可以使用可变对象来管理它们的状态,比如:private final Set<String> s=new HashSet<String>()。保存在不可变对象中的程序状态仍然可以更新,即通过将一个保存新状态的实例来替换原有的不可变对象。

(4)final域

final用于构建不可变对象,但是如果final引用的对象是可变的,那这些被引用的对象是可以修改的。

(5)使用volatile发布不可变对象

6.安全发布

(1)为了确保对象状态呈现出一致性,必须使用同步

(2)在不使用同步时发布不可变对象时必须满足不可变性的所有要求:状态不可变、final域、正确的构造过程。但是如果final域所指向的是可变对象,则在访问时仍需要同步。

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

                    ☆在静态初始化函数中初始化一个对象的引用:public static Hello hello=new Hello();

                    ☆把对象的引用保存到volatile类型或者AtomicReference对象中

                    ☆把对象的引用保存到某个正确构造对象的final域中

                    ☆把对象的引用保存到一个由锁保护的域中

(4)在线程安全容器内部的同步意味着把对象放入到某个线程安全的容器中,比如HashTable、Vectot、CopyOnWriteArraySet、ConcurrentLinkedQueue等

(5)事实不可变对象:在其状态发布后不会再改变的对象。在没有额外的同步下,任何线程都可以安全地访问事实不可变对象。

(6)对象的发布取决于他的可变性:

       ☆不可变对象可以通过任意机制来发布

       ☆事实不可变对象必须通过安全的方式来发布

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

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

       ☆线程关闭     ☆只读共享    ☆线程安全共享    ☆保护对象