《Java Concurrency》读书笔记,构建线程安全应用程序

时间:2022-06-23 04:12:59

1. 什么是线程安全性

调用一个函数(假设该函数是正确的)操作某对象常常会使该对象暂时陷入不可用的状态(通常称为不稳定状态),等到操作完全结束,该对象才会重新回到完全可用的状态。如果其他线程企图访问一个处于不可用状态的对象,该对象将不能正确响应从而产生无法预料的结果,如何避免这种情况发生是线程安全性的核心问题。

  1. 一个类在可以被多个线程安全调用时就是线程安全的。
  2. 类要成为线程安全的,首先必须在单线程环境中有正确的行为。
  3. 正确性与线程安全性之间的关系非常类似于在描述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));
}

2. 线程安全性分类

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() 的类。

3. Servlet的线程安全性

Servlet/JSP默认是以多线程模式执行的,所以,在编写代码时需要非常细致地考虑多线程的安全性问题。

无状态Servlet,表示Servlet不包含域,也没有引用其它类的域,一次特定计算的瞬时状态,会唯一的存储在本地变量中,这些本地变量存在线程的栈中,只有执行线程才能访问,一个执行该Servlet的线程不会影响访问同一个Servlet的其它线程的计算结果,因为两个线程不共享状态,他们如同在访问不同的实例。无状态Servlet是线程安全的。

有状态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在只有一个用户访问该Servlet时,程序会正常的运行,但当多个用户并发访问时,就可能会出现其它用户的信息显示在另外一些用户的浏览器上的问题。

4. 同步与互斥

线程通信主要通过共享访问字段或者字段引用的对象完成的,但是有可能出现两种错误:线程干扰(thread interference)和内存一致性错误(memory consistency)。用来防止这些错误的工具是同步(synchronization)。

参考:Java:多线程,线程同步,synchronized关键字的用法(同步代码块、非静态同步方法、静态同步方法)

5. 同步与volatile

Java 语言中的 volatile 变量可以被看作是一种 “程度较轻的 synchronized”;与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。在某些情况下,如果读操作远远大于写操作,volatile 变量还可以提供优于锁的性能优势。

具体描述请参考:Java 理论与实践: 正确使用 Volatile 变量

6. 活性

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

是指程序运行中,多个线程竞争共享资源时可能出现的一种系统状态,每个线程都被阻塞,都不会结束,进入一种永久等待状态。

6.2 饿死

饿死(starvation)描述这样的情况:一个线程不能获得对共享资源的常规访问,并且不能继续工作,当共享资源被贪婪线程长期占有而不可用时,就会发生这样的情况。

6.3 活锁

一个线程经常对另一个线程的操作作出响应,如果另一个线程的操作也对这个线程的操作作出响应,那么就可能导致活锁(livelock)。和死锁类似,发生活锁的线程不能进行进一步操作。但是,线程没有被锁定,它只是忙于相互响应,以致不能恢复工作。

活锁可以比喻为两人在走廊中相遇。A避让的自己的左边让B通过,而B同时避让到自己的右边让A通过。发现他们仍然挡住了对方,A就避让到自己的右边,而B同时避让到了自己的左边,他们还是挡住了对方,所以就没完没了。

7. 高级并发对象

主要增加的高级并发对象有:Lock对象,执行器,并发集合、原子变量和同步器。

相关阅读:

Java:多线程,线程同步,同步锁(Lock)的使用(ReentrantLock、ReentrantReadWriteLock)

Java:多线程,使用同步锁(Lock)时利用Condition类实现线程间通信

Java:多线程,java.util.concurrent.atomic包之AtomicInteger/AtomicLong用法