对象的共享

来源:互联网 发布:影响力的重要性知乎 编辑:程序博客网 时间:2024/05/22 14:08

在单线程环境中,如果向某个变量写入值,然后在没有其他写入操作的情况下,读取这个变量,那么总能得到相同的值。然后,当读操作和写操作在不同的线程执行时,情况并非如此,我们无法确保执行读操作的线程能适时地看到其他线程写入的值。为了确保多个线程之间对内存写操作的可见性,必须使用同步机制。

在下列的程序中说明了当多个线程在没有同步的情况下共享数据时出现的错误。在代码中,主线程和读线程都将访问共享变量ready和number。主线程启动读线程,然后将number设为42,并将ready设为true。读线程一直循环到发现ready的值变为true,然后输出number的值。谁然看起来程序会输出42,但事实上可能会输出0或者根本无法终止。因为程序中没有使用足够的同步机制,因此无法保证主线程写入的ready值和number值对于读线程是可见的。

public class NoVisibility{    private static boolean ready;    private static int number;    private static class ReadThread extends Thread    {        public void run()        {            while(!ready)            {                Thread.yield();            }            System.out.println(number);        }    }    public static void main(String[] args)    {        new ReadThread().start();        number = 42;        ready = true;    }}

1. 失效数据

NoVisibility展示了缺乏同步的程序中可能产生错误结果的一种情况:失效数据。当读线程查看ready变量时,可能会得到一个已经失效的值。除非在每次访问变量时都使用同步,否则很可能获得该变量的失效值。

下列的程序MutableInteger不是线程安全的,因为get和set都是在没有同步的情况下访问value的。如果某个线程调用了set,那么灵一个正在调用get的线程可能会看到更新后的value值,也可能看不到。

public class MutableInteger {    private int value;    public int get()    {        return value;    }    public void set(int value)    {        this.value = value;    }}

在下面的SynchronizedInteger中,通过对get和set等方法进行同步,可以使MutableInteger成为一个线程安全的类。仅对set方法进行同步是不够的,调用set的线程仍然会看见失效值。

public class SynchronizedInteger {    private int value;    public synchronized int get()    {        return value;    }    public synchronized void set(int value)    {        this.value = value;    }}

2. 加锁和可见性

内置锁可以确保某个线程以一种可预测的方式来查看另一个线程的执行结果,如下图所示。当线程B执行由锁保护的代码块时,可以看到线程A之前在同段代码的操作结果。加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。

这里写图片描述

3. Volatile变量

volatile是一种比synchronized关键字更轻量级的同步机制。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的。当线程A首先写入一个volatile变量并且线程B随后读取该变量时,A线程写入变量的值对于B线程来说是可见的。下面的程序展示了volatile变量的一种典型用法:检查某个编辑以判断是否退出循环。

volatile boolean asleep;...    while(!asleep)    {        doSomething();    }

虽然volatile变量很方便,但也存在一些局限性。volatile变量通常用做某个操作完成、发生中断或者状态的标志。volatile变量并不能确保原则性,例如递增操作。volatile只能确保变量的可见性,而加锁机制可以确保可见性和原子性。

当且仅当瞒住以下所有条件时,才应该使用volatile变量。

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

    • (2) 该变量不会与其他变量一起列入不变性条件。

    • (3) 在访问变量时不需要加锁。

4. 发布和逸出

“发布”一个对象的意思是指,使对象能够在当前作用域之外的代码中使用。如,将一个指向该对象的引用保存到其他代码可以访问的方法,或者在某一个非私有的方法内返回该对象,或者将引用传递到其他类的方法中。当某个不应该发布的对象被发布时,这种情况就被称为逸出。

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

public static Set<Secret> knownSecrets;public void initialize(){    knownSecrets = new HashSet<Secret>();}

当发布某个对象时,可能会间接地发布其他对象。如果将Secret对象添加到集合knownSecrets中,那么同样会发布这个对象,因为任何代码都可以遍历这个集合。并获得这个Secret对象的引用。同样,如果从非私有方法中返回一个引用,那么同样会发布返回对象。

下列的程序展示了一种错误的发布方式,如果按下述方式来发布states,就会出现问题。因为任何调用者都能修改这个数组的内容。在这个示例中,数组states已经移除了它所在的作用域。因为这个本应该是私有的变量已经发布了。

class UnsafeStates{    private String[] states = new String[]{"AK", "AL"};    public String[] getStates()    {        return states;    }}

最后一种发布对象或其内部状态的机制就是发布一个内部的类实例,如下列程序所示。当ThisEscape发布EventListener时,也隐含的发布了ThisEscape实例本身,因为在这个内部类的实例中,包含了对ThisEscape实例的隐含引用。

public class ThisEscape{    public ThisEscape(EventSource source){        source.registerListener{            new EventListener(){                public void onEvent(Event e){                    doSomething(e);                }            }        }    }}

