本文所整理的知识点源自于 (Java 并发编程实战) 一书,有兴趣的童鞋可以去网上下载或者在评论区里向博主讨要。
在并发编程中,由于不恰当的执行程序而出现不正确的结果是一种非常重要的情况,它的名字叫:竞态条件(RaceCondition).
在UnsafeCountingFactorizer中存在多个竞态条件,从而使结果变得不可靠。当某个正确性取决于多个线程的交替执行时序时,
就会发生竞态条件。相当于正确的结果要取决于运气。最常见的竞态条件类型是:先检查后执行(Check-Then-Act)操作,即通过
一个可能失效的结果来决定下一步的动作。
比如说:A和B约定在商城门口集合,A到了之后发现现场有两家商城,而他们并没有约定是哪一家,在1号商城门口没有看见B
于是A走向2号商城,但是2号商城门口也没有。这时有两种结果:1、B迟到了,还没有到任何一家商城;2,B先到了2号商城,
没看到A,于是走向1号商城,但此时A已经离开1号商城走向2号商城。这样下来,除非A和B还有别的见面方法,否则会一直
互相寻找。
在“去另一家商城寻找中”,问题有:A走向2号商城时,B离开了2号商城;同样,B从2号商城走向1号商城,但这可能不是
同时发生。商城之间的路程需要几分钟,在这几分钟里,程序的状态可能会发生变化。
这个例子就是一种竞态条件,想要获得正确的结果(A和B见面),必须取决于事情发生的时序(其中一个到达商城,在离开去另
一家商城之前等待多长时间.....)。当A离开1号商城门口时,它在1号商城门口的观察结果将会无效,因为B可能从另一条路走来
,A不知道。这种观察结果的失效就是大多数竞态条件的本质-基于可能失效的结果来做出判断或某个计算。这类的竞态条件
称为“先检查后执行”:首先观察某个条件为真(主线程判断文件A),A不存在,主线程创建A,但这之中,观察结果可能会无效
另一个执行创建A文件的线程启动了,创建了文件A,主线程先前的判断结果失效,但是依然又创建了文件A,这就导致了可能
出现的一系列问题(数据破坏,文件丢失,未知异常....)。
使用“先检查后执行”的一种常见情况就是延迟初始化。延迟初始化的目的是将对象的初始化操作推迟到实际使用时再进行,同时
要确保只被初始化一次:
@NotThreadSafe
public cass ObjFactory {
private Obj instance;
public Obj getInstance() {
if(instance == null) {
instance = new Obj();
}
return instance;
}
}
在ObjFactory中有一个竞态条件,它可能会破坏这个类的正确性。如:线程A和线程B同时执行getInstance。A看到instance为空,
创建一个Obj实例。B也需要判断instance是否为空。但此时instance是否为空取决于不可预测的时序,线程的调度,A创建Obj需要
的时间。如果B检查时,instance为空,那么两次调用getInstance会得到不同的结果。
在UnsafeCountingFactorizer的统计命中计数操作中还有一种竞态条件-“读取-修改-写入”(如:sun++),基于对象之前的状态来
定义对象状态的转换。要正确递增数值,必须知道它之前的值,并保证在递增的同时没有其他线程来修改或使用这个值。
与大多数并发错误一样,竞态条件不是总是产生错误,还需要某种不恰当的执行时序。但是如果在一些重要的操作中产生了竞态条件
比如,初始注册表,那么多个行为就会返回不一样的视图。
解决方法:
1、同步锁:synchronized (也称:内置锁)
在方法名前加上 synchronized 关键字,可以保证在同一时间内只能有一个线程调用本方法。当A执行 getInstance 判断完成到实例化 Obj期间,线程B或C、D等
会按照各自的优先级在锁处等待,直到A完成方法,释放锁,getInstance 才可以被其他线程调用。
弊端:
Java的内置锁相当于一种互斥体,每次只能有一个线程能够持有这种锁,假设A先掌握锁,但由于别的原因一直没有释放锁,那么所有
需要执行这个方法的线程B、C、D就要永远的等下去。
这种同步机制使得确保Service方法变得很安全,但是,这种方法却过于极端了,因为每次只能有一个用户来进行访问,服务器响应
会很慢。这时,我们就需要使用”重入“来解决这个弊端。
重入
当某个线程请求一个由其他线程持有的锁时,发出的请求会阻塞。但是,内置锁是可重入的,所以,如果某个线程视图获得一个已经
由它自己持有的锁,请求就会成功。“重入”意味着获取锁的粒度是”线程“而不是”调用“。重入一种实现方法是:为每个所关联一个获取
计数值和一个所有者线程。当计数值为0时,这个锁被认为没有被任何线程所执有。当一个线程请求一个未被执有的锁时,JVM会记下
锁的执有者,计数值+1,如果同一个线程再次请求这个锁,计数值会++,当线程释放锁时,计数值会相应的--,当计数值为0是,锁
将被释放。
代码:
//多个线程调用
public class Service {
public synchronized void service1() {
System.out.println("1号启动");
service2();
}
public synchronized void service2() {
System.out.println("2号启动");
service3();
}
public synchronized void service3() {
System.out.println("3号启动");
}
}
//主对象
public class MyThread extends Thread {
public void run() {
Service service = new Service();
service.service1();
}
}
//测试
class Test {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
myThread.start();
}
}
//结果
1号启动
2号启动
3号启动