java并发编程2:构建线程安全应用程序

线程安全性

        调用一个函数(假设该函数是正确的)操作某对象常常会使该对象暂时陷入不可用的状态(通常称为不稳定状态),等到操作完全结束,该对象才会重新回到完全可用的状态。如果其他线程企图访问一个处于不可用状态的对象,该对象将不能正确响应从而产生无法预料的结果,如何避免这种情况发生是线程安全性的核心问题。单线程的程序中是不存在这种问题的,因为在一个线程更新某对象的时候不会有其他线程也去操作同一个对象。(除非其中有异常,异常是可能导致上述问题的。当一个正在更新某对象的线程因异常而中断更新过程后,再去访问没有完全更新的对象,会出现同样的问题)

        类要成为线程安全的,首先必须在单线程环境中有正确的行为。此外,一个类要成为线程安全的,在被多个线程访问时,不管运行时环境执行这些线程有什么样的时序安排或者交错,它必须仍然有如上所述的正确行为,并且在调用的代码中没有任何额外的同步。其效果就是,在所有线程看来,对于线程安全对象的操作是以固定的、全局一致的顺序发生的。

         正确性与线程安全性之间的关系非常类似于在描述ACID(原子性、一致性、独立性和持久性)事务时使用的一致性与独立性之间的关系:从特定线程的角度看,由不同线程所执行的对象操作是先后(虽然顺序不定)而不是并行执行的。

考虑下面的代码片段,它迭代一个Vector 中的元素。尽管Vector 的所有方法都是同步的,但是在多线程的环境中不做额外的同步就使用这段代码仍然是不安全的,因为如果另一个线程恰好在错误的时间里删除了一个元素, 则get() 会抛出一个
ArrayIndexOutOfBoundsException 。

Vector v = new Vector();
    // contains race conditions -- may require external synchronization
    for (int i=0; i<v.size(); i++) {
    doSomething(v.get(i));
}
这里发生的事情是:get(index) 的规格说明里有一条前置条件要求 index 必须是非负的并且小于 size() 。但是,在多线程环境中,没有办法可以知道上一次查到的size() 值是否仍然有效,因而不能确定 i<size() ,除非在上一次调用了 size() 后独占地锁定Vector 。
更明确地说,这一问题是由 get() 的前置条件是以 size() 的结果来定义的这一事实所带来的。只要看到这种必须使用一种方法的结果作为另一种讲法的输入条件的样式,它就是一个状态依赖,就必须保证至少在调用这两种方法期间元素的状态没有改变。一般来说,做到这一点的唯一方法在调用第一个方法之前是独占性地锁定对象,一直到调用了后一种方法以后。在上面的迭代 Vector 元素的例子中,您需要在迭代过程中同步Vector 对象。

        如上面的例子所示,线程安全性不是一个非真即假的命题。Vector 的方法都是同步的,并且Vector 明确地设计为在多线程环境中工作。但是它的线程安全性是有限制的,即在某些方法之间有状态依赖(类似地,如果在迭代过程中Vector 被其他线程修改,那么由Vector.iterator() 返回的 iterator 会抛出 ConcurrentModificationException )。对于 Java 类中常见的线程安全性级别,没有一种分类系统可被广泛接受,不过重要的是在编写类时尽量记录下它们的线程安全行为。
 五类线程安全性的分类方法     

Bloch 给出了描述五类线程安全性的分类方法:不可变、线程安全、有条件线程安全、线程兼容和线程对立。只要明确地记录下线程安全特性,那么您是否使用这种系统都没关系。这种系统有其局限性——各类之间的界线不是百分之百地明确,而且有些情况它没照顾到,但是这套系统是一个很好的起点。这种分类系统的核心是调用者是否可以或者必须用外部同步包围操作(或者一系列操作)。下面分别描述了线程安全性的这五种类别。

