最近在看《java并发编程实战》,希望自己有毅力把它读完。
线程本身有很多优势,比如可以发挥多处理器的强大能力、建模更加简单、简化异步事件的处理、使用户界面的相应更加灵敏,但是更多的需要程序猿面对的是安全性问题。看下面例子:
public class UnsafeSequence { private int value; /*返回一个唯一的数值*/ public int getNext(){ return value++; } }
UnsafeSequence的问题在于,如果执行时机不对,那么两个线程在调用getNext时会得到相同的值,图1给出了这种错误情况。虽然递增运算value++看上去是单个操作,但事实上它包含三个独立的操作: 读取value、将value加1、将计算结果写入value。由于运行时可能将多个线程之间的操作交替执行,因此这两个线程可能同时执行读操作,从而使它们得到相同的值,并都将这个值加1。结果就是,在不同线程的调用中返回了相同的值。
在UnsafeSequence中说明的是一种常见的并发安全问题,称为竞态条件。当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。
再举个例子,延迟初始化中的竞态条件:
public class LazyInitRace { private HashMap<String, String> instance = null; public HashMap<String, String> getInstance(){ if (instance == null) { instance = new HashMap<String, String>(); } return instance; } }
在LazyInitRace中包含一个竞态条件,它可能会破坏这个类的正确性。假定线程A和线程B同时执行getInstance,A看到instance为空,因而创建一个新的HashMap实体,B同样需要判断instance是否为空,此时的instance是否为空,要取决于不可预测的时序,如果当B检查时,instance也为空,那么在两次调用getInstance时可能会得到不同的结果,即使getInstance通常被认为是返回相同的实例。
java提供了锁机制来解决这一问题,但这些终归只是一些机制,要编写线程安全的代码,其核心在于要对状态访问操作进行管理。
一、对象的状态是指存储在状态变量(例如实例或者静态域)中的数据。如果一个对象无状态,它一定是线程安全的。
public class StatelessServlet implements Servlet { public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException { int i = 1; i++; ... } }
与大多数servlet相同,StatelessServlet是无状态的:它即不包含任何域,也不包含任何对其他类中域的引用。计算过程中的临时状态仅存在于线程栈上的局部变量中(这块需要对jvm内存分配有基础了解),并且只能由正在执行的线程访问。线程之间没有共享状态,由于线程访问无状态对象的行为并不会影响其他线程中操作的正确性,因此无状态对象是线程安全的。
二、用锁来保护状态
内置锁
java提供了一种内置的锁机制来支持原子性:同步代码块。同步代码块包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。以关键字synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象,一般不要这么做,这样会影响效率。
synchronized(lock){
//访问或修改由锁保护的共享状态
}
每一个java对象都可以用作一个实现同步的锁,这些锁被称为内置锁或者监视器锁。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁。
对象的内置锁与其状态之间没有内在的关联。虽然大多数类都将内置锁用做一种有效的加锁机制,但对象的域不一定要通过内置锁来保护。当获取与对象关联的锁时,并不能阻止其他线程访问该对象,某个线程在获得对象的锁以后,只能阻止其他线程获得同一个锁。之所以每个对象都有一个内置锁,只是为了免去显式的创建锁对象。
开发中常见的内置锁的使用方法是,将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态饿代码路径进行同步,使得在该对象上不会发生并发访问,例如,Vector和其他的同步集合类。
总之,以对象的状态来理解线程安全以及为什么加锁,是这部分的核心意思。