《Java并发编程》第三章 — 对象的共享 — 读书笔记

    咱们已经知道了同步代码块和同步方法能够确保以原子的方式执行操做,但一种常见的误解是,认为关键字synchronized智能用于实现原子性和肯定“临界区(Critical Section)”。同步还有另外一个重要的方面:内存可见性(Memory Visibility)。咱们不只但愿防止某个线程正在使用对象状态而另外一个线程在同时修改该状态,并且但愿确保当一个线程修改了对象状态后,其余线程可以看到发生的状态变化。若是没有同步,那么这种状况就没法实现。你能够经过显式的同步或者类库中内置的同步来确保对象被安全的发布。java

3.1 可见性

    可见性是一种复杂的属性,由于可见性中的错误老是会违背咱们的直觉。在单线程环境中,若是向某个变量先写入值,而后在没有其余写入的状况下读取这个变量,那么总能获得相同的值。然而,当读取操做和写入操做在不一样的线程中执行时,状况却并不是如此。一般,咱们没法确保执行读取操做的线程能适时地看到其余线程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操做的可见性,必须使用同步机制,例如:程序员

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) {
		// 从代码上来看,先执行run函数,在设置number和ready的值
		new ReaderThread().start();
		number = 42;
		ready = true;
	}
}

    NoVisibility可能会持续循环下去,由于读线程可能永远都看不到ready的值。一种更奇怪的现象是,NoVisibility可能会输出0,由于读线程可能看到了写入ready的值,但却没有看到以后写入number的值,这种现象被称为“重排序(Reordering)”。只要在某个线程中没法检测到重排序状况,那么就没法确保线程中的操做将按照程序中指定的顺序来执行。当主程序先写入number,而后在没有同步的状况下写入ready,那么读线程看到的顺序可能与写入的顺序彻底相反。数组

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

3.1.1 失效数据

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

// 线程不安全
public class MutableInteger {
	private int value;

	public int getValue() {
		return value;
	}

	public void setValue(int value) {
		this.value = value;
	}
}

    MutableInteger不是线程安全的,由于get和set都是在没有同步的状况下访问value的。若是某个线程调用了set,那么另外一个正在调用get的线程可能会看到更新后的value值,也可能看不到。下面将该类改写成线程安全的:多线程

// 线程安全
public class MutableInteger {
	private int value;

	public synchronized int getValue() {
		return value;
	}

	public synchronized void setValue(int value) {
		this.value = value;
	}
}

    经过对get和set等方法进行同步,可使MutableInteger成为一个线程安全的类。仅仅对set方法进行同步是不够的,调用get的线程仍然会看到失效值。并发

3.1.2 非原子的64位操做

    当线程在没有同步的状况下读取变量时,可能会获得一个失效值,但至少这个值是由以前某个线程设置的值,而不是一个随机值。这种安全性保证也被称为最低安全性(out-of-thin-airsafety)。框架

    最低安全性适用于绝大多数变量,可是存在一个例外:非volatile类型的64位数值变量(double和long)。Java内存模型要求,变量的读取操做和写入操做都必须是原子操做,但对于非volatile类型的long和double变量,JVM容许将64位的读操做或写操做分解为两个32位的操做。当读取一个非volatile类型的long变量时,若是对该变量的读操做和写操做在不一样的线程中执行,那么极可能会读取到某个值的高32位和另外一个值的低32位。所以,即便不考虑失效数据问题,在多线程程序中使用共享且可变的long和double等类型的变量也不是安全的,除非用关键字volatile来声明他们,或者用锁保护起来。函数

3.1.3 加锁与可见性

    在访问某个共享且可变的变量时要求全部线程在同一个锁上同步,确保某个线程写入该变量的值对于其余线程来讲都是可见的。不然,若是一个线程在未持有正确锁的状况下读取某个变量,那么读到的多是一个失效值。this

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