1) 不可变
不可变的对象一定是线程安全的,并且永远也不需要额外的同步。因为一个不可变的对象只要构建正确,其外部可见状态永远也不会改变,永远也不会看到它处于不一致的状态。Java 类库中大多数基本数值类如Integer、String 和BigInteger 都是不可变的。
2) 线程安全
由类的规格说明所规定的约束在对象被多个线程访问时仍然有效,不管运行时环境如何排列,线程都不需要任何额外的同步。这种线程安全性保证是很严格的——许多类,如Hashtable 或者 Vector 都不能满足这种严格的定义。
3) 有条件的线程安全
有条件的线程安全类对于单独的操作可以是线程安全的,但是某些操作序列可能需要外部同步。条件线程安全的最常见的例子是遍历由 Hashtable 或者 Vector 或者返回的迭代器——由这些类返回的 fail-fast 迭代器假定在迭代器进行遍历的时候底层集合不会有变化。为了保证其他线程不会在遍历的时候改变集合,进行迭代的线程应该确保它是独占性地访问集合以实现遍历的完整性。通常,独占性的访问是由对锁的同步保证的——并且类的文档应该说明是哪个锁(通常是对象的内部监视器(intrinsic monitor))。如果对一个有条件线程安全类进行记录,那么您应该不仅要记录它是有条件线程安全的,而且还要记录必须防止哪些操作序列的并发访问。用户可以合理地假设其他操作序列不需要任何额外的同步。
4) 线程兼容
线程兼容类不是线程安全的,但是可以通过正确使用同步而在并发环境中安全地使用。这可能意味着用一个synchronized 块包围每一个方法调用,或者创建一个包装器对象,其中每一个方法都是同步的( 就像 Collections.synchronizedList() 一样) 。也可能意味着用
synchronized 块包围某些操作序列。为了最大程度地利用线程兼容类,如果所有调用都使用同一个块,那么就不应该要求调用者对该块同步。这样做会使线程兼容的对象作为变量实例包含在其他线程安全的对象中,从而可以利用其所有者对象的同步。许 多常见的类是线程兼容的, 如集合类 ArrayList 和 HashMap 、java.text.SimpleDateFormat 、或者 JDBC 类 Connection 和 ResultSet 。
5) 线程对立
线程对立类是那些不管是否调用了外部同步都不能在并发使用时安全地呈现的类。线程对立很少见,当类修改静态数据,而静态数据会影响在其他线程中执行的其他类的行为,这时通常会出现线程对立。线程对立类的一个例子是调用 System.setOut() 的类。

        线程安全类(以及线程安全性程度更低的的类) 可以允许或者不允许调用者锁定对象以进行独占性访问。Hashtable 类对所有的同步使用对象的内部监视器, 但是ConcurrentHashMap 类不是这样,事实上没有办法锁定一个 ConcurrentHashMap 对象以进行独占性访问。除了记录线程安全程序,还应该记录是否某些锁——如对象的内部锁——对类的行为有特殊的意义。

        通过将类记录为线程安全的(假设它确实是线程安全的),您就提供了两种有价值的服务:您告知类的维护者不要进行会影响其线程安全性的修改或者扩展,您还告知类的用户使用它时可以不使用外部同步。通过将类记录为线程兼容或者有条件线程安全的,您就告知了用户这个类可以通过正确使用同步而安全地在多线程中使用。通过将类记录为线程对立的,您就告知用户即使使用了外部同步,他们也不能在多线程中安全地使用这个类。

Servlet 的线程安全性

           Servlet 体系结构是建立在Java 多线程机制之上的,它的生命周期是由Web 容器负责的。当客户端第一次请求某个Servlet 时,Servlet 容器将会根据web.xml 配置文件实例化这个Servlet 类。当有新的客户端请求该Servlet 时,一般不会再实例化该Servlet 类,也就是有多个线程在使用这个实例。Servlet 容器会自动使用线程池等技术来支持系统的运行。


这样,当两个或多个线程同时访问同一个Servlet 时,可能会发生多个线程同时访问同一资源的情况,数据可能会变得不一致。所以在用Servlet 构建的Web 应用时如果不注意线程安全的问题,会使所写的Servlet 程序有难以发现的错误。

1. 无状态Servlet
下面是一个无状态的Servlet,它从Request 中解包数据,然后将这两个数据进行相乘,最后把结果封装在Response 中。

import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class ConcurrentServlet extends HttpServlet {
   private static final long serialVersionUID = 1L;
   public ConcurrentServlet() {
      super();
   }
   protected void doGet(HttpServletRequest request,
       HttpServletResponse response) throws ServletException,
   IOException {
       String s1 = request.getParameter("num1");
       String s2 = request.getParameter("num2");
       int result = 0;
       if (s1 != null && s1 != null) {
           result = Integer.parseInt(s1) * Integer.parseInt(s2);
       }
       PrintWriter out = response.getWriter();
       out.print(result);
       out.close();
    }
}
这个Servlet 是无状态的,它不包含域,也没有引用其它类的域,一次特定计算的瞬时状态,会唯一的存储在本地变量中,这些本地变量存在线程的栈中,只有执行线程才能访问,一个执行该Servlet 的线程不会影响访问同一个Servlet 的其它线程的计算结果,因为两个线程不共享状态,他们如同在访问不同的实例。因为线程访问无状态对象的行为,不会影响其它线程访问对象时的正确性,所以无状态对象是线程安全的。

