非线程安全演变成线程安全---原子性与加锁机制

无状态对象一定是线程安全的。即无共享变量。

//计算过程中的临时状态仅存在于线程栈上的局部变量中,并且只能由正在执行的线程访问,多个线程间没有共享状态。
public class StatelessFactorizer implements Servlet {
    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        encodeIntoResponse(resp, factors);
    }
}

提高需求:统计已处理请求数量。这样就会产生共享状态,需要考虑线程安全。

方法一:普通++i。非线程安全

//下面的++cnt是非原子操作,且没有进行同步,是非线程安全的
public class UnsafeCountingFactorizer implements Servlet {
    private long cnt = 0;
    public long getCnt() {
        return cnt;
    }
    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        ++cnt;
        encodeIntoResponse(resp, factors);
    }
}

方法二:利用线程安全类AtomicLong来管理类的状态。将复合操作++i转变为原子操作,保证线程安全性。

//这个类的状态就是cnt的状态,而cnt是线程安全的,所以这个类 也是线程安全的
public class UnsafeCountingFactorizer implements Servlet {
    private long cnt = 0;
    public long getCnt() {
        return cnt;
    }
    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        ++cnt;
        encodeIntoResponse(resp, factors);
    }
}

提高需求:将最近的计算结果缓存起来,当两个连续的请求对相同的数值进行因数分解时,可以直接使用上一次的计算结果,而无须重新计算 。这样,需要保存两个状态:最近执行因数分解的数值和分解结果。

方法一:使用原子类。由于这里有两个变量需要缓存,而一个原子类只能保证自身的线程安全性,不能保证两个变量同时更新,所以可能产生不变性条件(两者同时更新,两者存在对应关系)被破坏了。因此是非线程安全的。

public class UnsafeCachingFactorizer implements Servlet {
    private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();
    private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>();
    
    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        if(i.equals(lastNumber.get());
            encodeIntoResponse(resp, lastFactors.get());
        else {
            BigInteger[] factors = factor(i);
            //存入缓存,单个set对自身具有线程安全性
            lastNumber.set(i);
            lastFactors.set(factors);
            encodeIntoResponse(resp, factors);
        }
    }
}

方法二:使用synchronized关键字。由于产生的是互斥锁(内置锁),所以多个线程无法同时使用这个因数分解servlet,因此会导致性能问题,而不是线程安全问题。

public class SynchronizedFatorizer implements Servlet {
    private BigInteger lastNumber;
    private BigInteger[] lastFactors;
    
    public synchronized void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        if(i.equals(lastNumber)) 
            encodeIntoResponse(resp, lastFactors);
        else {
            BigInteger[] factors = factor(i);
            lastNumber = i;
            lastFactors = factors;
            encodeIntoResponse(resp, factors);
        }
    }
}

提高需求:增加命中率的计算,即缓存命中/总命中的数值。将大的同步代码块分解成小的同步代码块,既提高性能,也保证线程安全。但是同步代码块不要过小,并且不要将本应是原子的操作拆分到多个同步代码块中,应尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去。

方法一:将大的同步代码块分解成小的同步代码块。是线程安全的。

public class CachedFactorizer implements Servlet {
    private BigInteger lastNumber;
    private BigInteger[] lastFacots;
    //命中数
    private long hits;
    //缓存命中数
    private long cacheHits;
    
    public synchronized long getHits() {
        return hits;
    }
    public synchronized double getCacheHitRatio() {
        return (double) cacheHits / (double) hits;
    }

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = null;
        //检查缓存是否命中,即上一次是否已经计算过该因数分解
        synchronized(this) {
            ++hits;
            //如果上一次已经计算过因数分解,则直接返回
            if(i.equals(lastNumber)) {
                ++cacheHits;
                factors = lastFactors.clone();
            }
        }
        //如果上一次没有计算过因数分解,则计算,并存入缓存
        if(factors == null) {
            factors = factor(i);
            synchronized(this) {
                lastNumber = i;
                lastFactors = factors.clone();
            }
        }
        encodeIntoResponse(resp, factors);
    }
}

猜你喜欢

转载自www.cnblogs.com/cing/p/9049820.html
今日推荐