public class StatelessFactorizer implements Servlet {
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
encodeIntoResponse(resp, factors);
}
}
在这个代码中,是一个实现了Servlet的普通的Servlet,一般而言,这样的servlet是线程安全的,每一个请求都会单独创建一个线程进行隔离处理,不同的请求进入到相同的servlet但是每个线程中的参数都是请求过来的传参,所以不存在线程安全问题。
主要原因:是无状态的。状态:(属性),也就不会有共享问题。
访问StatelessFactorizer的线程不会影响另一个访问同一个StatelessFactorizer的线程的计算结果,因为这两个线程并没有共享状态,就好像它们都在访问不同的实例。由于线程访问无状态对象的行为并不会影响其他线程中操作的正确性,因此无状态对象是线程安全的。
无状态对象一定是线程安全的。
大多数Servlet都是无状态的,从而极大地降低了在实现Servlet线程安全性时的复杂性。只有当Servlet在处理请求时需要保存一些信息,线程安全性才会成为一个问题。
public class StatelessFactorizer implements Servlet {
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
encodeIntoResponse(resp, factors);
save(object);
}
}
比如添加了save,如果save是会写入一个共同的状态中,那么就会存在线程安全问题。
当我们在无状态对象中增加一个状态时,会出现什么情况?假设我们希望增加一个“命中计数器”(Hit Counter)来统计所处理的请求数量。一种直观的方法是在Servlet中增加一个long类型的域,并且每处理一个请求就将这个值加1
public class UnsafeCountingFactorizer implements Servlet {
private long count = 0;
public long getCount() {
return count;
}
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
++count;
encodeIntoResponse(resp, factors);
}
}
不幸的是,UnsafeCountingFactorizer并非线程安全的,尽管它在单线程环境中能正确运行。与前面的UnsafeSequence一样,这个类很可能会丢失一些更新操作。虽然递增操作++count是一种紧凑的语法,使其看上去只是一个操作,但这个操作并非原子的,因而它并不会作为一个不可分割的操作来执行。实际上,它包含了三个独立的操作:读取count的值,将值加1,然后将计算结果写入count。这是一个“读取-修改-写入”的操作序列,并且其结果状态依赖于之前的状态。
给出了两个线程在没有同步的情况下同时对一个计数器执行递增操作时发生的情况。如果计数器的初始值为9,那么在某些情况下,每个线程读到的值都为9,接着执行递增操作,并且都将计数器的值设为10。显然,这并不是我们希望看到的情况,如果有一次递增操作丢失了,命中计数器的值就将偏差1。
个人:这个如果不同的线程访问的是不同的对象那么不会存在线程问题,因为都是隔离的,但是在Spring中的Servlet是单例的,所以对同一个对象进行处理就会存在线程安全的问题。
你可能会认为,在基于Web的服务中,命中计数器值的少量偏差或许是可以接受的,在某些情况下也确实如此。但如果该计数器被用来生成数值序列或者唯一的对象标识符,那么在多次调用中返回相同的值将导致严重的数据完整性问题 [1] 。在并发编程中,这种由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,它有一个正式的名字:竞态条件(Race Condition)。
在UnsafeCountingFactorizer中存在多个竞态条件,从而使结果变得不可靠。当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。换句话说,就是正确的结果要取决于运气 [2] 。最常见的竞态条件类型就是“先检查后执行(Check-Then-Act)”操作,即通过一个可能失效的观测结果来决定下一步的动作。
public class UnsafeCountingFactorizer {
public void ifNullAdd(Map<String, Object> map) {
if (!map.containsKey("name")) {
// dothing
}
}
}
如果是这样的代码,那么多线程情况下就会存在“先检查后执行”,通过if判断执行,比如map中是否存在name,如果不存在则覆盖,可能存在多线程情况下,一个执行到判断是true了,但是进行了线程的切换,这个时候并没有被写入,另外一个线程也get这个时候也是true,就会存在前者写入被后者覆盖的问题。这就是由于不恰当的线程执行顺序导致的问题:竞态条件。
在实际情况中经常会遇到竞态条件。例如,假定你计划中午在University Avenue的星巴克与一位朋友会面。但当你到达那里时,发现在University Avenue上有两家星巴克,并且你不知道说好碰面的是哪一家。在12:10时,你没有在星巴克A看到朋友,那么就会去星巴克B看看他是否在那里,但他也不在那里。这有几种可能:你的朋友迟到了,还没到任何一家星巴克;你的朋友在你离开后到了星巴克A;你的朋友在星巴克B,但他去星巴克A找你,并且此时正在去星巴克A的途中。我们假设是最糟糕的情况,即最后一种可能。现在是12:15,你们两个都去过了两家星巴克,并且都开始怀疑对方是否失约了。现在你会怎么做?回到另一家星巴克?来来回回要走多少次?除非你们之间约定了某种协议,否则你们整天都在University Avenue上走来走去,倍感沮丧。
在“我去看看他是否在另一家星巴克”这种方法中,问题在于:当你在街上走时,你的朋友可能已经离开了你要去的星巴克。你首先看了看星巴克A,发现“他不在”,并且开始去找他。你可以在星巴克B中做同样的选择,但不是同时发生。两家星巴克之间有几分钟的路程,而就在这几分钟的时间里,系统的状态可能会发生变化。
----《Java并发编程实战》
A和B可以视为两个线程,但是因为正好因为顺序的问题,导致没有碰面。
在星巴克这个示例中说明了一种竞态条件,因为要获得正确的结果(与朋友会面),必须取决于事件的发生时序(当你们到达星巴克时,在离开并去另一家星巴克之前会等待多长时间……)。当你迈出前门时,你在星巴克A的观察结果将变得无效,你的朋友可能从后门进来了,而你却不知道。这种观察结果的失效就是大多数竞态条件的本质—基于一种可能失效的观察结果来做出判断或者执行某个计算。这种类型的竞态条件称为“先检查后执行”:首先观察到某个条件为真(例如文件X不存在),然后根据这个观察结果采用相应的动作(创建文件X),但事实上,在你观察到这个结果以及开始创建文件之间,观察结果可能变得无效(另一个线程在这期间创建了文件X),从而导致各种问题(未预期的异常、数据被覆盖、文件被破坏等)。
----《Java并发编程实战》
因为时序的问题,没有通知我得到的反馈是确实不存在,另外一个人也得到反馈,确实不存在,接下来就会做出一个处理。但是这个处理正好相同,就会导致一些问题。
数据竞争的定义:在一个线程写一个变量,在另一个线程读同一个变量,而且写和读没有通过同步来排序。 不能与竞态条件混淆:个人理解竞态条件是一种因为多线程情况下执行顺序的不同造成最终结果不是预计的结果。
数据竞争也是执行顺序造成的一种读和写导致的结果不是预计的,但是更加关注的是对数据的读写操作发生的确实的冲突。
示例:延迟初始化中的竞态条件
使用“先检查后执行”的一种常见情况就是延迟初始化。延迟初始化的目的是将对象的初始化操作推迟到实际被使用时才进行,同时要确保只被初始化一次。在程序清单2-3中的LazyInitRace说明了这种延迟初始化情况。getInstance方法首先判断ExpensiveObject是否已经被初始化,如果已经初始化则返回现有的实例,否则,它将创建一个新的实例,并返回一个引用,从而在后来的调用中就无须再执行这段高开销的代码路径。
public class UnsafeCountingFactorizer {
private Object object = null;
public Object getObject() {
if (object == null) {
object = new Object();
}
return object;
}
}
如果了解过单例模式的话,可以看出来是一种懒加载非线程安全的代码示例。符合先检查后执行的逻辑,存在竞态条件,多线程都执行到ifnull,发现是null之后进入会被覆盖对象。并没有减少开销和单例,造成原有对象中的状态可能被覆盖。
与大多数并发错误一样,竞态条件并不总是会产生错误,还需要某种不恰当的执行时序。然而,竞态条件也可能导致严重的问题。假定LazyInitRace被用于初始化应用程序范围内的注册表,如果在多次调用中返回不同的实例,那么要么会丢失部分注册信息,要么多个行为对同一组注册对象表现出不一致的视图。如果将UnsafeSequence用于在某个持久化框架中生成对象的标识,那么两个不同的对象最终将获得相同的标识,这就违反了标识的完整性约束条件。
----《Java并发编程实战》
会存在之前的object中的固有属性被覆盖的问题,导致程序出现错误。
2.2.3 复合操作
LazyInitRace和UnsafeCountingFactorizer都包含一组需要以原子方式执行(或者说不可分割)的操作。要避免竞态条件问题,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或之后读取和修改状态,而不是在修改状态的过程中。
----《Java并发编程实战》
多线程的情况下,两个线程同时执行,如果存在竞态条件,那么解决办法就是原子性操作,原子性:不能被中断的操作。只要不能被中断就不存在上下文切换之后被覆盖等等的竞态条件。
如果UnsafeSequence中的递增操作是原子操作,那么图1-1中的竞态条件就不会发生,并且递增操作在每次执行时都会把计数器增加1。为了确保线程安全性,“先检查后执行”(例如延迟初始化)和“读取-修改-写入”(例如递增运算)等操作必须是原子的。我们将“先检查后执行”以及“读取-修改-写入”等操作统称为复合操作:包含了一组必须以原子方式执行的操作以确保线程安全性。在2.3节中,我们将介绍加锁机制,这是Java中用于确保原子性的内置机制。就目前而言,我们先采用另一种方式来修复这个问题,即使用一个现有的线程安全类,如程序清单2-4中的CountingFactorizer所示。
----《Java并发编程实战》
public class CountingFactorizer implements Servlet {
private final AtomicLong count = new AtomicLong(0);
public long getCount() {
return count.get();
}
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
count.incrementAndGet();
encodeIntoResponse(resp, factors);
}
}
先不用考虑换成AtomicLong 之后为什么就不会存在竞态条件的问题,因为了解原子性,或者说知道了多线程的情况是为什么存在线程不安全的,因为存在上下文的切换的问题,所以代码被中断,而其它线程继续执行导致执行顺序不对存在数据覆盖或者数据不一致的问题。
在java.util.concurrent.atomic包中包含了一些原子变量类,用于实现在数值和对象引用上的原子状态转换。通过用AtomicLong来代替long类型的计数器,能够确保所有对计数器状态的访问操作都是原子的。 [1] 由于Servlet的状态就是计数器的状态,并且计数器是线程安全的,因此这里的Servlet也是线程安全的。
Servlet是线程安全的,而servelt中只存在一个状态计数器,并且目前的计数器也已经是线程安全的实现类,自增操作也是原子性的,所以该servlet是线程安全的。
2.3 加锁机制
当在Servlet中添加一个状态变量时,可以通过线程安全的对象来管理Servlet的状态以维护Servlet的线程安全性。但如果想在Servlet中添加更多的状态,那么是否只需添加更多的线程安全状态变量就足够了?
目前来说,Java给予了一些默认的线程安全的类的实现供我们使用,但是一个程序或者一个项目不可能这么简单的几个类就可以实现具体的业务,我们可能自己封装一些类,那么怎么可以保证我们自己的类也是线程安全的呢?
假设我们希望提升Servlet的性能:将最近的计算结果缓存起来,当两个连续的请求对相同的数值进行因数分解时,可以直接使用上一次的计算结果,而无须重新计算。(这并非一种有效的缓存策略,5.6节将给出一种更好的策略。)要实现该缓存策略,需要保存两个状态:最近执行因数分解的数值,以及分解结果。
我们曾通过AtomicLong以线程安全的方式来管理计数器的状态,那么,在这里是否可以使用类似的AtomicReference [1] 来管理最近执行因数分解的数值及其分解结果吗?在程序清单2-5中的UnsafeCachingFactorizer实现了这种思想。
----《Java并发编程实战》
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);
lastNumber.set(i);
lastFactors.set(factors);
encodeIntoResponse(resp, factors);
}
}
}
然而,这种方法并不正确。尽管这些原子引用本身都是线程安全的,但在UnsafeCaching Factorizer中存在着竞态条件,这可能产生错误的结果。
在线程安全性的定义中要求,多个线程之间的操作无论采用何种执行时序或交替方式,都要保证不变性条件不被破坏。UnsafeCachingFactorizer的不变性条件之一是:在lastFactors中缓存的因数之积应该等于在lastNumber中缓存的数值。只有确保了这个不变性条件不被破坏,上面的Servlet才是正确的。当在不变性条件中涉及多个变量时,各个变量之间并不是彼此独立的,而是某个变量的值会对其他变量的值产生约束。因此,当更新某一个变量时,需要在同一个原子操作中对其他变量同时进行更新。
在某些执行时序中,UnsafeCachingFactorizer可能会破坏这个不变性条件。在使用原子引用的情况下,尽管对set方法的每次调用都是原子的,但仍然无法同时更新lastNumber和lastFactors。如果只修改了其中一个变量,那么在这两次修改操作之间,其他线程将发现不变性条件被破坏了。同样,我们也不能保证会同时获取两个值:在线程A获取这两个值的过程中,线程B可能修改了它们,这样线程A也会发现不变性条件被破坏了。
----《Java并发编程实战》
线程AB都在进行这个操作,一个线程在get的过程中发生了切换,另外一个线程直接写入了,这是一个。
两个线程都在else里面,但是发生了切换一个set了另外一个又进行了set直接破坏了缓存的数据。
同样可能一个已经发生了set,另外一个正在get获取到了,但是还没有开始写入发生错误。
2.3.1 内置锁
Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。(第3章将介绍加锁机制以及其他同步机制的另一个重要方面:可见性)同步代码块包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。以关键字synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。静态的synchronized方法以Class对象作为锁。
----《Java并发编程实战》
一个是在普通的对象实例中上锁是以this当前对象作为锁,静态方法没有办法获取实例,所以用当前对象的类型对象作为锁。
synchronized(lock){
//访问或修改由锁保护的共享状态
}
每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,而无论是通过正常的控制路径退出,还是通过从代码块中抛出异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
----《Java并发编程实战》
内置锁通过synchronized 关键字对()中的对象进行上锁操作,只有当线程执行到了synchronized时才可以去获取这个对象的锁。
Java的内置锁相当于一种互斥体(或互斥锁),这意味着最多只有一个线程能持有这种锁。当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或者阻塞,直到线程B释放这个锁。如果B永远不释放锁,那么A也将永远地等下去。
由于每次只能有一个线程执行内置锁保护的代码块,因此,由这个锁保护的同步代码块会以原子方式执行,多个线程在执行该代码块时也不会相互干扰。并发环境中的原子性与事务应用程序中的原子性有着相同的含义—一组语句作为一个不可分割的单元被执行。任何一个执行同步代码块的线程,都不可能看到有其他线程正在执行由同一个锁保护的同步代码块。
上锁代码块可以视为原子性的操作,上锁代码块中的一组代码是原子性的操作,则当前线程执行中,不会被其它线程中断插入执行,所以上锁代码块中的操作是线程串行的,可以视为安全,但是上锁代码块以外就不是线程安全的了。
如果上锁代码块中的数据对外开放了,那么后面仍然可能存在线程安全的问题。
这种同步机制使得要确保因数分解Servlet的线程安全性变得更简单。在程序清单2-6中使用了关键字synchronized来修饰service方法,因此在同一时刻只有一个线程可以执行service方法。现在的SynchronizedFactorizer是线程安全的。然而,这种方法却过于极端,因为多个客户端无法同时使用因数分解Servlet,服务的响应性非常低,无法令人接受。这是一个性能问题,而不是线程安全问题。
----《Java并发编程实战》
public class UnsafeCachingFactorizer implements Servlet {
private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>();
public synchronized void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
if (i.equals(lastNumber.get()))
encodeIntoResponse(resp, lastFactors.get());
else{
BigInteger[] factors = factor(i);
lastNumber.set(i);
lastFactors.set(factors);
encodeIntoResponse(resp, factors);
}
}
}
直接对这个service方法进行上锁,但是这样就会存在线程安全的问题,比如一个本身是可以多个线程共同访问,但是因为线程安全的问题,不得不上锁,这个时候表示这个方法一次只能一个线程访问那么就会由并行改为多线程串行,一个线程进入这个方法之后,其它线程进来发现已经被其它线程上锁执行,就会进行一个挂起,这个时候当当前线程执行完毕释放锁资源,去唤醒其它的线程,其它的线程才有机会继续执行,但是也可能执行完毕之后又是这个线程继续执行,等于荒废了其它的线程。
AtomicLong是一种替代long类型整数的线程安全类,类似地,AtomicReference是一种替代对象引用的线程安全类。在第15章将介绍各种原子变量(Atomic Variable)及其优势。
2.3.2 重入
当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。“重入”意味着获取锁的操作的粒度是“线程”,而不是“调用” [1] 。重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1。如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁将被释放。
----《Java并发编程实战》
有点抽象,个人理解大概意思,多个线程进入加锁代码块的时候,如果当前锁已经被一个线程持有,那么尝试获取锁的线程就会被堵塞等待锁的释放。
每一个锁都会保存一个获取计数器和一个获取这个锁的对应的线程。如果计数器是0就表示没有被持有??为什么不用所有者线程去判断???不是更方便吗
当一个线程发送了请求执行到了加锁的代码块,JVM会记录当前请求的线程保存到锁的对应线程中,并且把计数器置为1,表示已经被线程绑定和获取了。
如果同样的线程请求再次获取到这个锁,这个时候已经持有了这个锁,那么就会把计数器+1,当前线程可能积攒了多个请求正在执行,当执行完毕一个加锁代码块,就会-1表示当前线程一个代码块已经执行完毕,当置为0的时候表示当前线程已经可以全部执行完毕,我不要这个锁了。这个时候其它线程就有机会获取锁并且访问。
public class UnsafeCachingFactorizer {
public synchronized void method1() {
method2();
}
private synchronized void method2() {
}
}
如果Java代码中不存在锁重入,那么同一个线程请求method1,同时间接的请求到了method2,两个方法都存在上锁,并且锁对象都是当前这个this实例,那么method1持有锁,method2直接发生阻塞无法正常执行,就造成了死锁。
2.4 用锁来保护状态
由于锁能使其保护的代码路径以串行形式 [1] 来访问,因此可以通过锁来构造一些协议以实现对共享状态的独占访问。只要始终遵循这些协议,就能确保状态的一致性。
访问共享状态的复合操作,例如命中计数器的递增操作(读取-修改-写入)或者延迟初始化(先检查后执行),都必须是原子操作以避免产生竞态条件。如果在复合操作的执行过程中持有一个锁,那么会使复合操作成为原子操作。然而,仅仅将复合操作封装到一个同步代码块中是不够的。如果用同步来协调对某个变量的访问,那么在访问这个变量的所有位置上都需要使用同步。而且,当使用锁来协调对某个变量的访问时,在访问变量的所有位置上都要使用同一个锁。
----《Java并发编程实战》
复合操作:将一个读写的操作拆分称为了多个对一个对象进行多部操作,中间可能发生线程上下文切换的操作。
原子性操作:一个操作或者一组操作不可被打断发生上下文切换的操作。
一种常见的错误是认为,只有在写入共享变量时才需要使用同步,然而事实并非如此
对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。
对象的内置锁与其状态之间没有内在的关联。虽然大多数类都将内置锁用做一种有效的加锁机制,但对象的域并不一定要通过内置锁来保护。当获取与对象关联的锁时,并不能阻止其他线程访问该对象,某个线程在获得对象的锁之后,只能阻止其他线程获得同一个锁。之所以每个对象都有一个内置锁,只是为了免去显式地创建锁对象。 [2] 你需要自行构造加锁协议或者同步策略来实现对共享状态的安全访问,并且在程序中自始至终地使用它们。
----《Java并发编程实战》
虽然看起来锁保护住了对象中的状态,不如说是锁保护住了正在执行的代码块中只能被一个线程访问,避免同时访问。
虽然上锁,但是无法避免其它操作访问对象并进行写入操作。比如两个方法,一个方法上锁,一个没有上锁,但是最终都是读取和写入同一个,如果一个线程A去访问上锁的方法,线程B去访问没有上锁的,那么还是无法解决线程安全的问题。
每个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁。
不同的方法对同一个对象操作一般来说都上同一个对象作为锁,避免出现访问不同的方法导致锁的失效,存在数据安全的问题。
if(!vector.contains(element))
vector.add(element);
并非所有数据都需要锁的保护,只有被多个线程同时访问的可变数据才需要通过锁来保护。
上面的Vector是List的一种线程安全的实现,所以constains和add都是线程安全的。
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
随便抽出来里面的add的实现,发现使用内置锁进行了上锁。那么说这个这个add操作是原子性的,但是为什么存在线程安全的问题呢,虽然两个操作都是线程安全的, 但是存在竞态条件,我在if判读那的时候,发生了切换,另外一个线程也线程安全的获取到了发现不存在,这个时候一起进入add,又是不安全的了。所以一个结论:多个原子性操作,一个是安全的,多个组合起来就不是原子性操作了,需要在外层再上一把锁。