处理器的时钟频率已经很难再提高,想要提升计算机的性能,一个很明显的趋势是使用多处理器。
因为程序调度的基本单元是线程,一个单线程程序一次只能运行在一个处理器上,在双处理器系统,就浪费了一般的CPU资源,在100个CPU系统中,就浪费了99%的CPU资源。而使用多线程编程则能够充分利用处理器资源,提高吞吐量。
多线程的使用并不是绝对有利的,它同时也引入了单线程环境不存在的安全性问题,在多线程环境中,因为竞争条件的存在,在没有充分同步的情况下,多线程中的各个操作的顺序是不可预测的,有时甚至让人惊讶。
在设计良好的应用程序使用多线程,能够获得不错的性能收益,但多线程仍会带来一定程度的性能开销。上下文切换,当调度程序时挂起正在运行的线程,另一个线程开始运行--这在多线程系统中是很频繁的,会带来很大的性能消耗;保存和恢复线程执行的上下文,会让CPU的时间花费在对线程的调度而不是运行上。当线程共享数据时,需要使用同步机制,这回限制编译器的优化。
......
什么是线程安全
当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要同步及在调用代码时不需要额外的协调,这个类的行为仍是正确的,那么这个类是线程安全的。
无状态的对象永远是线程安全的
public class StatelessServlet implements Servlet{
public void service(ServletRequest req, ServletResponse res){
Integer i = extractFromRequest(req);
Integer[] factors = factor(i);
encodeIntoResponse(res, factors);
}
}
以一段伪代码来说明。
StatelessServlet是一个无状态的servlet,因为它不包含域也没有引用其他类的域。线程之间不共享状态,一个请求过来,会唯一地存在本地变量,这些变量保存在线程的栈中,只有执行线程才能访问,不会影响同一个servlet的其他请求线程。
因为线程访问无状态对象的行为,不会影响其他线程访问该对象的正确性,因此无状态对象是线程安全的。
原子性
给上面的伪代码加上计数功能,如下:
public class UnsafeCountServlet implements Servlet{
private long count = 0;
public long getCount(){return count;}
public void service(ServletRequest req, ServletResponse res){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
++count;
encodeIntoResponse(res, factors);
}
}
此时的UnsafeCountServlet是一个有状态的类,在多线程环境中不再是线程安全的,因为++count不是一个原子操作,而是三个离散操作的的简写:获取当前值,加1,返回新值,是一个“读-改-写”操作。
这样的操作在多线程环境中很容易出现问题,加入初始值为0,某个时刻线程一读取到值0,此时线程调度,另一个线程二也读到0,然后加1,返回新值1,再切回线程一,加1,返回新值1,这就缺少了一次自增。
出现这样的错误是因为:竞争条件的存在。
竞争条件
当计算的正确性依赖于运行时的时序或者多线程的交替时,就会产生竞争条件。最常见的竞争条件就是“检查再运行”。
下面是一个惰性初始化的例子:
public class LazyInitClass{
private ExpensiveObject instance = null;
public ExpensiveObject getInstance(){
if(instance == null){
instance = new ExpensiveObject();
}
return instance;
}
}
LazyInitClass的竞争条件会破坏其正确性。假如两个线程A和B同时执行getInstance(),A看到instance为null,执行初始化,此时B也在检查instance是否为null,而instance是否为null,依赖于时序,是无法预期的,如果B检查也为null,则线程B也会执行初始化,得到两个不同的对象,和惰性初始化只初始化一次是矛盾的。
使用线程安全对象管理类的全部状态,可以维护类的线程安全性
将UnsafeCountServlet改造一下,代码如下:
public class SafeCountServlet{
private final AtomicLong count = new AtomicLong(0);
public long getCount(){return count.get();}
public void service(ServletRequest req, ServletResponse res){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
count.incrementAndGet();
encodeIntoResponse(res, factors);
}
}
java.util.concurrent.atomic包中包括了原子变量类,这些类用来实现数字和对象引用的原子状态转换,把long换成AtomicLong,可以确保所有访问计数器状态的操作都是原子的,计数器是线程安全的了,而计数器的状态就是SafeCountServlet的状态,所以SafeCountServlet也变成了线程安全的。
使用锁可以维护类的线程安全性
使用线程安全对象管理类的全部状态,可以维护类的线程安全性,但如果类中存在多个实例域,即有多个状态,仅仅加入更多的线程安全的状态变量时不够的。
为了保护状态的一致性,要在单一的原子操作中更新相互关联的的状态变量。
java提供了锁可以保护类的线程安全性。