内容简介:JAVA 并发之路 (二) 线程安全性
对象的状态是指存储在状态变量(例如实例域,静态域)中的数据,还可能包括其他依赖对象的域。对象中的域的值的集合描述着当前特征的信息,这就是对象的状态。在对象的状态中包含了任何可能影响其外部可见行为的数据。
要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对 共享的 和 可变的 状态的访问。“ 共享 ”意味着变量可以被多个线程同时访问;“ 可变 ”意味着变量的值在其生命周期内可以发生变化。
一个对象是否需要是线程安全的,取决于它是否被多个线程访问。这指的是在程序中访问对象的方式,而不是对象要实现的功能。要使得对象是线程安全的,需要采用同步机制来协同对对象可变状态的访问。
Java中的主要同步机制是关键字synchronized,它提供了一种独占的加锁方式。但是“ 同步 ”这个术语还包括volatile类型的变量,显式锁以及原子变量。
如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。有三种方式可修复这个问题:
- 不在线程之间共享该状态变量。
- 将状态变量修改为不可变的变量;
- 在访问状态变量时使用同步;
应当始终遵循的原则
编写并发应用程序时应当始终遵循的原则是:首先使代码正确运行,然后再提高代码的速度。即便如此,最好也只是当性能测试结果和应用需求告诉你必须提高性能,以及测量结果表明这种优化在实际环境中确实能够带来性能提升时,才进行优化。
线程安全性
线程安全性是一个在代码上使用的术语,但它只是与状态相关的,因此只能应用于封装其状态的整个代码,这可能是一个对象,也可能是整个程序。
在线程安全性的定义中,最核心的概念就是 正确性 。正确性的含义是:某各类的行为与其规范完全一致。在良好的规范中通常会定义各种 不变性条件来约束对象的状态 ,以及定义各种 后验条件来描述对象操作的结果 。
线程安全性: 当多个线程访问某个类时 ,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同, 这个类都能表现出正确的行为,那么就称这个类是线程安全的。
也可以将线程安全类认为是一个在单线程环境和并发环境都不会被破坏的类。如果正确地实现了某个对象,那么在任何操作中都不会违背不变性条件或后验条件。在线程安全类的对象实例上执行的任何串行或并行操作都不会使对象处于无效状态。
无状态对象一定是线程安全的。因为不存在任何域,也不包含任何对其他类中域的引用。计算过程中的临时状态仅仅存在于线程栈上的局部变量中,并且只能由正在执行的线程访问。
竞态条件: 在并发编程中,由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况。当某个计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件。
常见的竞态条件类型: “ 先检查后执行 ”操作:通过一个可能失效的观测结果来决定下一步的动作,或者来做出判断,或者执行某个计算。常见情况就是延迟初始化。 “ 读取-修改-写入 ”操作:基于对象之前的状态来定义对象状态的转换。比如递增运算。
要避免竞态条件问题,就必须在某个线程修改变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或之后读取和修改状态,而不是在修改状态的过程中。
复合操作
原子操作是指,对于访问同一个状态的所有操作(包括该操作本身)来说,这个操作是一个以原子方式执行的操作。
复合操作是指,包含了一组必须以原子方式执行的操作以确保线程安全性。比如“ 先检查后执行 ”,“ 读取-修改-写入 ”等操作统称为复合操作。
如何确保原子性?
-
使用一个现有的线程安全类:java.util.concurrent.atomic包中就包含了一些原子变量类。当在无状态的类中添加 一个 状态时,如果该状态完全由线程安全的对象来管理,那么这个类仍然是线程安全的。然而,当状态变量的数量由一个变为多个时,并不会像状态变量由零个变为一个那样简单。。。
为何?见下例
在线程安全性的定义中要求,多个线程之间的操作无论采用何种执行时序或交替方式,都要保证不变性条件不被破坏。 当在不变性条件中涉及多个变量时,各个变量之间并不是彼此独立的,而是某个变量的值会对其他变量的值产生约束。因此,当更新某一个变量时,需要在同一个原子操作中对其他变量同时进行更新。
@NotThreadSafe public class UnsafeCachingFactorizer implements Servlet { private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>(); private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>; public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); if (i.equals(lastNumber.get())) { encodeIntoResponse(resp, lastFactors.get()); } else { BigInteger[] factors = factor(i); // 尽管这里对set方法的每次调用都是原子的,但仍然无法同时更新lastNumber和lastFactors这两个 // 状态变量,而它们之间存在值的约束关系.而且也不能保证会同时获取这两个值。 lastNumber.set(i); lastFactors.set(factors); encodeIntoResponse(resp, factors); } } }
-
加锁机制:Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block).
同步代码块包括两部分,一个是作为锁的 对象引用 ,一个作为由锁保护的 代码块 。以关键字synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的 锁 就是方法调用所在的对象。静态的synchronized方法以Class对象作为 锁 。
每个 Java 对象都可以用做一个实现同步的锁,这些锁被称为 内置锁或监视器锁 ,它们相当于一种互斥体或互斥锁,这意味着最多只有一个线程能持有这种锁。所以每次只能有一个线程执行内置锁保护的代码块,所以由这个锁保护的同步代码块是 以原子方式执行 的。而获得锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
并发环境中的原子性与事务应用程序中的原子性有着相同的含义,即 一组语句作为一个不可分割的单子被执行 。
重入:内置锁是可以重入的。如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功,而不会阻塞。重入进一步提升了加锁行为的封装性。
重入意味着获取锁的操作的粒度是“线程”,而不是调用。一种实现方法就是为每个锁关联一个 获取计数值 和一个 所有者线程 。当锁不被任何线程持有时,计数值为0;当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并将获取计数值置为1;如果同一个线程再次获取这个锁,计数值将递增;而当线程退出同步代码块时,计数值会相应地递减;直到计数值为0时,锁被释放。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
注意力经济: 如何把大众的注意力变成生意
吴修铭 / 中信出版集团股份有限公司 / 2018-4-1 / 69
编辑推荐 这本书由万维钢作序,并在《得到》日课中多次推荐!中文版未上市之前,中文前沿媒体就在力推这本书!关于注意力争夺战的历史和现在,作者给了权威的梳理和定位! 百年来,在争夺注意力的战场上,媒体、广告、商人、企业和大众成为博弈的主角。商人是如何在注意力争夺战中获利的?媒体是如何在改变报道形式的?广告是如何进化的?以及,营销是如何变得随处可见、无孔不入的呢?这本书讲述了令商人或企业从吸......一起来看看 《注意力经济: 如何把大众的注意力变成生意》 这本书的介绍吧!