线程安全性:当多个线程访问某个类时,这个类始终表现出正确的行为,则这个类是线程安全的。
编写线程安全的代码,核心在于要对状态访问操作进行管理,特别是对共享的和可变的状态的访问。
当多个线程访问同一个可变状态变量时,如果没有合适的同步,则程序会产生错误的结果。有三种方式可以修改这个问题:
-
不在线程之间共享该变量;
-
将状态变量修改为不可变的变量;
-
在访问状态变量时使用同步。
*比较合适的开发原则是:先保证代码的正确性,再提高代码运行速度。最好只有当有明确的性能测试结果和应用需求要求必须提高性能,且测量结果表明该优化在实际环境中确实能带来性能的提升时,才进行性能优化。
无状态对象是线程安全的。如下例所示:
@ThreadSafe
public class StatelessFactorizer implements Servlet
{
public void service (ServletRequest req, ServletResponse resp)
{
BigInteger i = parseReq(req);
BigInteger[] factors = factor(i);
encodeIntoResponse(resp, factors);
}
}
竞态条件:并发编程中,由于不恰当的执行时序而出现不正确的结果。当某个计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件。
示例一:先检查后执行(Check-Then-Act):
@NotThreadSafe
public class UnsafeCountingFactorizer implements Servlet
{
private long count = 0;public long getCount()
{
return count;
}public void service (ServletRequest req, ServletResponse resp)
{
BigInteger i = parseReq(req);
BigInteger[] factors = factor(i);
++count;//Check-Then-Act
encodeIntoResponse(resp, factors);
}
}
大多数竞态条件的本质是,基于一种可能失效的观察结果来做出判断或者执行某个计算。
示例二:延迟初始化:
@NotThreadSafe
public class LazyInitRate
{
private LazyInitRate instance = null;public LazyInitRate getInstance()
{
if(null == instance)
{
instance = new LazyInitRate();
}
return instance;
}
}
示例二是一种非线程安全的单例模式的实现方式,其它两种实现方式,后面会专门介绍。
原子操作:对于访问同一个状态的所有操作,这个操作是以原子方式执行的操作。要么全部执行,要么全不执行。
示例一解决方案一:采用内置的线程安全类:
@ThreadSafe
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 = parseReq(req);
BigInteger[] factors = factor(i);
count.incrementAndGet();
encodeIntoResponse(resp, factors);
}
}
在实际情况中,应尽可能使用现有的线程安全对象(java.util.concurrent.atomic)来管理类的状态。
示例三:多个变量:
@NotThreadSafe
public class UnsafeCachingFactorizer implements Servlet
{
private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>();public long getCount()
{
return count;
}public void service (ServletRequest req, ServletResponse resp)
{
BigInteger i = parseReq(req);
if(i.equals(lastNumber.get()))
{
encodeIntoResponse(resp, lastFactors.get());
}
else
{
BigInteger[] factors = factor(i);
lastNumber.set(i);
lastFactors.set(factors);
encodeIntoResponse(resp, factors);
}
}
}
当不变性条件中涉及多个变量时,各个变量之间并不是彼此独立的,而是某个变量的值会对其他变量的值产生约束。因此,当更新某一个变量时,需要在同一个原子操作中对其他变量同时进行更新。
*要保持状态的一致性,就需要在单个的原子操作中更新所有相关的状态变量。
Synchronized:Java内置锁来支持原子性,一种互斥锁。
示例四:示例三的一种解决方案:
@ThreadSafe
public class SynchronizedFactorizer implements Servlet
{
private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>();public long getCount()
{
return count;
}public synchronized void service (ServletRequest req, ServletResponse resp)
{
BigInteger i = parseReq(req);
if(i.equals(lastNumber.get()))
{
encodeIntoResponse(resp, lastFactors.get());
}
else
{
BigInteger[] factors = factor(i);
lastNumber.set(i);
lastFactors.set(factors);
encodeIntoResponse(resp, factors);
}
}
}
重入:某个线程可以重复获得已经由它自己持续的锁。内置锁是可重入的。重入意味着获取锁的操作粒度是“线程”,而非“调用”。
示例五:如果内置锁不可重入,则如下示例会发生死锁:
public class Widget
{
public synchronized void doSomething()
{
...
}
}
public class LoggingWidget extends Widget
{
public synchronized void doSomething()
{
super.doSomething();
}
}
*用锁来保护状态:对于可能被多个线程访问的可变状态变量,在访问它时都需要持有同一个锁;每个共享的和可变的状态变量都应该只由一个锁来保护;对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。
虽然synchronized方法可以确保单个操作的原子性,但如果要把多个操作合并为一个复合操作,还需要额外的加锁机制。
一种常见的加锁约定是,将所有可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得在该对象上不发生并发访问。
活跃性和性能问题:示例四解决方案存在不良并发。
示例六:示例四的改进,缩小同步范围,提高性能,避免不良并发:
@ThreadSafe
public class CachedFactorizer implements Servlet
{
private final BigInteger lastNumber;
private final BigInteger[] lastFactors;
private long hits;
private long cacheHits;
public synchronized long getHits()
{
return hits;
}
public synchronized double getCacheHitRatio()
{
return (double)cacheHits/(double)hits;
}
public synchronized void service (ServletRequest req, ServletResponse resp)
{
BigInteger i = parseReq(req);
BigInteger[] factors = null;
synchronized(this)
{
++hits;
if(i.equals(lastNumber))
{
++cacheHits;
factor = lastFactors.clone();
}
}
if(null == factors)
{
factors = factor(i);
synchronized(this)
{
lastNumber = i;
lastFactors = factors.clone();
}
}
encodeIntoResponse(resp, factors);
}
}
*通常,在简单性与性能之间存在着相互制约的因素。当实现某个同步策略时,一定不要盲目的为了性能而牺牲简单性。
*当执行时间较长的计算或可能无法快速完成的操作时(如网络IO),一定不要持有锁。
网友评论