从0学习java并发编程实战-读书笔记-对象的共享(2)

要编写正确的并发程序,关键在于:
在访问共享的可变状态时,须要进行正确的管理。

可见性

public class NoVisibility {
    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread {
        public void run() {
            while (!ready) {
                Thread.yield();
                System.out.println(number);
            }
        }
    }
    
    public static void main(String[] args) {
            new ReaderThread().start();
            new ReaderThread().start();
            new ReaderThread().start();
            number = 42;
            ready = true;
    }
}
  • 这段代码可能出现的结果java

    • 输出0: 由于ReaderThread可能看到了ready的值,但却没看到number的值。
    • 持续的循环下去:由于可能很长时间内,ReaderThread都没法看到ready的值。
    • 输出42: ReaderThread同时看到了number和ready的值。
在没有同步的状况下,编译器,处理器以及运行时等均可能对操做的执行顺序进行一些意想不到的调整,在缺少足够同步的多线程程序中,要想对内存操做执行顺序进行判断,几乎没法得出正确的结论。

失效数据

当ReaderThread查看ready变量时,可能会获得一个已经失效的值,并且失效值可能不会同时出现:一个线程可能得到了某个变量的最新值,而得到了另外一个变量的失效值。
  • 在SynchronizedInteger中,经过对get和set方法进行同步,使其成为一个线程安全类(须要将对象getter/setter方法都进行同步)。

非原子的64位操做

最低安全性:在没有进行同步时读取某个变量,可能会获得一个失效值,但这个值至少是由以前某个线程设置的,而非随机值。这种安全性保证也被称为最低安全性。
  • 不符合最低安全性的变量:非volatile类型的64位数值变量(double和long)
因为Java内存模型要求,变量的读取操做和写入操做必须都是原子操做,但对于非volatile类型的long和double变量,JVM容许将64位的读操做或写操做分解为两个32位操做。
  • 因此当读取一个非volatile类型的long变量时,若是对该变量的读操做和写操做在不一样的线程中进行,那么可能会只读取到这个变量的高32位或者低32位。
  • 因此在多线程中,double和long须要用volatile声明,或者用锁保护起来。

加锁和可见性

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

volatile变量

  • java提供了一种稍弱的同步机制:volatile变量,用来确保将变量的更新操做通知到其余线程。当把变量声明位volatile类型后,编译器与运行时都会足以到这个变量是共享的。所以不会将该变量上的操做与其余内存操做一块儿重排序。volatile变量不会被缓存在寄存器或者其余对处理器不可见的地方,所以在读取volatile类型的变量时总会返回最新写入的值。
  • 从内存的可见性来看,读取volatile至关于进入同步代码块,写入volatile变量至关于退出同步代码块。
  • 可是不建议过分使用volatile提供的可见性,若是代码中依赖volatile变量来控制可见性,一般比使用锁的代码更脆弱,也更难理解。
  • volatile正确的使用方式:确保它们自身状态的可见性确保它们所引用对象状态的可见性,以及表示一些重要的声明周期事件的发生(例如初始化,关闭,循环退出条件等。)
/**
* 数绵羊
*/
volatile boolean asleep;
while(!asleep){
    countSomeSheep();
}
  • 加锁机制既能保证可见性,又能够确保原子性。而volatile变量只能保证可见性
  • 当且仅当知足如下全部条件时,才应该使用volatile变量:编程

    • 对变量的写入操做不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
    • 该变量不会与其余状态变量一块儿归入不变性条件中。
    • 在访问变量时不须要加锁。

发布与溢出

  • 发布(publish)一个对象,指的是对象可以在当前做用域以外的代码中使用。数组

    • 例如,将一个指向该对象的引用缓存

      • 保存到其余代码能够访问的地方
      • 或者在某一个非私有的方法中返回该引用。
      • 或者即哪一个引用传递到其余类的方法中。
  • 但若是在发布时要确保线程安全性,则可能须要同步。发布内部状态可能会破坏封装性,并使程序难以维持不变性条件。安全

    • 例如在对象构造完成以前就发布该对象,就会破坏线程安全性。
    • 当某个不该该发布的对象被发布时,这种状况就被称为逸出(Escape)
public class UnsafeState {

    private String[] states = new String[]{
            "A","B","C","D","E"
    };
    public String[] getStates(){
        return states;
    }
}
  • 任何调用者均可以修改states里的内容,数组states已经逸出了它所在的做用域:由于做为私有变量的内容已经被发布了。
  • 当发布一个对象时,在该对象的非私有域中的对象一样会被发布。
  • 若是一个已经发布的对象可以经过非私有的变量引用和方法调用到达其余对象,那么这些对象也会被发布。
  • 当某个对象逸出后,你必须假设有某个类或者线程在误用该对象。这正是使用封装最主要的缘由:封装可以使得对程序的正确性进行分析变得可能,并使得无心中破坏设计约束条件变得更难。

