java并发编程实战之线程安全性
2015-04-09 14:36
519 查看
什么是线程安全性?
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类时线程安全的。
也就是说当多个线程访问该类时,他都能表现出正确的行为(类不会被破坏),这就是线程安全。
什么是竞态条件?
先看下面的代码:
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 ;
encoderIntResponse(resp, factors);
}
}
不幸的是,UnsafeCountingFactorizer不是线程安全的,尽管它能在单线程环境中正确的运行。实际上,它包含了三个独立的操作:读取count的值,将值加1,然后将计算结果写入count中。如果计算器的初始值为9,那么在某些情况下,每个线程读到的值都为9,接着执行递增操作,并且都将计算器的值设为10。显然,这并不是我们希望看到的情况,如果有一次递增操作丢失了,命中计算器的值将偏差1。在并发编程中,这种由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,叫做:竞态条件。
如何解决这种竞态条件问题?
可以通过java的内置锁来保护状态,以关键字synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,其中该代码块的锁就是方法调用所在的对象。
synchronized(lock) {
//访问或修改由锁保护的共享状态。
}
由于每一次只能有一个线程执行内置锁保护的代码块,因此,由这个锁保护的同步代码块会以原子的方式执行,多个线程在执行该代码块时也不会相互干扰。
实例代码:
public class SynchronizedFactorizer implements Servlet {
private BigInteger lastNumber;
private BigInteger [] lastFactors;
public synchronized void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
if(i.equals(lastNumber)) {
encoderIntoResponse(resp, lastFactors);
} else {
BigInteger [] factors = factor(i);
lastNumber = i;
lastFactors = factors;
encodeIntoResponse(resp, factors);
}
}
}
如上面的代码所示,SynchronizedFactorizer是线程安全的,但是服务的响应性非常低,无法令人接受。这是个性能的问题,不是线程安全的问题。
如何解决性能的问题?
并非所有数据都需要锁的保护,只有多个线程同时访问的可变数据才需要通过锁来保护。修改的代码如下:
public class CacherFactorizer implements Servlet {
private BigInteger lastNumber;
private BigInteger [] lastFactors;
private long hits;
private long caheHits;
public sysnchronized long getHits() { return hits ;}
public sysnchronized double getCacheHitRatio() {
return (double) cacheHits / (double) hits;
}
public synchronized void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger [] factors = null ;
synchronized (this) {
++hits;
if(i.equals(lastNumber)) {
++cacheHits;
factors = lastFactors.clone();
}
}
if(factors == null) {
factors = factor(i);
synchronized (this) {
lastNumber = i;
lastFactors = factors.clone();
}
encodeIntoResponse(resp, factors);
}
}
}
代码中CachedFactorizer将Servlet的代码修改为了使用两个独立的同步代码块,每个同步代码块都只包含一小段代码。重新构造后的CachedFactorizer实现了再简单性与并发性之间的平衡。要判断同步代码块的合理性,需要在各种需求之间进行平衡,包括安全性、简单性和性能。CachedFactorizer中已经说明在简单性与性能之间能找到某种合理的平衡。
结论:
当使用锁时,应该首先清楚代码块中实现的功能,以及在执行该代码块时是否需要很长的时间,如果持有锁的时间过长将带来性能的问题。当执行时间较长的计算或无法快速完成的操作时(比如网络访问),一定不要使用锁。
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类时线程安全的。
也就是说当多个线程访问该类时,他都能表现出正确的行为(类不会被破坏),这就是线程安全。
什么是竞态条件?
先看下面的代码:
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 ;
encoderIntResponse(resp, factors);
}
}
不幸的是,UnsafeCountingFactorizer不是线程安全的,尽管它能在单线程环境中正确的运行。实际上,它包含了三个独立的操作:读取count的值,将值加1,然后将计算结果写入count中。如果计算器的初始值为9,那么在某些情况下,每个线程读到的值都为9,接着执行递增操作,并且都将计算器的值设为10。显然,这并不是我们希望看到的情况,如果有一次递增操作丢失了,命中计算器的值将偏差1。在并发编程中,这种由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,叫做:竞态条件。
如何解决这种竞态条件问题?
可以通过java的内置锁来保护状态,以关键字synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,其中该代码块的锁就是方法调用所在的对象。
synchronized(lock) {
//访问或修改由锁保护的共享状态。
}
由于每一次只能有一个线程执行内置锁保护的代码块,因此,由这个锁保护的同步代码块会以原子的方式执行,多个线程在执行该代码块时也不会相互干扰。
实例代码:
public class SynchronizedFactorizer implements Servlet {
private BigInteger lastNumber;
private BigInteger [] lastFactors;
public synchronized void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
if(i.equals(lastNumber)) {
encoderIntoResponse(resp, lastFactors);
} else {
BigInteger [] factors = factor(i);
lastNumber = i;
lastFactors = factors;
encodeIntoResponse(resp, factors);
}
}
}
如上面的代码所示,SynchronizedFactorizer是线程安全的,但是服务的响应性非常低,无法令人接受。这是个性能的问题,不是线程安全的问题。
如何解决性能的问题?
并非所有数据都需要锁的保护,只有多个线程同时访问的可变数据才需要通过锁来保护。修改的代码如下:
public class CacherFactorizer implements Servlet {
private BigInteger lastNumber;
private BigInteger [] lastFactors;
private long hits;
private long caheHits;
public sysnchronized long getHits() { return hits ;}
public sysnchronized double getCacheHitRatio() {
return (double) cacheHits / (double) hits;
}
public synchronized void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger [] factors = null ;
synchronized (this) {
++hits;
if(i.equals(lastNumber)) {
++cacheHits;
factors = lastFactors.clone();
}
}
if(factors == null) {
factors = factor(i);
synchronized (this) {
lastNumber = i;
lastFactors = factors.clone();
}
encodeIntoResponse(resp, factors);
}
}
}
代码中CachedFactorizer将Servlet的代码修改为了使用两个独立的同步代码块,每个同步代码块都只包含一小段代码。重新构造后的CachedFactorizer实现了再简单性与并发性之间的平衡。要判断同步代码块的合理性,需要在各种需求之间进行平衡,包括安全性、简单性和性能。CachedFactorizer中已经说明在简单性与性能之间能找到某种合理的平衡。
结论:
当使用锁时,应该首先清楚代码块中实现的功能,以及在执行该代码块时是否需要很长的时间,如果持有锁的时间过长将带来性能的问题。当执行时间较长的计算或无法快速完成的操作时(比如网络访问),一定不要使用锁。
相关文章推荐
- java 并发编程实战 之 线程安全性
- Java 并发编程实战学习笔记——串行任务转并行任务
- [Java 并发] Java并发编程实践 思维导图 - 第二章 线程安全性
- 【Java并发编程实战】—–synchronized
- 【Java并发编程实战】-----“J.U.C”:ReentrantLock之三unlock方法分析
- 【Java并发编程实战】—–“J.U.C”:ReentrantLock之一简介
- Java 并发编程实战学习笔记——路径查找类型并行任务的终止
- java 并发编程实战第三章同步辅助类CyclicBarrier解析
- 【Java并发编程实战】-----“J.U.C”:锁,lock
- 【java并发编程实战】-----线程基本概念
- 【Java并发编程实战】-----“J.U.C”:ReentrantLock之三unlock方法分析
- Java并发编程实战--
- java 并发编程实战 第二天
- 【Java并发编程实战】—–“J.U.C”:锁,lock
- 学习java并发编程实战的一些心得体会(一)
- Java 并发编程之线程安全性
- java 并发编程实战 第五天 ThreadPoolExecutor 源码分析
- Java 并发编程实战学习笔记
- 【Java并发编程实战】-----“J.U.C”:ReentrantLock之二lock方法分析
- 【Java并发编程实战】-----“J.U.C”:ReentrantLock之一简介