3.1.4 Volatile变量

    Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操做通知到其余线程。当把变量声明为volatile类型后,编译器在运行时都会注意到这个变量是共享的,所以不会将该变量上的操做与其余内存操做一个重排序。volatile变量不会被缓存在寄存器或者对其余处理器不可见的地方,所以在读取volatile类型的变量时总会返回最新写入的值。

    注意,在访问volatile变量时不会执行加锁操做,所以也就不会使执行线程阻塞,所以volatile变量是一种比sychronized关键字更轻量级的同步机制。

    volatile变量对可见性的影响比volatile变量自己更为重要。从内存可见性的角度来看,写入volatile变量至关于退出同步代码块,而读取volatile变量至关于进入同步代码块。然而,并不建议过分依赖volatile变量提供可见性。

    仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。若是在验证正确性时须要对可见性进行复杂的判断,那么就不要使用volatile变量。

    volatile变量的正确使用方式包括:确保他们自身的状态的可见性,确保他们所引用的对象的状态的可见性,以及标识一些重要的程序生命周期事件的发生(例如,初始化或关闭)。

    虽然volatile变量很方便,但也存在一些局限性。volatile变量一般用做某个操做完成、发生中断或者状态的标志。尽管volatile变量也能够用于表示其余的状态信息,但在使用时要很是当心。例如,volatile的语义不足以确保递增操做(count++)的原子性,除非你能确保只有一个线程对变量执行写操做。

加锁机制既能够确保可见性又能够确保原子性,而volatile变量只能确保可见性。

    当且仅当知足一下全部条件时,才应该使用volatile变量:

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

3.2 发布与逸出

    发布(Publish)一个对象的意思是指,使对象可以在当前做用域以外的代码中使用。在许多状况中,咱们要确保对象及其内部状态不被发布。而在某些状况下,咱们有须要发布这个对象,但若是在发布时要确保线程安全性,则可能须要同步。发布内部状态会破坏封装性,并使程序难以维持不变性条件。当某个不该该发布的对象被发布时,这种状况就被称为逸出(Escape)。例如:

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

    在initialize方法中实例化一个新的HashSet对象,并将对象的引用保存到knownSecrets中以发布对象。

3.2.1 避免内部的可变状态逸出

    当发布某个对象时,可能会间接发布其余对象。若是将一个Secret对象添加到集合knownSecrets中,那么一样会发布这个对象,由于任何代码均可以遍历这个集合,并得到对这个新Secret对象的引用。一样,若是从非私有方法中返回一个引用,那么一样会发布返回的对象。看一段代码:

class UnsafeStates {
	private String[] states = new String[] { "AK", "AL", ... };

	public String[] getStates() {
		return states;
	}
}

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

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

3.2.2 隐士地使this引用逸出

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

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

不要在构造过程当中使this引用逸出。

    当内部的EventListener实例发布时,在外部封装的ThisEscape实例也逸出了。当且仅当对象的构造函数返回时,对象才处于可预测的和一致的状态。所以,当从对象的构造函数中发布对象时,只是发布了一个还没有构造完成的对象。即便发布对象的语句位于构造函数的最后一行也是如此。若是this引用在构造过程当中逸出,那么这种对象就被认为是不正确构造。

    可使用工厂方式来防治this引用在构造过程当中逸出。

3.3 线程封闭

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

    在Java语言中并无强制规定某个变量必须由锁来保护,一样在Java语言中也没法强制将对象封闭在某个线程中。线程封闭式在程序设计中的一个考虑因素,必须在程序中实现。Java语言及其核心库提供了一些机制来帮助维持线程封闭性,例如局部变量和ThreadLocal类。但比便如此,程序员仍然须要负责确保封闭在线程中的对象不会从线程中逸出。

