【Java并发.3】对象的共享

  本章将介绍如何共享和发布对象,从而使他们可以安全地由多个线程同时访问。这两章合在一块儿就造成了构建线程安全类以及经过 java.util.concurrent 类库来构建开发并发应用程序的重要基础。html

3.1  可见性java

  可见性是一种复杂的属性,由于可见性中的错误老是违背咱们的直觉。为了确保多个线程之间对内存写入操做的可见性,必须使用同步机制数据库

  在下面的清单中 NoVisibility 说明了当多个线程在没有同步的状况下共享数据出现的错误。主线程启动读线程,而后将 number 设为 42,并将 ready 设为 true。读线程一直循环直到发现 ready 的值变为 true,而后输出 number 的值。虽然看起来会输出 42,但事实上可能输出 0,或者根本没法终止。这是由于代码中没有使用足够的同步机制,所以没法保证主线程写入的ready 值和 nunber 值对于读线程来讲是可见的。编程

public class NoVisibility {                    【皱眉脸-不要这样作private static boolean ready;
    private static int number;

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

  NoVisibility 可能会持续循环下去,由于读线程可能永远都看不到 ready 值。一种更奇怪的现象是,NoVisibility 可能会输出 0,由于读线程可能看到了写入 ready 值,但却没有看到以前写入 number 值,这种现象称为“重排序(Reordering)”。(注释:这看上去彷佛是一种失败的设计,但倒是使 JVM 充分地利用现代多核处理器的强大性能。)数组

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

 

3.1.1  失效数据安全

  NoVisibility 展现了在缺少同步的程序中可能产生错误结果的一种状况:失效数据。当读线程查看 ready 变量时,可能会获得一个已经失效的值。除非在每次访问变量时都使用同步,不然极可能得到该变量的一个失效值。更糟糕的是,失效值可能不会同时出现:一个程序可能得到某个变量的最新值,而得到另外一个变量的失效值。数据结构

  失效数据还可能致使一些使人困惑的故障,例如意料以外的异常、被破坏的数据结构、不精确的计算以及无限循环等。多线程

  在以下程序清单 Mutableinteger 不是线程安全的,由于 get 和 set 都是没有同步的状况下访问 value 的。若是某个线程调用了 set,那么另外一个在调用的get 线程可能会看到更新后的值,也可能看不到。并发

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

  在程序清代 SynchronizedInteger 中,经过对 get 和 set 方法进行同步,可使MutableInteger 成为一个线程安全的类。仅对 set 方法进行同步时不够的,调用 get 线程仍然会看到失效值。ide

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

 

3.1.2  非原子的64位操做

  忽略。。。

 

3.1.3  加锁与可见性

  内置锁能够用于确保某个线程以一种可预测的方式来查看另外一个线程的执行结果。对于同一个锁,后面进入锁的线程能够看到以前线程在锁中的全部操做结果(加锁能够保证可见性)。

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

 

3.1.4  Volatile变量

  对于volatile 关键字的详细介绍,建议你们去仔细观看 volatile关键字解析 ,因此在这不作介绍。

 

3.2  发布与逸出

  “发布(Publish)”一个对象的一块儿是指,是对象可以在当前做用域以外的代码中使用。例如,将一个指向该对象的引用保存到其余代码能够访问的地方,或者在某一个非私有的方法中返回该引用,或者将引用传递到其余类的方法中。在许多状况中,咱们要确保对象及其内部状态不被发布。而在某些状况下,咱们又须要发布某个对象,但若是在发布时要确保线程安全性,则可能须要同步。当某个不该该发布的对象被发布时,这种状况就被称为逸出(Escape)

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

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

  程序清单:是内部的可变状态逸出:

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

  如何按照上述方式来发布 states,就会出现问题,由于任何调用者都能修改这个数组的内容。在这个实例中,数组 states 已经逸出了它所在的做用域,由于这个本应是私有的变量已经被发布了。

  当发布一个对象时,在该对象的非私有域中引用的全部对象一样会被发布。通常来讲,若是一个已经发布的对象可以经过非私有的变量引用和方法调用到达其余的对象,那么这些对象也都会被发布。

 

3.3  线程封闭

  当访问共享的可变数据时,一般须要使用同步。一种避免使用同步的方式就是不一样享数据。若是仅在单线程内访问数据,就不须要同步。这种技术称为线程封闭(Thread Confinement),它是实现线程安全性的最简单方式之一。