2 有状态Servlet
对上面的Servlet 进行修改,把result 变量提升为类的实例变量。那么这个Servlet 就有状态了。有状态的Servlet 在多线程访问时,有可能发生线程不安全性。请看下面的代码。

import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class StatefulServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
	int result = 0;

	public StatefulServlet() {
		super();
	}
	protected void doGet(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {
		String s1 = request.getParameter("num1");
		String s2 = request.getParameter("num2");
		if (s1 != null && s1 != null) {
			result = Integer.parseInt(s1) * Integer.parseInt(s2);
		}
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		PrintWriter out = response.getWriter();
		out.print(result);
		out.close();
	}
}
在Servlet 中定义了一个实例变量result,Servlet 把它的值进行输出。当只有一个用户访问该Servlet 时,程序会正常的运行,但当多个用户并发访问时,就可能会出现其它用户的信息显示在另外一些用户的浏览器上的问题。这是一个严重的问题。为了突出并发问题,便于测试、观察,我们在回显用户信息时执行了一个延时的操作。打开两个浏览器窗口,分别输入:
http://localhost:8080/test/StatefulServlet?num1=5&num2=80
http://localhost:8080/test/StatefulServlet?num1=5&num2=70。
相隔 5000 毫秒之内执行这两个请求,产生的结果如下图:


从运行结果可以看出,两个请求显示了相同的计算结果,也就是说,因为两个线程访问了共同的有状态的Servlet,其中一个线程的计算结果覆盖了另外一个线程的计算结果。从程序分析可以看出第一个线程在输出result 时,暂停了一段时间,那么它的值就被第二个线程的计算结果所覆盖,两个请求输出了相同的结果。这就是潜在的线程不安全性。要解决线程不安全性,其中一个主要的方法就是取消Servlet 的实例变量,变成无状态的Servlet。另外一种方法是对共享数据进行同步操作。使用synchronized 关键字能保证一次只有一个线程可以访问被保护的区段,同步后的Servlet 如下:

import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class StatefulServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
	int result = 0;
	
	public StatefulServlet() {
		super();
	}

	protected void doGet(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {
		String s1 = request.getParameter("num1");
		String s2 = request.getParameter("num2");
		synchronized (this) {
			if (s1 != null && s1 != null) {
				result = Integer.parseInt(s1) * Integer.parseInt(s2);
			}
			try {
				Thread.sleep(5000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			PrintWriter out = response.getWriter();
			out.print(result);
			out.close();
		}
	}
}
Servlet 的线程安全问题只有在大量的并发访问时才会显现出来,并且很难发现,因此在编写Servlet 程序时要特别注意。线程安全问题主要是由实例变量造成的,因此在Servlet 中应避免使用实例变量。如果应用程序设计无法避免使用实例变量,那么使用同步来保护要使用的实例变量,但为保证系统的最佳性能,应该同步可用性最小的代码路径。

同步与互斥

干扰:线程通信主要通过共享访问字段或者字段引用的对象完成的,但是有可能出现两种错误:线程干扰(thread interference)和内存一致性错误(memory consistency)。用来防止这些错误的工具是同步(synchronization)。不同线程中的两个操作对相同数据进行操作时,就会出现干扰,就是说,两个操作有多个步骤组成,并且操作步骤的序列重叠了。

如银行存款:“balance = balance - amount;”一般可能会分解成3 个步骤:1)取出balance 的值,2)执行减法,3)计算结果赋值给balance。其中balance是共享变量,多个线程对这个语句操作时上述步骤可能会重叠。
同步
当两个线程需要使用同一个对象时,存在交叉操作而破坏数据的可能性。这种潜在的干扰动作在术语上被称作临界区(critical section)。通过同步(Synchronize)对临界区的访问可以避免这种线程干扰。某些动作操作对象之前,必须先获得这个对象的锁。获取待操作对象上的锁可以阻止其他对象获取这个锁,直至这个锁的持有者释放它为止。这样,多线程就不会同时执行那些会互相干扰的动作。同步是围绕被称为内在锁(intrinsic lock)或者监视器锁(monitor lock)的内部实体构建的,强制对对象状态的独占访问,以及建立可见性所需的发生前关系。每个对象都具有与其关联的内在锁,按照约定,需要对对象的字段进行独占和一致性访
问的线程,在进行访问之前,必须获得这个对象的内在锁,访问操作完成之后必须释放内在锁。在从获得锁到释放锁的时间段内,线程被称为拥有内在锁。只要有线程拥有内在锁,其他线程就不能获得同一个锁,试图获得锁的其他线程将被阻塞。Java 提供了synchronized 关键字来支持内在锁。Synchronized 关键字可以放在方法的前面、对象的前面、类的前面。