安全的对象构造过程

  • 不要在构造过程当中使this引用逸出。
  • 当且仅当对象的构造函数返回时,对象才处于可预测和一致的状态。

线程封闭

  • 当访问共享的可变数据时,一般须要使用同步。
  • 避免使用同步的方式就是不共享数据,若是仅在当线程内访问数据,就不须要同步。这种技术就被称为线程封闭(Thread Confinement)
  • JDBC的Connection对象就使用了线程封闭技术。在典型的服务器应用程序中,线程从JDBC链接池中得到一个Connection对象,而且用该对象来处理请求,使用完后再将对象返回给链接池。服务器

    • 因为大多数请求(如Servlet)都是由单线程采用同步的方式来处理,而且在Connection对象返回以前,链接池不会再将它分给其余线程。所以这种链接管理模式实际上是隐式的将Connection对象封闭在线程中。

Ad-hoc线程封闭

Ad-hoc线程封闭是指,维护线程封闭性的职责所有由程序实现来承担。
  • Ad-hoc线程封闭是很是脆弱的,由于没有一种语言特性,例如可见性修饰符或局部变量,可以将对象封闭到目标线程上。
  • 当决定使用线程封闭技术时,一般是由于要将某个特定的子系统实现为一个单线程子系统。在某些状况下,单线程子系统提供的简便性要赛过Ad-hoc线程封闭技术的脆弱性。
  • 因为Ad-hoc的脆弱性,所以在程序里尽可能少使用,尽量的使用更强的线程封闭技术(如栈封闭和ThreadLocal类)

栈封闭

栈封闭是线程封闭的一种特例,在栈封闭中,只能经过局部变量才能访问对象。局部变量的特性之一就是封闭在执行线程中。它们位于执行线程的栈中,其余线程没法访问这个栈。

ThreadLocal类

维护线程封闭性的一种更规范方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get/set等访问接口和方法,

ThreadLocal是什么

ThreadLocal是一个建立线程局部变量的类。多线程

使用了ThreadLocal建立的变量只能被当且线程访问,其余线程没法访问和修改。并发

ThreadLocal的使用

private void testThreadLocal() {
    Thread t = new Thread() {
        ThreadLocal<String> mStringThreadLocal = new ThreadLocal<>();

        @Override
        public void run() {
            super.run();
            mStringThreadLocal.set("123");
            mStringThreadLocal.get();
        }
    };

    t.start();
}

为ThreadLocal设置初始值的话,则须要重写initialValue方法:框架

ThreadLocal<String> mThreadLocal = new ThreadLocal<String>() {
    @Override
    protected String initialValue() {
      return Thread.currentThread().getName();
    }
};

对象存放

本质上ThreadLocal是在堆上建立对象,可是将对象引用持有在线程的栈内存上。ide

许多事务性的框架功能,经过将事务的上下文保存在静态的ThreadLocal对象中,当须要判断是哪个事务时,只须要从ThreadLocal对象中读取事务上下文便可。

不变性

知足同步需求的另外一种方法是使用不可变对象(Immutable Object),以前的例如获得失效数据,丢失更新操做或者观察到某个对象处于不一致的状态等问题,都与多线程试图同时访问一个可变变量有关,若是这个变量是不可变的,那么这些问题也就天然消失了。

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

对象不可变的条件

当知足如下条件时,对象才是不可变的:

  • 对象建立后其状态就不可修改。
  • 对象的全部域都是final类型。
  • 对象是正确建立的(对象建立期间,this引用没有逸出)

final域

关键字final用于构造不可变对象。final类型的域是不可修改的,但若是final域所引用的对象是可变的,那么这些被引用的对象是能够修改的。

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

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

安全发布

在某些状况下,咱们须要在多个线程之间共享对象,此时必须确保安全地进行共享

不正确的发布

不能期望一个未被彻底建立的对象拥有完整性。

public class Holder {
    private int n;

    public Holder(int n) {
        this.n = n;
    }

    public void assertSanity() {
        if (n != n) {
            throw new AssertionError("this statement is false");
        }
    }
}

在发布Holder的线程发布完成以前,Holder域是个失效值,此时的n多是空引用。

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

Java内存模型对不可变对象的共享提供了一种特殊的初始化安全性保证。

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

若是final类型的域所指向的是可变对象,那么在访问这些域所指向的对象的状态时仍需同步。

安全发布的经常使用模式

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

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

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

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

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

public static Holder holder = new Holder(1);
因为静态初始化构造器由JVM在类的初始化阶段执行,在JVM内部存在着同步机制,所以经过这种方式初始化的任何对象均可以被安全的发布。

事实不可变对象

  • 即使对象从技术上来看是可变的,可是其状态在发布后不会被改变,就是事实不可变对象(Effectively Immutable Object)
  • 在没有额外同步的状况下,任何线程均可以安全地使用被安全发布的事实不可变对象。

可变对象

  • 若是对象在构造后能够被修改,那么安全发布只能保证发布当时的可见性。对象的发布需求取决于它的可变性:

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

安全的共享对象

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

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