3.1.1 Ad-hoc线程封闭

    Ad-hoc线程封闭是指,维护线程封闭性的职责彻底由程序实现来承担。Ad-hoc线程封闭式很是脆弱的,由于没有任何一种语言特性,能将对象封闭到目标线程上。事实上,对线程封闭对象的引用一般保存在公有变量中。

    当决定使用线程封闭技术时,一般是由于要将某个特定的子系统实现为一个单线程子系统。在某些状况下,单线程子系统提供的简便性要赛过Ad-hoc线程封闭技术的脆弱性。

    举个例子,在volatile变量上存在一种特殊的线程封闭。只要你能肯定只有单个线程对共享的volatile变量执行写入操做,那么就能够安全的在这些共享的volatile变量上执行“读取-修改-写入”的操做。在这种状况下,至关于修改操做封闭在单个线程中以防止发生竞态条件,而且volatile变量的可见性保证还确保了其余线程能看到最新的值。

    因为Ad-hoc线程封闭技术的脆弱性,所以在程序中尽可能少用它,可能的状况下,应该使用更强的线程封闭技术(例如,栈封闭和ThreadLocal类)。

3.1.2 栈封闭

    栈封闭是线程封闭的一种特例,在栈封闭中,只能经过局部变量才能访问对象。正如封装能是的代码更容易维持不变性条件那样,同步变量也能使对象更易与封闭在线程中。局部变量的固有属性之一就是封闭在执行线程中。它们位于执行线程的栈中,其余线程没法访问这个栈。栈封闭比Ad-hoc线程封闭更易于维护,也更加健壮。

public int loadTheArk(Collection<Animal> candidates) {
        SortedSet<Animal> animals;
        int numPairs = 0;
        Animal candidate = null;
 
        animals = new TreeSet<Animal>(new SpeciesGenderComparator());
        animals.addAll(candidates);
        for (Animal a : animals) {
            if (candidate == null || !candidate.isPotentialMate(a))
                candidate = a;
            else {
                ark.load(new AnimalPair(candidate, a));
                ++numPairs;
                candidate = null;
            }
        }
        return numPairs;
    }

    在上面的代码中,numPairs不管如何都不会破坏栈封闭性。因为任何方法都没法得到对基本类型的引用,所以Java语言的这种语义就确保了基本类型的局部变量封闭在线程内。

    在维持对象引用的栈封闭性时,程序员须要多作一些工做以确保被引用的对象不会逸出。在loadTheArk中实例化一个TreeSet对象,并将指向该对象的一个引用确保到animals中。此时,只有一个引用指向集合animals,这个引用被封闭在局部变量中,所以也被封闭在执行线程中。然而,若是发布了对集合animals的引用,那么封闭性将被破坏,并致使对象animals的逸出。

3.1.3 ThreadLocal类

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

    ThreadLocal对象一般用于防止对可变的单实例变量(Singleton)或全局变量进行共享。例如Connection对象,因为JDBC的链接对象不必定是线程安全的,所以,当多线程应用程序在没有协同的状况下使用全局变量时,就不是线程安全的。经过将JDBC的链接保存到ThreadLocal对象中,每一个线程都会拥有属于本身的连接,例如:

public int loadTheArk(Collection<Animal> candidates) {
        SortedSet<Animal> animals;
        int numPairs = 0;
        Animal candidate = null;
 
        animals = new TreeSet<Animal>(new SpeciesGenderComparator());
        animals.addAll(candidates);
        for (Animal a : animals) {
            if (candidate == null || !candidate.isPotentialMate(a))
                candidate = a;
            else {
                ark.load(new AnimalPair(candidate, a));
                ++numPairs;
                candidate = null;
            }
        }
        return numPairs;
    }

    在实现应用程序框架时大量使用了ThreadLocal。例如,在EJB调用期间,J2EE容器须要将一个事务上下文(Transaction Context)与某个执行中的线程关联起来。经过将事务上下文保存在静态的ThreadLocal对象中,能够很容易地实现这个功能:当框架代码须要判断当前运行的是哪个事务时,只需从这个ThreadLocal对象中读取事务上下文。

    开发人员常常滥用ThreadLocal,例如将全部的全局变量都做为ThreadLocal对象,或者做为一种“隐藏”方法参数的手段。ThreadLocal变量相似于全局变量,它能下降代码的课重用性,并在类之间引入隐含的耦合性,所以在使用时须要格外当心。