1. 同步方法中的锁
当线程调用同步方法时,它自动获得这个方法所在对象的内在锁,并且方法返回时释放锁,如果发生未捕获的异常,也会释放锁。
当调用静态同步方法时,因为静态方法和类相关联,线程获得和这个类关联的 Class 对象的内在锁。

2. 同步语句
同步语句必须指定提供内在锁的对象,其基本用法如下:
synchronized(提供锁的对象){
      临界代码
}

3. 同步类
把 synchronized 关键字放在类的前面,这个类中的所有方法都是同步方法。
4. 可重入同步
线程可以获得他已经拥有的锁,运行线程多次获得同一个锁,就是可以重入(reentrant)同步。这种情况通常是同步代码直接或者间接的调用也包含了同步代码的方法,并且两个代码集都使用同一个锁。如果没有可重入同步,那么,同步代码就必须采取很多额外的预防措施避免线程阻塞自己。

同步与volatile

Java 语言中的 volatile 变量可以被看作是一种 “程度较轻的 synchronized”;与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。本文介绍了几种有效使用 volatile 变量的模式,并强调了几种不适合使用 volatile 变量的情形。
锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。
Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile 变量的最新值。Volatile 变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。因此,单独使用 volatile 还不足以实现计数器、互斥锁或任何具有与多个变量相关的不变式(Invariants)的类(例如 “start <=end”)。出于简易性或可伸缩性的考虑,您可能倾向于使用 volatile 变量而不是锁。当使用 volatile 变量而非锁时,某些习惯用法(idiom)更加易于编码和阅读。此外,volatile 变量不会像锁那样造成线程阻塞,因此也很少造成可伸缩性问题。在某些情况下,如果读操作远远大于写操作,volatile 变量还可以提供优于锁的性能优势。
正确使用 volatile 变量的条件
您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
(1)对变量的写操作不依赖于当前值。
(2)该变量没有包含在具有其他变量的不变式中。
实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
第一个条件的限制使 volatile 变量不能用作线程安全计数器。虽然增量操作(x++)看上去类似一个单独操作,实际上它是一个由读取-修改-写入操作序列组成的组合操作,必须以原子方式执行,而 volatile 不能提供必须的原子特性。实现正确的操作需要使 x 的值在操作期间保持不变,而 volatile 变量无法实现这点。(然而,如果将值调整为只从单个线程写入,那么可以忽略第一个条件。)
大多数编程情形都会与这两个条件的其中之一冲突,使得 volatile 变量不能像 synchronized 那样普遍适用于实现线程安全。清单 1 显示了一个非线程安全的数值范围类。它包含了一个不变式 —— 下界总是小于或等于上界。

清单 1. 非线程安全的数值范围类
                
@NotThreadSafe 
public class NumberRange {
    private int lower, upper;


    public int getLower() { return lower; }
    public int getUpper() { return upper; }


    public void setLower(int value) { 
        if (value > upper) 
            throw new IllegalArgumentException(...);
        lower = value;
    }


    public void setUpper(int value) { 
        if (value < lower) 
            throw new IllegalArgumentException(...);
        upper = value;
    }
}


这种方式限制了范围的状态变量,因此将 lower 和 upper 字段定义为 volatile 类型不能够充分实现类的线程安全;从而仍然需要使用同步。否则,如果凑巧两个线程在同一时间使用不一致的值执行 setLower 和 setUpper 的话,则会使范围处于不一致的状态。例如,如果初始状态是 (0, 5),同一时间内,线程 A 调用 setLower(4) 并且线程 B 调用 setUpper(3),显然这两个操作交叉存入的值是不符合条件的,那么两个线程都会通过用于保护不变式的检查,使得最后的范围值是 (4, 3) —— 一个无效值。至于针对范围的其他操作,我们需要使 setLower() 和 setUpper() 操作原子化 —— 而将字段定义为 volatile 类型是无法实现这一目的的。
性能考虑
使用 volatile 变量的主要原因是其简易性:在某些情形下,使用 volatile 变量要比使用相应的锁简单得多。使用 volatile 变量次要原因是其性能:某些情况下,volatile 变量同步机制的性能要优于锁。
很难做出准确、全面的评价,例如 “X 总是比 Y 快”,尤其是对 JVM 内在的操作而言。(例如,某些情况下 VM 也许能够完全删除锁机制,这使得我们难以抽象地比较 volatile 和 synchronized 的开销。)就是说,在目前大多数的处理器架构上,volatile 读操作开销非常低 —— 几乎和非 volatile 读操作一样。而 volatile 写操作的开销要比非 volatile 写操作多很多,因为要保证可见性需要实现内存界定(Memory Fence),即便如此,volatile 的总开销仍然要比锁获取低。
volatile 操作不会像锁一样造成阻塞,因此,在能够安全使用 volatile 的情况下,volatile 可以提供一些优于锁的可伸缩特性。如果读操作的次数要远远超过写操作,与锁相比,volatile 变量通常能够减少同步的性能开销。
正确使用 volatile 的模式
很多并发性专家事实上往往引导用户远离 volatile 变量,因为使用它们要比使用锁更加容易出错。然而,如果谨慎地遵循一些良好定义的模式,就能够在很多场合内安全地使用 volatile 变量。要始终牢记使用 volatile 的限制 —— 只有在状态真正独立于程序内其他内容时才能使用 volatile —— 这条规则能够避免将这些模式扩展到不安全的用例。
模式 #1:状态标志
也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。
很多应用程序包含了一种控制结构,形式为 “在还没有准备好停止程序时再执行一些工作”,如清单 2 所示:


清单 2. 将 volatile 变量作为状态标志使用
                
volatile boolean shutdownRequested;


...


public void shutdown() { shutdownRequested = true; }


public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}


很可能会从循环外部调用 shutdown() 方法 —— 即在另一个线程中 —— 因此,需要执行某种同步来确保正确实现 shutdownRequested 变量的可见性。(可能会从 JMX 侦听程序、GUI 事件线程中的操作侦听程序、通过 RMI 、通过一个 Web 服务等调用)。然而,使用 synchronized 块编写循环要比使用清单 2 所示的 volatile 状态标志编写麻烦很多。由于 volatile 简化了编码,并且状态标志并不依赖于程序内任何其他状态,因此此处非常适合使用 volatile。
这种类型的状态标记的一个公共特性是:通常只有一种状态转换;shutdownRequested 标志从 false 转换为 true,然后程序停止。这种模式可以扩展到来回转换的状态标志,但是只有在转换周期不被察觉的情况下才能扩展(从 false 到 true,再转换到 false)。此外,还需要某些原子状态转换机制,例如原子变量。
模式 #2:一次性安全发布(one-time safe publication)
缺乏同步会导致无法实现可见性,这使得确定何时写入对象引用而不是原语值变得更加困难。在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。(这就是造成著名的双重检查锁定(double-checked-locking)问题的根源,其中对象引用在没有同步的情况下进行读操作,产生的问题是您可能会看到一个更新的引用,但是仍然会通过该引用看到不完全构造的对象)。
实现安全发布对象的一种技术就是将对象引用定义为 volatile 类型。清单 3 展示了一个示例,其中后台线程在启动阶段从数据库加载一些数据。其他代码在能够利用这些数据时,在使用之前将检查这些数据是否曾经发布过。


清单 3. 将 volatile 变量用于一次性安全发布
                
public class BackgroundFloobleLoader {
    public volatile Flooble theFlooble;


    public void initInBackground() {
        // do lots of stuff
        theFlooble = new Flooble();  // this is the only write to theFlooble
    }
}


public class SomeOtherClass {
    public void doWork() {
        while (true) { 
            // do some stuff...
            // use the Flooble, but only if it is ready
            if (floobleLoader.theFlooble != null) 
                doSomething(floobleLoader.theFlooble);
        }
    }
}


如果 theFlooble 引用不是 volatile 类型,doWork() 中的代码在解除对 theFlooble 的引用时,将会得到一个不完全构造的 Flooble。
该模式的一个必要条件是:被发布的对象必须是线程安全的,或者是有效的不可变对象(有效不可变意味着对象的状态在发布之后永远不会被修改)。volatile 类型的引用可以确保对象的发布形式的可见性,但是如果对象的状态在发布后将发生更改,那么就需要额外的同步。
模式 #3:独立观察(independent observation)
安全使用 volatile 的另一种简单模式是:定期 “发布” 观察结果供程序内部使用。例如,假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。
使用该模式的另一种应用程序就是收集程序的统计信息。清单 4 展示了身份验证机制如何记忆最近一次登录的用户的名字。将反复使用 lastUser 引用来发布值,以供程序的其他部分使用。


清单 4. 将 volatile 变量用于多个独立观察结果的发布
                
public class UserManager {
    public volatile String lastUser;


