只有一个状态的类的线程安全性
无状态的对象,一定是线程安全的。
无状态:不包括任何域,也不包含任何对其它类中域的引用。
兑态条件(Race condition):由于不恰当的执行顺序而出现的不正确的结果。
先检查后执行(check-then-act):通过一个可能失效的观测结果来执行下一步的动作。
星巴克找人的例子;
延迟初始化中的竞态条件;
i++;
//延迟初始化中的竞态条件
@NotThreadSafe
public class LazyInitRace{
private ExpensiveObject = null;
private ExpensiveObject getInstance(){
if(instance == null){
instance = new ExpensiveObject();
}
return instance;
}
}
数据竞争:如果在访问非共享的非final类型的域时没采用同步来进行协同,那么就会引起数据竞争。与竞态条件是有区别的。请从定义区分。
对于资源(变量)的修改需要以原子的方式执行,防止其它线程使用这个资源(变量),才能保证修改数据的安全性。
复合操作:
- 先检查后执行、『读取-修改-写入』(i++)操作,
- 包含了一组必须以原子方式执行的操作,以确保线程安全性⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ 。
线程安全的自增操作
//AtomLong类型的变量,保证自增的原子性
@ThreadSafe
public class CountingFactorizer implement Servlet {
private final AtomicLong count = new AtomicLong(0);
public long getCount(){return count;}
public void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor[i];
count.IncrementAndGet();
encodeIntoResponse(resp, factors);
}
}
在无状态中的类中增加一个状态时,如果这个状态由线程安全的类来管理,那个这个类是线程安全的,但是有多个状态时,却不是这么简单的。
应该尽可能的使用现有的线程安全的对象,如AtomicLong。
有多个状态的类的线程安全性
通过加锁机制实现。
要保持状态的一致性,就要在单个原子操作中更新所有相关的状态变量。
1.内置锁:synchronized代码块
包括两部分:一个作为锁的对象引用,一个作为这个锁保护的代码块。
synchronized修饰的方法,就是横跨整个方法体的同步代码块,其同步代码块的锁就是此方法所在的对象。⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
静态的synchronized方法以Class对象作为锁。⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
每个java对象都可以用作一个实现同步的锁,被称为内置锁(Intrinsic lock)或监视锁(Monitor Lock)。
//分解质因数的servlet,并发性非常糟糕
@ThreadSafe
public class SynchronizedFactorier implements Servlet{
@GardedBy("this") private BigInteger lastNumber;
@GardedBy("this") private BigInteger[] lastFactories;
public synchronized void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
if(i.equals(lastNumber)){
encodeIntoResponse(resp, lastFactories);
}else {
BigInteger[] factors= factor(i);
lastNumber = i;
lastFactors = factors;
encodeIntoResponse(resp, factories);
}
}
}
这个程序的缺点是只能有一个线程执行service方法,效率很低,但是确实是线程安全的。
2.重入
一个线程请求他自己持有的锁,就会成功。
获取锁的操作粒度是『线程』而非『调用』。
实现方法是,为每个锁关联一个获取计数值和所有者线程。
不可重入将导致子类无法调用父类中的同步(synchronized)方法,出现死锁。
3.用锁来保护状态
锁能够使其保护的代码以串行方式访问。
对象的内置锁与其状态之间没有内在的关联。
synchronized使用的是对象的内置锁,是可重入的。可重入的锁,有自动的计数。
每个对象都有一个内置锁。
一种常见的加锁约定是,将每个可变的状态都放在一个对象中,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得该对象上不会发生并发访问。
再声明一次,竞态条件,就是后面语句的执行,会依赖前面语句的结果,当然是在多线程的环境中。
将类中的每个方法都声明为synchronized会带来活跃性和性能问题。
4.活跃性(liveness)与性能(performance)
要确保同步代码块不要过小,并且不要将原子操作拆分到多个同步代码块中。应该尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去,从而在这些操作的执行过程中,其它线程可以访问共享状态。
当执行时间长的计算或者可能无法快速完成的操作时,一定不要持有锁。
线程中的局部变量是不需要同步操作的,因为与其它线程之间没有共享状态。