3.4 不变性

    若是某个对象在被建立后其状态就不能被修改,那么这个对象就称为不可变对象(Immutable Object)。线程安全性是不可变对象的固有属性之一,它们的不变性条件是由构造函数建立的,只要它们的状态不改变,那么这些不变性条件就能得以维持。

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

    不可变对象很简单。它们只有一种状态,而且该状态由构造函数来控制。在程序设计中,一个最困难的地方就是判断复杂对象的可能状态。然而,判断不可变对象的状态却很简单。

    一样,不可变对象也更加安全。若是将一个可变对象传递给不可信的代码,或者将该对象发布到不可信代码能够访问它的地方,那么就很危险 —— 不可信代码会改变它们的状态,更糟的是,在代码中将保留一个对该对象的引用并稍后再其余线程中修改对象的状态。另外一方面,不可变对象不会像这样被恶意代码或者有问题的代码破坏,所以能够安全地共享和发布这些对象,而无须建立保护性的副本。

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

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

  • 对象建立之后其状态就不能修改;
  • 对象的全部域都是fianl类型;
  • 对象是正确建立的;

    关键字final能够视为C++中const机制的一种受限版本,用于构造不可变性对象。final类型的域是不能修改的。然而,在Java内存模型中,final域还有着特殊的语义。fianl域能确保初始化过程的安全性,从而能够不受限制地访问不可变对象,并在共享这些对象时无需同步。

    即便对象时可变的,经过将对象的某些域声明为final类型,仍然能够简化对状态的判断,所以限制对象的可变性也就至关于限制了该对象可能的状态集合。仅包含一个或两个可变状态的“基本不可变”对象仍然比包含多个可变状态的对象简单。经过将域声明为final类型,也至关于告诉维护人员这些域是不会变化的。

除非须要某个域是可变的,不然应将其声明为final域。

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) throw new AssertionError("this statement is false.");
    }
}

    因为没有使用同步来确保Holder对象对其余线程可见,所以将Holder称为“未被正确发布”。这里面存在两个问题,首先,除了发布对象的线程外,其余线程能够看到的Holder域是一个失效值,所以将看到一个空引用或者以前的旧值。然而,更糟的状况是线程看到Holder引用的值是最新的,但Holder状态的值倒是失效的。状况变得更加不可预测的是,某个线程在第一次读取域时获得失效值,而再次读取这个域时会获得一个更新值,这也是assertSainty抛出AssertionError的缘由。

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

    因为不可变对象是一种很是重要的对象,所以Java内存模型为不可变对象提供了一种特殊的初始化安全性保证。咱们已经知道,即便某个对象的引用对其余线程是可见的,也并不意味着对象状态对于使用该对象的线程来讲必定是可见的。为了确保对象状态能呈现出一直的视图,就必须使用同步。

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

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

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

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

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

3.5.4 事实不可变对象

    若是对象在发布后不会被修改,那么对于其余在没有额外同步的状况下安全地访问这些对象的线程来讲,安全发布是足够的。全部的安全发布机制都能确保,当对象的引用对全部访问该对象的线程可见时,对象发布时的状态对于全部线程也将是可见的,而且若是对象状态不会再改变,那么就足以确保任何访问都是安全的。

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

3.5.5 可变对象

    若是对象在构造后能够修改,那么安全发布只能确保“发布当时”状态的可见性。对于可变对象,不只在发布对象时须要使用同步,并且在每次对象访问时一样须要使用同步来确保后续修改操做的可见性。

    对象的发布需求取决于它的可变性:

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

3.5.6 安全的共享对象

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

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