在构建稳健的并发程序时,必须正确地使用线程和锁。但这些终归只是一些机制。要编写线程安全的代码,其核心在于要对访问状态操做进行管理,特别是对共享的(Shared)和可变的(Mutable)状态的访问。java
从非正式的意义上来讲,对象的状态是指存储在状态变量中的数据。对象的状态可能包括其余依赖的对象的域。例如,某个HashMap的状态不只存储在HashMap对象自己,还存储在许多Map.Entry对象中。在对象的状态中包含了任何可能影响其外部可见行为的数据。web
“共享”意味着变量能够由多个线程同时访问,而“可变”则意味着变量的值在其生命周期内能够发生变化。一个对象是否须要是线程安全的,取决于它是否被多个线程访问。这指的是在程序中访问对象的方式,而不是对象要实现的功能。要使得对象是线程安全的,须要采用同步机制来协同对对象可变状态的访问。若是没法实现协同,那么可能会致使数据破坏以及其余不应出现的结果。编程
当多个线程访问某个状态变量而且其中有一个线程执行写入操做时,必须采用同步机制来协同这些线程对变量的访问。Java中的主要同步机制是关键字synchronized,它提供了一种独占的加锁方式,但“同步”这个术语还包括volatile类型的变量,显示锁(Explicit Lock)以及原子变量。安全
若是当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。有三种方式能够修复这个问题:网络
- 不在线程之间共享该状态变量;
- 将状态变量修改成不可变的变量;
- 在访问状态变量时使用同步;
若是在设计类时没有考虑并发访问的状况,那么在采用上述方法时可能须要对设计进行重大修改,所以要修复这个问题可谓是知易行难。若是从一开始就设计一个线程安全的类,那么比在之后再将这个类修改成线程安全的类要容易的多。并发
当设计线程安全的类时,良好的面向对象技术、不可修改性,以及明晰的不变性规范都能起到必定的帮助做用。less
当多个线程访问某个类时,无论运行时环境采用何种调度方式或者这些线程将如何交替执行,而且在主调代码中不须要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。性能
在线程安全类中封装了必要的同步机制,所以客户端无需进一步采用同步措施。atom
来看一个实例:一个无状态的类spa
public class StatelessFactorizer { public void service(int i) { } }
它既不包含任何域,也不包含任何对其余类中域的引用。访问StatelessFactorizer的线程不会影响到另外一个访问同一个StatelessFactorizer的线程的计算结果。
无状态对象必定是线程安全的。
在web开发中,大多数Servlet都是无状态的,从而极大地下降了在实现Servlet线程安全性时的复杂性。
当咱们在无状态对象中增长一个状态时,会出现什么状况呢?假设咱们但愿增长一个“命中计数器(Hit Counter)”来统计所处理的请求数量。一种直观的方式是在类中增长一个long类型的域,而且每处理一个请求就将这个值加1,例如:
// 线程不安全 public class StatelessFactorizer { private long count = 0; public long getCount() { return count; } public void service(int i) { ++count; } }
很不幸,虽然递增操做++count是一宗紧凑的语法,使其看上去只是一个操做,但这个操做并不是原子性的,于是它并不会做为一个不可分割的操做来执行。实际上,它包含了三个独立的操做:读取count的值,将值+1,而后将计算结果写入count中。这个一个“读取——修改——写入”的操做序列,而且其结果状态依赖于以前的状态。
在并发编程中,这种因为不恰当的执行时序而出现不正确的结果是一种很是重要的状况,他有一个正式的名字:竞态条件(Race Condition)。
当存在多个竞态条件,会使结果变得不可靠。当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生 竞态条件。换句话说,就是结果取决与运气。最多见的竞态条件类型就是“先检查后执行(Check-Then-Act)”操做,即经过一个可能失效的观测结果来决定下一步的动做。
使用“先检查后执行”的一种常见状况就是延迟初始化。延迟初始化的目的是将对象的初始化操做延迟到实际被使用时才进行,同事要确保只初始化一次。例如:
// 线程不安全 public class LazyInitRace { private LazyInitRace instance = null; public LazyInitRace getInstance() { if (instance == null) instance = new LazyInitRace(); return instance; } }
在LazyInitRace中包含了一个竞态条件,它可能会破坏这个类的正确性。假定线程A和线程B同时执行getInstance。A看到instance为空,于是建立一个新的实例。B一样须要判断instance是否为空。此时的instance是否为空,取决于不可预测的时序,包括线程的调度方式,以及A须要花多长时间来初始化实例并设置instance。若是当B检查时,instance为空,那么在两次调用getInstance时可能会获得不一样的结果,即便getInstance一般被认为是返回相同的实例。
LazyInitRace和StatelessFactorizer都包含一组须要以原子的方式执行的操做。要避免竞态条件问题,就必须在某个线程修改该变量时,经过某种方式阻止其余线程使用这个变量,从而确保其余线程只能在修改操做完成以前或以后读取和修改状态,而不是在修改状态的过程当中。
在StatelessFactorize中的计数器问题,咱们可使用加锁机制,在接下会介绍,这是Java中用于确保原子性的内置机制。就目前而言,咱们先采用另外一种方式来修复这个问题,即便用一个现有的线程安全类:
package cn.net.bysoft.lesson2; import java.util.concurrent.atomic.AtomicLong; // 线程安全 public class StatelessFactorizer { private final AtomicLong count = new AtomicLong(0); public long getCount() { return count.get(); } public void service(int i) { count.incrementAndGet(); } }
在实际状况中,应尽量地使用现有的线程安全对象来管理类的状态。与非线程安全的对象相比,判断线程安全对象的可能状态及其状态转换状况要更为容易,从而也更容易维护和验证线程安全性。
当在StatelessFactorizer中添加一个状态变量时,能够经过线程安全的对象来管理Servlet的状态以维护安全性。但若是想在其中添加更多的状态,那么是否只需添加更多的线程安全状态变量就足够了?
要保持状态的一致性,就须要在单个原子操做中更新全部相关的状态变量。
Java提供了一种内置锁来支持原子性:同步代码块(Synchronized Block)。同步代码块包括两部分:一个做为锁的对象引用,一个做为由这个锁保护的代码块。以关键字synchronized来修饰的方法就是一种横跨整个方法体的同步块,其中该同步块的锁就是方法调用所在的对象。静态的synchronized方法以Class对象做为锁。
每一个Java对象均可以用作一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视锁(Monitor Lock)。线程在进入同步代码块以前会自动得到锁,而且在退出同步代码块时自动释放锁,而不管是经过正常的控制路径退出,仍是经过从代码块中抛出异常退出。得到内置锁的惟一途径就是进入由这个锁保护的代码块或方法。
当某个线程请求一个由其余线程持有的锁时,发出请求的线程就会阻塞。而后,因为内置锁是可重入的,所以若是某个线程试图得到一个已经由本身持有的锁,那么这个请求就会成功。“重入”意味着获取锁的操做的颗粒是“线程”,而不是“调用”。重入的一种实现方法是,为每一个锁关联一个获取计数值和一个全部者线程。当计数值为0时,这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,而且将获取计数值设置为1.若是同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁被释放。
因为锁能使其保护的代码路径以串行形式来访问,所以能够经过锁来构造一些协议以实现对共享状态的独占访问。只要始终遵循这些协议,就能确保状态的一致性。
对于可能被多个线程同时访问的可变状态变量,在访问它时都须要持有一个锁,在这种状况下,咱们称状态变量是由这个锁保护的。
对象的内置锁与其状态之间没有内在的关联。虽然大多数类都将内置锁用做一种有效的加锁机制,但对象的域并不必定要经过内置锁来保护。当获取与对象关联的锁时,并不能阻止其余线程访问该对象,某个线程在得到对象的锁以后,只能阻止其余线程得到同一个锁。之因此每一个对象都有一个内置锁,只是为了免去显示地建立锁对象。你须要自行构造加锁协议或者同步策略来实现对共享状态的安全访问,而且在程序中自始至终地使用他们。
每一个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道是哪个锁。
当某个变量由锁来保护时,意味着在每次访问这个变量时都须要首先得到锁,这样就确保在同一时刻只有一个线程能够访问这个变量。当类的不变性条件涉及多个状态变量时,那么还有另一个需求:在不变性条件中的每一个变量都必须由同一个锁来保护。
对于每一个包含多个变量的不变性条件,起重涉及的全部变量都须要由同一个锁来保护。
有时候将一个方法用synchronized修饰得到锁,性能方面会很是糟糕。由于每次只能有一个线程在执行这个方法。若是这个方法须要执行很长时间,那么其余的线程必须一直等待这个线程执行完。
在设计时要判断同步代码块的合理大小,须要在各类需求之间权衡,包括安全性、简单性和性能。有时候,在简单性和性能之间会发生冲突,但在两者之间一般能找到某种合理的平衡。
一般,在简单性与性能之间存在着相互制约因素。当实现某个同步策略时,必定不要盲目地为了性能而牺牲简单性(这可能会破坏安全性)。
当使用锁时,你应该清除代码块中实现的功能,以及在执行该代码块时是否须要很长的时间。不管是执行计算密集的操做,仍是在执行某个可能阻塞的操做,若是持有锁的时间过长,那么都会带来活跃性或性能问题。
当执行时间较长的计算或者可能没法快速完成的操做时(例如,网络I/O或控制台I/O),必定不要持有锁。