    public boolean authenticate(String user, String password) {
        boolean valid = passwordIsValid(user, password);
        if (valid) {
            User u = new User();
            activeUsers.add(u);
            lastUser = user;
        }
        return valid;
    }



该模式是前面模式的扩展;将某个值发布以在程序内的其他地方使用,但是与一次性事件的发布不同,这是一系列独立事件。这个模式要求被发布的值是有效不可变的 —— 即值的状态在发布后不会更改。使用该值的代码需要清楚该值可能随时发生变化。
模式 #4:“volatile bean” 模式
volatile bean 模式适用于将 JavaBeans 作为“荣誉结构”使用的框架。在 volatile bean 模式中,JavaBean 被用作一组具有 getter 和/或 setter 方法 的独立属性的容器。volatile bean 模式的基本原理是:很多框架为易变数据的持有者(例如 HttpSession)提供了容器,但是放入这些容器中的对象必须是线程安全的。
在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于对象引用的数据成员,引用的对象必须是有效不可变的。(这将禁止具有数组值的属性,因为当数组引用被声明为 volatile 时,只有引用而不是数组本身具有 volatile 语义)。对于任何 volatile 变量,不变式或约束都不能包含 JavaBean 属性。清单 5 中的示例展示了遵守 volatile bean 模式的 JavaBean:


清单 5. 遵守 volatile bean 模式的 Person 对象
                
@ThreadSafe
public class Person {
    private volatile String firstName;
    private volatile String lastName;
    private volatile int age;


    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public int getAge() { return age; }


    public void setFirstName(String firstName) { 
        this.firstName = firstName;
    }


    public void setLastName(String lastName) { 
        this.lastName = lastName;
    }


    public void setAge(int age) { 
        this.age = age;
    }
}


volatile 的高级模式
前面几节介绍的模式涵盖了大部分的基本用例,在这些模式中使用 volatile 非常有用并且简单。这一节将介绍一种更加高级的模式,在该模式中,volatile 将提供性能或可伸缩性优势。
volatile 应用的的高级模式非常脆弱。因此,必须对假设的条件仔细证明,并且这些模式被严格地封装了起来,因为即使非常小的更改也会损坏您的代码!同样,使用更高级的 volatile 用例的原因是它能够提升性能,确保在开始应用高级模式之前,真正确定需要实现这种性能获益。需要对这些模式进行权衡,放弃可读性或可维护性来换取可能的性能收益 —— 如果您不需要提升性能(或者不能够通过一个严格的测试程序证明您需要它),那么这很可能是一次糟糕的交易,因为您很可能会得不偿失,换来的东西要比放弃的东西价值更低。
模式 #5:开销较低的读-写锁策略
目前为止,您应该了解了 volatile 的功能还不足以实现计数器。因为 ++x 实际上是三种操作(读、添加、存储)的简单组合,如果多个线程凑巧试图同时对 volatile 计数器执行增量操作,那么它的更新值有可能会丢失。
然而,如果读操作远远超过写操作,您可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。清单 6 中显示的线程安全的计数器使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。


清单 6. 结合使用 volatile 和 synchronized 实现 “开销较低的读-写锁” 
                
@ThreadSafe
public class CheesyCounter {
    // Employs the cheap read-write lock trick
    // All mutative operations MUST be done with the 'this' lock held
    @GuardedBy("this") private volatile int value;


    public int getValue() { return value; }


    public synchronized int increment() {
        return value++;
    }
}


之所以将这种技术称之为 “开销较低的读-写锁” 是因为您使用了不同的同步机制进行读写操作。因为本例中的写操作违反了使用 volatile 的第一个条件,因此不能使用 volatile 安全地实现计数器 —— 您必须使用锁。然而,您可以在读操作中使用 volatile 确保当前值的可见性,因此可以使用锁进行所有变化的操作,使用 volatile 进行只读操作。其中,锁一次只允许一个线程访问值,volatile 允许多个线程执行读操作,因此当使用 volatile 保证读代码路径时,要比使用锁执行全部代码路径获得更高的共享度 —— 就像读-写操作一样。然而,要随时牢记这种模式的弱点:如果超越了该模式的最基本应用,结合这两个竞争的同步机制将变得非常困难。
结束语
与锁相比,Volatile 变量是一种非常简单但同时又非常脆弱的同步机制,它在某些情况下将提供优于锁的性能和伸缩性。如果严格遵循 volatile 的使用条件 —— 即变量真正独立于其他变量和自己以前的值 —— 在某些情况下可以使用 volatile 代替 synchronized 来简化代码。然而,使用 volatile 的代码往往比使用锁的代码更加容易出错。本文介绍的模式涵盖了可以使用 volatile 代替 synchronized 的最常见的一些用例。遵循这些模式(注意使用时不要超过各自的限制)可以帮助您安全地实现大多数用例,使用 volatile 变量获得更佳性能。

活性
并发应用程序按照及时方式执行的能力称为活性(liveness)。一般包括三种类型的问题死锁、饿死和活锁。

1. 死锁
线程死锁是并发程序设计中可能遇到的主要问题之一。他是指程序运行中,多个线程竞争共享资源时可能出现的一种系统状态,每个线程都被阻塞,都不会结束,进入一种永久等待状态。可能发生死锁的最典型的例子是哲学家用餐问题:五个哲学家围坐在一圆桌旁,每人的两边放着一支筷子,共5 支筷子。大家边讨论问题边用餐,并规定如下条件:1)每个人只有拿起位于自己两边的筷子,合成一双才可以用餐;2)用餐后,每人必须将两支筷子放回原处。可以想想,如果每个哲学家都彬彬有礼,并且高谈阔论,轮流吃饭,则这种融洽的气氛可以长久的保持下去。但是可能出现这样一种情景:当每个人都拿起自己左手边的筷子,并同时去拿自己右手边的筷子时,5 个人每人拿着一根筷子,盯着自己右手边那位哲学家手里的筷子,处于僵持状态。这就发生了线程死锁。

