美文网首页
【再读Java并发编程】线程安全性

【再读Java并发编程】线程安全性

作者: Rockie_h | 来源:发表于2018-03-06 23:34 被阅读56次

线程安全性:当多个线程访问某个类时,这个类始终表现出正确的行为,则这个类是线程安全的。
编写线程安全的代码,核心在于要对状态访问操作进行管理,特别是对共享的和可变的状态的访问。
当多个线程访问同一个可变状态变量时,如果没有合适的同步,则程序会产生错误的结果。有三种方式可以修改这个问题:

  1. 不在线程之间共享该变量;

  2. 将状态变量修改为不可变的变量;

  3. 在访问状态变量时使用同步。
    *比较合适的开发原则是:先保证代码的正确性,再提高代码运行速度。最好只有当有明确的性能测试结果和应用需求要求必须提高性能,且测量结果表明该优化在实际环境中确实能带来性能的提升时,才进行性能优化。
    无状态对象是线程安全的。如下例所示:
    @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),一定不要持有锁。

相关文章

网友评论

      本文标题:【再读Java并发编程】线程安全性

      本文链接:https://www.haomeiwen.com/subject/bzqdfftx.html