  线程封闭技术的常见应用时 JDBC 的 Connection 对象。线程从链接池中得到一个 Connection 对象,而且用该对象来处理请求,使用完后再将对象返还给链接池。因为大多数请求都是由单个线程采用同步的方式来处理,而且在 Connection 对象返回以前,链接池不会再将它分配给其余线程,所以,这种链接管理模式在处理请求时隐含地将 Connection 对象封闭在线程中。

 

3.3.1  Ad-hoc线程封闭

  略...

 

3.3.2  栈封闭

  栈封闭式线程封闭的一种特例,在栈封闭中,只能经过局部变量才能访问对象。局部变量的固有属性之一就是封闭在执行线程中。它们位于执行线程的栈中,其余线程没法访问这个栈。栈封闭(也被称为线程内部使用或者线程局部使用,不要与核心类库中的 ThreadLocal 混淆)。

  对于基本类型的局部变量,以下程序清单中 loadTheArk 方法的 numPairs,不管如何都不会破坏栈封闭性,因为任何方法都没法得到基本类型的引用,所以Java 语言的这种语义就确保了基原本兴的局部变量始终封闭在线程内。

public int loadTheArk(Collection<Animal> candidates) {
        SortedSet<Aniaml> animals;
        int numPairs = 0;   //基本类型的局部变量
        Aniaml candidate = null;
        // animals 被封闭在方法中,不要使它们逸出
        animals = new TreeSet<Animal>(new SpeciesGenderComparator());
        animals.addAll(candidates);
        for (Animal a : animals) {
            numPairs++;
        }
        return numPairs;
}

 

3.3.3  ThreadLocal 类

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

  ThreadLocal 对象一般用于放置对可变的单实例变量(Singleton)或全局变量进行共享。例如,在单线程应用程序中可能会维持一个全局的数据库链接,并在程序启动时初始化这个链接对象,从而避免在调用每一个方法时都要传递一个 Connection 对象。

  private static ThreadLocal<Connection> connectionThreadLocal = new ThreadLocal<>() {
        @Override
        protected Object initialValue() {
            return DriverManager.getConnection(URL);
        }
    }
    
    public static Connection getConnection() {
        return connectionThreadLocal.get();
    }

  当某个线程初次调用 ThreadLocal.get 方法时,就会调用 initialValue 来获取初始值。从概念上看,你能够将 ThreadLocal<T> 视为包含了 Map<Thread, T> 对象,其中保存了特定于该线程的值,但 ThreadLocal 的实现并不是如此。这些特定于线程的值保存在 Thread 对象,当线程终止后,这些值会做为垃圾回收

 

3.4  不变性

  知足同步需求的另外一种方法时使用不可变对象。到目前为止,咱们介绍了许多与原子性和可见性相关的问题,例如获得失效数据,丢失更新操做或者观察到某个对象处于不一致的状态等等,都与多线程试图同时访问同一个可变的状态相关。若是对象的状态不会改变,那么这些问题与复杂性也就天然消失了。

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

  虽然在Java 语言规范和 Java 内存模型中都没有给出不可变性的正式定义,但不可变性并不等于将对象中全部的域都声明为 final 类型,即便对象中全部的域都是 final 类型的,这个对象也仍然是可变的,由于在 final 类型的域中能够保存对可变对象的引用。

当知足如下条件时,对象才是不可变的:
  • 对象建立之后其状态不可能修改。
  • 对象的全部域都是 final 类型。
  • 对象时正确建立的(在对象的建立期间, this 引用没有逸出)。

  看个例子:在可变对象基础上构建的不可变类

public class ThreeStooges {
    private final Set<String> stooges = new HashSet<>();
    public ThreeStooges() {
        stooges.add("one");
        stooges.add("two");
        stooges.add("three");
    }
    public boolean isStooge(String name) {
        return stooges.contains(name);
    }
}

 

3.4.1  Final 域

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

正如“除非须要更高的可见性,不然应将全部的域都声明为私有域”是一个良好的编程习惯,“除非须要某个域是可变的,不然应将其声明为 final 域”也是一个良好的编程习惯。

 

3.4.2  示例:使用 volatile 类型来发布不可变对象

  对于volatile 关键字的详细介绍,建议你们去仔细观看 volatile关键字解析 ,因此在这不作过多介绍。贴一个代码:

public class VolatileCachedFactorizer implements Servlet {
    private volatile OneValueCache cache = new OneValueCache(null, null);
    public void service(ServletRequest request, ServletResponse response) {
        BigInteger i = extractFromRequest(request);
        BigInteger[] factors = cache.getFactors(i);
        if (factors == null) {
            factors = factor(i);
            cache = new OneValueCache(i, factors);
        }
        encodeIntoResponse(response, factors);
    }
}

 

3.5  安全发布