2. 饿死
饿死(starvation)描述这样的情况:一个线程不能获得对共享资源的常规访问,并且不能继续工作,当共享资源被贪婪线程长期占有而不可用时,就会发生这样的情况。
3. 活锁
一个线程经常对另一个线程的操作作出响应,如果另一个线程的操作也对这个线程的操作作出响应,那么就可能导致活锁(livelock)。和死锁类似,发生活锁的线程不能进行进一步操作。但是,线程没有被锁定,它只是忙于相互响应,以致不能恢复工作。活锁可以比喻为两人在走廊中相遇。A 避让的自己的左边让B 通过,而B 同时避让到自己的右边让A 通过。发现他们仍然挡住了对方,A 就避让到自己的右边,而B 同时避让到了自己的左边,他们还是挡住了对方,所以就没完没了。

ThreadLocal 变量

早在 JDK 1.2 的版本中就提供java.lang.ThreadLocal,为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。ThreadLocal 很容易让人望文生义,想当然地认为是一个“本地线程”。其实,ThreadLocal并不是一个Thread,而是Thread 的局部变量,也许把它命名为ThreadLocalVariable 更容易让人理解一些。当使用ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

从线程的角度看,目标变量就是线程的本地变量,这也是类名中“Local”所要表达的意思。线程局部变量并不是Java 的新发明,很多语言(如IBM XL FORTRAN)在语法层面就
提供线程局部变量。在Java 中没有提供语言级支持,而是变相地通过ThreadLocal 的类提供支持。
JDK 5 以后提供了泛型支持,ThreadLocal 被定义为支持泛型:
public class ThreadLocal<T> extends Object
T 为线程局部变量的类型。该类定义了4 个方法:
1) protected T initialValue():返回此线程局部变量的当前线程的“初始值”。

线程第一次使用 get() 方法访问变量时将调用此方法,但如果线程之前调用了 set(T) 方法,则不会对该线程再调用 initialValue 方法。通常,此方法对每个线程最多调用一次,但如果在调用 get()后又调用了 remove(),则可能再次调用此方法。该实现返回 null;如果程序员希望线程局部变量具有 null 以外的值,则必须为ThreadLocal 创建子类,并重写此方法。通常将使用匿名内部类完成此操作。
2)public T get():返回此线程局部变量的当前线程副本中的值。

如果变量没有用于当前线程的值,则先将其初始化为调用 initialValue() 方法返回的值。
3)public void set(T value):将此线程局部变量的当前线程副本中的值设置为指定值。

大部分子类不需要重写此方法,它们只依靠 initialValue() 方法来设置线程局部变量的值。
4)public void remove():移除此线程局部变量当前线程的值。

如果此线程局部变量随后被当前线程读取,且这期间当前线程没有设置其值,则将调用其 initialValue() 方法重新初始化其值。这将导致在当前线程多次调用 initialValue 方法。下面是一个使用 ThreadLocal 的例子,每个线程产生自己独立的序列号。就是使用ThreadLocal 存储每个线程独立的序列号复本,线程之间互不干扰。

package sync;

public class SequenceNumber {
	// 定义匿名子类创建ThreadLocal的变量
	private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {
		// 覆盖初始化方法
		public Integer initialValue() {
			return 0;
		}
	};

