如果多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就可能会出错。有三种方式可以修复这个问题:
- 1)不在线程直接共享该状态(设计成只供单线程独自使用)
- 2)将状态变量变成不可变的变量(可见,没有变量的类也一定是线程安全的)
- 3)在访问状态变量时使用同步
如何定义一个类是否线程安全:当多个线程访问某个类时,不管运行时环境采用何种调用方式(单线程或多线程)又或者这些线程如何交替执行,并且在主调用代码(即使用该类的客户代码)中不需要任何额外的同步或协同机制,这个类都能表现出正确的预期行为,那么这个类就是线程安全的。
发生线程安全问题的情况有几种:
1、保证原子操作。例如以为i++是原子操作,其实不然,i++是分两步完成的,所以当我们在多个线程并发操作时就可能产生错误,例如以下代码:
public class UnsafeSequence{
private int value;
public int getNext(){
return value++;
}
}
在多线程时:
正确的书写方法:
public class UnsafeSequence{
private int value;
public synchronized int getNext(){
return value++;
}
}
解决这个问题,除了可以用同步方法外,还可以使用java提供的原子变量,例如AtomicLong 、AtomicReference
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);
}
}
因此,servlet中的变量都必须使用同步或者原子变量。
2、竞态条件(racing condition)
例如我们常用的javabean中的get代码:
public class LazyInitRace {
private ExpensiveObject instance = null;
public ExpensiveObject getInstance() {
if (instance == null)
instance = new ExpensiveObject();
return instance;
}
}
其实在多线程下也是错误的,因为同时两个线程访问getInstance方法就有可能返回了两个不同的对象
正确写法
public class LazyInitRace {
private ExpensiveObject instance = null;
public synchronized ExpensiveObject getInstance() {
if (instance == null)
instance = new ExpensiveObject();
return instance;
}
}
内置锁(intrinsic Lock)
使用synchronized(this)括起的代码段(如果没有指定其他锁名),都默认使用类中的内置锁,即锁住对象本身。
但使用synchronized声明的方法锁住的是调用该方法的对象。这个要注意。
内置锁是一种互斥锁,只有一个线程能持有锁,其他线程必须等待。synchronized使代码段以串行方式执行,代码段中的变量都受锁保护。
使用锁的简单原则
- 如果使用了synchronized,那么每次只能供一个线程调用,因此servlet中的方法不宜使用。
- 一个线程使用一个CPU内核,所以多核环境对多线程程序很有帮助。
- 同步代码段大会影响吸性能,所以当执行时间长或者无法快速完成的操作,如网络I/O等,建议不要用锁。
structs Action的线程问题:
Struts1 Action是单例模式并且必须是线程安全的,因为仅有Action的一个实例来处理所有的请求。单例策略限制了Struts1 Action能作的事,并且要在开发时特别小心。Action资源必须是线程安全的或同步的。
Struts2 Action可以使用原型模式scope=prototype,这样对象为每一个请求产生一个实例,因此没有线程安全问题。
总结:
在Java的Web服务器环境下开发,要注意线程安全的问题。最简单的实现方式就是在Servlet和Struts Action里不要使用类变量、实例变量,但可以使用类常量和实例常量。如果有这些变量,可以将它们转换为方法的参数传入,以消除它们。
注意一个容易混淆的地方:被Servlet或Action调用的类中(如值对象、领域模型类)中是否可以安全的使用实例变量?如果你在每次方法调用时新建一个对 象,再调用它们的方法,则不存在同步问题---因为它们不是多个线程共享的资源,只有共享的资源才需要同步---而Servlet和Action的实例对于多个线程是共享 的。
换句话说,Servlet和Action的实例会被多个线程同时调用,而过了这一层,如果在你自己的代码中没有另外启动线程,且每次调用后续业务对象时都是先 新建一个实例再调用,则都是线程安全的。