  到目前为止,我么重点讨论的是如何确保对象不被发布,例如让对象封闭在线程或另外一个对象的内部。固然,在某些状况下咱们但愿多个线程间共享对象,此时必须确保安全地进行共享。

  以下:在没有足够同步的状况下发布对象(不要这样作)

//不安全的发布
public Holder holder;
public void initialize() {
    holder = new Holder(42);
}

  因为可见性问题,其余线程看到的 Holder 对象将处于不一致的状态,即使在该对象的构建函数中已经正确地构建了不便性条件。这种不正确的发布致使其余线程看到还没有建立完成的对象。

 

3.5.1  不正确的发布:正确的对象被破坏

  你不能期望一个还没有被彻底建立的对象拥有完整性。某个观察该对象的线程将看到对象处于不一致的状态,而后看到对象的状态忽然发生变化,即便线程在对象发布后尚未修改过它。

  以下:因为未被正确发布,所以这个类可能出现故障

public class Holder {
    private int n;
    public Holder(int n) {
        this.n = n;
    }
    public void assertSanity() {
        if(n != n)    //这句没看懂,就算同步时会出现 n 极可能成为失效值,可是难道 (n != n)不是原子操做?求解。
        throw new AssertionError("this statement is false");
    }
}

 

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

   因为不可变对象是一种很是重要的对象,所以Java 内存模型为不可变对象的共享提供了一种特殊的初始化安全性保障。

任何线程均可以在不须要额外同步的状况下安全地访问不可变对象,即便在发布这些对象时没有使用同步。

 

3.5.3  安全发布的经常使用模式

    要安全地发布一个对象,对象的引用以及对象的状态必须同时对其余线程可见。一个正确构造的对象能够经过如下方式来安全地发布:
  • 在静态初始化函数中初始化一个对象引用。
  • 将对象的引用保存到 volatile 类型的域或者 AtomicReferance 对象中
  • 将对象的引用保存到某个正确构造对象的 final 类型域中。
  • 将对象的引用保存到一个由锁保护的域中。

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

  • 经过将一个键或者值放入 Hashtable、synchronizedMap 或者 ConcurrentMap 中,能够安全地将它发布给任何从这些同期中访问它的线程(不管是直接访问仍是经过迭代器访问)
  • 经过将某个元素放入 Vector、CopyiOnWriteArrayList、CopyOnWriteArraySet、synchronizedListsynchronizedSet 中,能够将该元素安全地发布到任何从这些容器中访问该元素的线程。
  • 经过将某个元素放入 BlockingQueue 或者 ConcurrentLinkedQueue 中,能够将该元素安全地发布到任何从这些队列中访问该元素的线程。

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

public static Holder holder = new Holder(42);

 

3.5.4  事实不可变对象

  若是对象在发布后不会被修改,那么 程序只需将它们视为不可变对象便可。

在没有额外的同步状况下,任何线程均可以安全地使用被安全发布的事实不可变对象。

  例如,Date 自己是可变的,但若是将它做为不可变对象来使用,那么在多个线程之间共享 Date 对象时,就能够省去对锁的使用。假设须要维护一个 Map 对象,其中保存了每位用户的最近登陆时间:

public Map<String, Date> lastLogin = Collections.synchronizedMap(new HashMap<String, Date>());

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

 

3.5.5  可变对象

  对于可变对象,不只在发布对象时须要使用同步,并且在每次对象访问时一样须要使用同步来确保后续修改操做的可见性。

    对象的发布须要取决于它的可变性:
  • 不可变对象能够经过任何机制来发布
  • 事实不可变对象必须经过安全方式来发布。
  • 可变对象必须经过安全方式来发布,而且必须是线程安全的或者由某个锁保护起来。

 

3.4.5  安全地共享对象

  当发布一个对象时,必须明确地说明对象的访问方式。

    在并发程序中使用和共享对象时,可使用一些实用的策略包括:
  线程封闭:线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,而且只能由这个线程修改。
  只读共享:在没有额外同步的状况下,共享的只读对象能够由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
  线程安全共享:线程安全的对象在其内部实现同步,所以对个线程能够经过对象的公有接口来进行访问而不须要进一步的同步。
  保护对象:被保护的对象只能经过持有特定的锁来访问。保护对象包括封装在其余线程安全对象中的对象,以及已发布的而且由某个特定锁保护的对象。
相关文章
相关标签/搜索