首页 > 代码库 > 《Java并发变成实践》读书笔记---第二章 线程安全性
《Java并发变成实践》读书笔记---第二章 线程安全性
什么是线程安全性
要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的(Shared)和可变的(Mutable)状态的访问。从非正式的意义上来说,对象的状态是指存储在状态变量(例如实例或静态域)中的数据。“共享”意味着变量可以由多个线程同时访问,而“可变”则意味着变量的值在其生命周期内可以发生变化。所以编写线程安全的代码更侧重于如何防止在数据上发生不受控的并发访问。
如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。有三种方式可以修复这个问题。
1.不在线程之间共享该状态变量
2.将状态变量修改为不可变的变量
3.在访问状态变量时使用同步
当设计线程安全的类时,良好的面向对象技术,不可修改性,以及明细的不可变性规范都能起到一定的帮助作用
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调用代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
下面给出一个无状态的Servlet的例子,一个线程安全的类,无状态的类是绝对线程安全的,而无状态的类就是指上述所说的不存在实例或者静态域。
public class StatelessFactorizer extends HttpServlet{ @Override public void service(ServletRequest arg0, ServletResponse arg1) throws ServletException, IOException { // TODO Auto-generated method stub super.service(arg0, arg1); } }
原子性
只要给无状态对象加上状态,这个对象就会变成非线程安全的,例如给上述的Servlet增加一个计数器。
public class UnsafeCountingFactorizer extends HttpServlet{ private long count = 0; @Override public void service(ServletRequest arg0, ServletResponse arg1) throws ServletException, IOException { // TODO Auto-generated method stub super.service(arg0, arg1); count++; } }
原因是这个计数器的自增不是一个原子性的操作,这是一个“读取-修改-写入”的操作序列,并且其结果状态依赖于之前的状态。
竞争条件
UnSafeCountingFactorizer存在竞争条件(count 成员变量),从而使得结果变得不可靠。当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。
当两个线程同时竞争修改counnt这个变量时,就是出现计时器出错的情况,因为当前线程的累加操作有赖于上一个线程的结果,当前线程需要观察上上一线程的结果值。这种观察结果失效就是大多数竞态条件的本质--基于一种可能失效的观察结果来作出判断或者执行某个计算。这种类型的竞态条件称为”先检查后执行“。
使用”先检查后执行“的一种常见情况就是延迟初始化。
public class LazyInitRace { private LazyInitRace instance = null; public LazyInitRace getInstance(){ if(instance == null){ instance = new LazyInitRace(); } return instance; } }
LazyInitRace中就产生了一个竞条件,instance的初始化会被重复执行,假定线程A和线程B同时执行getInstance,A看到instance为空,就会创建一个LazyInitRace,但是B也同时需要判读instance是否为空,要取决于不可预测的时序。
与大多数并发错误一样,竞态条件并不总会产生错误,还需要某种不恰当的执行时序,这就是多线程问题比较难定位原因。
复合操作
LazyInitRace和UnsafeCountingFactorizer都包含一组需要以原子方式执行(或者说不可分割)的操作。要避免竞态条件问题,就必须在某个线程修改该变量时,通过某种方式阻止其他线程修改这个变量,从而确保其他线程只能在修改操作完成之前或者之后读取和修改状态,儿不是在修改状态过程中。
我们把CountFactorizer中的计数器的”读取-修改-写入“等操作称之为复合操作:包含了一组必须以原子方式执行的操作以确保线程安全性。下面我们使用java.util.concurrent包中包含的一些原子变量类来保证数值和对象引用上的原子状态转换来改造CountFactorizer,使得它变得线程安全。
public class CountFactorizer extends HttpServlet{ private final AtomicLong count = new AtomicLong(0); @Override public void service(ServletRequest arg0, ServletResponse arg1) throws ServletException, IOException { // TODO Auto-generated method stub super.service(arg0, arg1); count.incrementAndGet(); } }
加锁机制
Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。对于LazyInitRace的非线程安全,我们可以通过加锁机制来保证其线程安全。在getInstance方法上加上synchronized修饰符。
public class LazyInitRace { private LazyInitRace instance = null; public synchronized LazyInitRace getInstance(){ if(instance == null){ instance = new LazyInitRace(); } return instance; } }
重入
每个锁都关联一个请求计数器和一个占有他的线程,当请求计数器为0时,这个锁可以被认为是unhled的,当一个线程请求一个unheld的锁时,JVM记录锁的拥有者,并把锁的请求计数加1,如果同一个线程再次请求这个锁时,请求计数器就会增加,当该线程退出syncronized块时,计数器减1,当计数器为0时,锁被释放。
public class Widget { public synchronized void doSomething() { ... } } public class LoggingWidget extends Widget { public synchronized void doSomething() { System.out.println(toString() + ": calling doSomething"); super.doSomething(); } }如果没有Java锁的可重入性,当一个线程获取LoggingWidget的doSomething()代码块的锁后,这个线程已经拿到了LoggingWidget的锁,当调用父类中的doSomething()方法的时,JVM会认为这个线程已经获取了LoggingWidget的锁,而不能再次获取,从而无法调用Widget的doSomething()方法,从而照成死锁。从中我们也能看出,java线程是基于“每线程(per-thread)”,而不是基于“每调用的(per-invocation)”的,也就是说java为每个线程分配一个锁,而不是为每次调用分配一个锁。