5. 不要在构造过程中使this引用逸出

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

使用工厂方法来防止this医用在构造函数中逸出,如下列所示。

public class SafeListener{    private final EventListener listener;    private SafeListener(){        listener = new EventListener(){            public void onEvent(Event e){                doSomething(e);            }        }    }} public static SafeListener newInstance(EventSource source){    SafeListener safe = new SafeListener();    source.registerListerner(safe.listener);    return safe;}

6. 栈封闭

栈封闭式线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。正如封装能使得代码更容易维持不变性条件那样,同步变量也能使对象更易于封闭在线程中,局部变量的固有属性之一就是封闭在执行线程中。它们位于执行线程的栈中,其他线程无法访问栈。

对于基本类型的局部变量,如下列所示,无论如何都不会破坏栈封闭。由于任何方法都无法获得对基本类型的引用。

public int loadTheArk(Collection<Animal> candidates){    SortedSet<Animal> animals;    int numPairs = 0;    Animal candidate = null;    //animals被封闭在方法中,不要使它们逸出    animals = new TreeSet<Animal>(new SpecialGenderComparator());    animals.addAll(candidates);    for(Animal a : animals){        if(candidate == null || !candidate.isPotentialMate(a)){            candidate = a;        }else{            ark.load(new AniamlPair(candidate, a));            ++numPairs;            candidate = null;        }    }    return numPairs;}

7. ThreadLocal类

维持线程封闭性的一种更规范方法是使用ThreadLocal,这个类使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get和set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。如下列程序所示。当某个线程初次调用ThreadLocal.get方法时,就会调用initialValue来获取初始值。

private static ThreadLocal<Connection> connectionHolder     = new ThreadLocal<Connection>(){        public Connection initalValue(){            return DriverManager.getConnection(DB_URL);        }    }public static Connection getConnection(){    return connectionHolder.get();

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

满足同步需求的另一种方法是使用不可变对象,如果某个对象在创建后其状态就不能被修改,那么这个对象就称为不可变对象。线程安全性是不可变对象的固有属性之一。它们的不变性条件是由构造函数创建的,只要它们的状态不改变,那么这些不变形条件就能得以维持。

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

  • 对象创建以后其状态就不能修改。
  • 对象的所有域都是final类型。
  • 对象是正确创建的(在对象创建过程期间,this引用没有逸出)

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

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 isStooges(String name){        return stooges.contains(name);    }}

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

因式分解Servlet将执行两个原子操作:更新缓存的结果,以及通过判断缓存中的数值等于请求的数值来决定是否直接读取缓存中的因式分解结果。每当需要对一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据。如下列程序所示。

class OnValueCache{    private final BigInteger lastNumber;    private fianl BigInteger[] lastFactors;    public OneValueCache(BigInteger i, BigInteger factors){        lastNumber = i;        lastFactors = Arrays.copyOf(factors, factors.length);    }    public BigInteger[] getFactors(BigInteger i){        if(lastNumber == null || !lastNuber.equals(i)){            return null;        }else{            return Arrays.copyOf(lastFactors, lastFactors.length);        }    }}
public class VolatileCachedFactorizer implements Servile{    private volatile OnValueCache cache = new OnValueCache(null, null);    public void Service(ServletRequest req, ServletResponse resp){        BigInteger i = extractFromRequest(req);        BigInteger[] factors = cache.getFactors(i);        if(factors == null){            factors = factor(i);            cache = new OnValueCache(i, factors);        }        encodeIntoResponse(resp, factors);    }}

10. 安全发布

某些情况下,我们希望在多个线程间共享某个对象,此时必须确保安全的共享。如果只是像下列程序将对象的引用保存到公有域下,那么还不足以安全地发布这个对象。除了发布对象的线程外,其他线程看到的Holder域有可能是一个失效值。Object的构造函数会在子类的构造函数运行之前先将默认值写入所有的域。因此,某个域的默认值可能视为失效值。

public Holder holder;private int n;public Holder(int n){    this.n = n;}public void assertSanity(){    if(n != n){        throw new AssertionError("This statement is false.");    }}

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

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

    在线程安全容器内部的同步意味着,在将对象放入到某个容器,将满足上述的最后一条需求。

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

  • 通过将某个元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或synchronizedSet中,可以将该元素安全地发布到任何从这些容器访问该元素地线程。
  • 通过将某个元素放入BlockingQueue或者ConcurrentLinekdQueue中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程。

通常,要发布一个静态构造的对象,最简单和最安全的方式是使用静态的初始化器,静态初始化器由JVM在类的初始化阶段执行,由于在JVM内部存在着同步机制,因此通过这种方式初始化的任何对象都可以被安全地发布:
public staitc Holder holder = new Holder(42);

0 0