	// 下一个序列号
	public int getNextNum() {
		seqNum.set(seqNum.get() + 1);
		return seqNum.get();
	}

	private static class TestClient extends Thread {
		private SequenceNumber sn;

		public TestClient(SequenceNumber sn) {
			this.sn = sn;
		}

		// 线程产生序列号
		public void run() {
			for (int i = 0; i < 3; i++) {
				System.out.println("thread[" + Thread.currentThread().getName()
						+ "] sn[" + sn.getNextNum() + "]");
			}
		}
	}

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		SequenceNumber sn = new SequenceNumber();
		// 三个线程产生各自的序列号
		TestClient t1 = new TestClient(sn);
		TestClient t2 = new TestClient(sn);
		TestClient t3 = new TestClient(sn);
		t1.start();
		t2.start();
		t3.start();
	}
}
程序的运行结果如下:

thread[Thread-1] sn[1]
thread[Thread-1] sn[2]
thread[Thread-1] sn[3]
thread[Thread-2] sn[1]
thread[Thread-2] sn[2]
thread[Thread-2] sn[3]
thread[Thread-0] sn[1]
thread[Thread-0] sn[2]
thread[Thread-0] sn[3]
从运行结果可以看出,使用了ThreadLocal 后,每个线程产生了独立的序列号,没有相互干扰。通常我们通过匿名内部类的方式定义ThreadLocal 的子类,提供初始的变量值。
ThreadLocal 和线程同步机制相比有什么优势呢?ThreadLocal 和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。

在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需
要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。而 ThreadLocal 则从另一个角度来解决多线程的并发访问。ThreadLocal 会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal 提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal 采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。需要注意的是ThreadLocal 对象是一个本质上存在风险的工具,应该在完全理解将要使用的线程模型之后,再去使用ThreadLocal 对象。这就引出了线程池(thread pooling)的问题,线程池是一种线程重用技术,有了线程池就不必为每个任务创建新的线程,一个线程可能会多次使用,用于这种环境的任何ThreadLocal 对象包含的都是最后使用该线程的代码所设置的状态,而不是在开始执行新线程时所具有的未被初始化的状态。那么 ThreadLocal 是如何实现为每个线程保存独立的变量的副本的呢?通过查看它的源代码,我们会发现,是通过把当前“线程对象”当作键,变量作为值存储在一个Map 中。

private T setInitialValue() {
	T value = initialValue();
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
	if (map != null)
		map.set(this, value);
	else
		createMap(t, value);
	return value;
}
高级并发对象
本章重点介绍的是低级别的 API,都是Java 平台最基本的组成部分,这些都足以胜任基本的任务,但是更加高级的任务需要更高级别的API,对应充分利用现代多处理器和多核心
系统功能的大规模并发应用程序来说,这尤其重要。JDK5.0 以后的版本都引入了高级并发特性,并且新的版本在不断的补充和完善。大多数的特性在java.util.concurrent 包中实现,Java 集合框架中也有新的并发数据结构。主要增加的高级并发对象有:Lock 对象,执行器,并发集合、原子变量和同步器。
1)Lock 对象
前面介绍的同步代码依靠简单类型的可重入锁,即内部锁(隐式锁)。这种类型的锁易于使用,但是有很多局限性。新的Lock 对象支持更加复杂的锁定语法。和隐式锁类似,每一时刻只有一个线程能够拥有Lock 对象,通过与其相关联的Condition对象,Lock 对象也支持wait 和notify 机制。Lock 对象的最大优势在于能够阻挡获得锁的企图。如果锁不能立即可用或者在超时时间到期之前可用,tryLock 方法就会阻挡,如果另一个线程在获得锁之前发送中断,lockInterruptibly 方法就会阻挡。
2)执行器
前面例子,线程完成的任务(Runnable 对象)和线程对象(Thread)之间紧密相连。适用于小型程序,在大型应用程序中,把线程管理和创建工作与应用程序的其余部分分离开更有意义。封装线程管理和创建的对象被称为执行器(Executor)。JDK 中定义了3 个执行器接口:Executor,ExecutorService 和ScheduledExecutorService。
3)并发集合
并 发 集 合 是原有集合框架的补充, 为多线程并发程序提供了支持。主要有:BlockingQueue,ConcurrentMap,ConcurrentNavigableMap。
4)原子变量
定义了支持对单一变量执行原子操作的类。所有类都有 get 和set 方法,工作方法和对volatile 变量的读取和写入一样。
5)同步器
提 供 了 一 些 帮 助 在 线 程 间 协 调 的 类 , 包 括 semaphores, mutexes, barriers, latches,exchangers 等。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值