Java并发编程实战 线程安全性总结

非线程安全的数值序列生成器

@NotThreadSafe
public class UnsafeSequence {
    private int value;

    /**
     * Returns a unique value.
     */
    public int getNext() {
        return value++;
    }
}

线程安全的数值序列生成器

@ThreadSafe
public class Sequence {
    @GuardedBy("this") private int nextValue;

    public synchronized int getNext() {
        return nextValue++;
    }
}

框架通过在框架线程中调用应用程序代码将并发性引入到程序中 在代码中将不可避免地访问应用程序状态 因此所有访问这些状态的代码路径都必须是线程安全的

线程安全性
如果当多个线程访问同一个可变的状态变量时没有使用合适的同步 那么程序就会出现错误 有三种方式可以修复这个问题

  • 不在线程之间共享该状态变量
  • 将状态变量修改为不可变的变量
  • 在访问状态变量时使用同步

当设计线程安全的类时 良好的面向对象技术 不可修改性 以及明晰的不变性规范都能起到一定的帮助作用

什么是线程安全性
当多个线程访问某个类时 不管运行时环境采用何种调度方式或者这些线程将如何交替执行 并且在主调代码中不需要任何额外的同步或协同 这个类都能表现出正确的行为 那么就称这个类是线程安全的

在线程安全类中封装了必要的同步机制 因此客户端无须进一步采取同步措施

一个无状态的Servlet

@ThreadSafe
public class StatelessFactorizer extends GenericServlet implements Servlet {

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        encodeIntoResponse(resp, factors);
    }

    void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
    }

    BigInteger extractFromRequest(ServletRequest req) {
        return new BigInteger("7");
    }

    BigInteger[] factor(BigInteger i) {
        // Doesn't really factor
        return new BigInteger[] { i };
    }
}

无状态对象一定是线程安全的

原子性
在没有同步的情况下统计已处理请求数量的Servlet(不要这么做)

@NotThreadSafe
public class UnsafeCountingFactorizer extends GenericServlet implements Servlet {
    private long count = 0;

    public long getCount() {
        return count;
    }

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        ++count;
        encodeIntoResponse(resp, factors);
    }

    void encodeIntoResponse(ServletResponse res, BigInteger[] factors) {
    }

    BigInteger extractFromRequest(ServletRequest req) {
        return new BigInteger("7");
    }

    BigInteger[] factor(BigInteger i) {
        // Doesn't really factor
        return new BigInteger[] { i };
    }
}

竞态条件
首先观察到某个条件为真(例如文件X不存在) 然后根据这个观察结果采用相应的动作(创建文件X) 但事实上 在你观察到这个结果以及开始创建文件之间 观察结果可能变得无效(另一个线程在这期间创建了文件X) 从而导致各种问题(未预期的异常 数据被覆盖 文件被破坏等)

延迟初始化中的竞态条件(不要这么做)

@NotThreadSafe
public class LazyInitRace {
    private ExpensiveObject instance = null;

    public ExpensiveObject getInstance() {
        if (instance == null)
            instance = new ExpensiveObject();
        return instance;
    }
}

class ExpensiveObject { }

复合操作
假定有两个操作A和B 如果从执行A的线程来看 当另一个线程执行B时 要么将B全部执行完 要么完全不执行B 那么A和B对彼此来说是原子的 原子操作是指 对于访问同一个状态的所有操作(包括该操作本身)来说 这个操作是一个以原子方式执行的操作

我们将 先检查后执行 以及 读取 修改 写入 等操作统称为复合操作 包含了一组必须以原子方式执行的操作以确保线程安全性

使用AtomicLong类型的变量来统计已处理请求的数量

@ThreadSafe
public class CountingFactorizer extends GenericServlet 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);
    }

    void encodeIntoResponse(ServletResponse res, BigInteger[] factors) {}
    BigInteger extractFromRequest(ServletRequest req) {return null; }
    BigInteger[] factor(BigInteger i) { return null; }
}

在实际情况中 应尽可能地使用现有的线程安全对象(例如AcomicLong)来管理类的状态 与非线程安全的对象相比 判断线程安全对象的可能状态及其状态转换情况要更为容易 从而也更容易维护和验证线程安全性

加锁机制
该Servlet在没有足够原子性保证的情况下对其最近计算结果进行缓存(不要这么做)

@NotThreadSafe
public class UnsafeCachingFactorizer extends GenericServlet implements Servlet {
    private final AtomicReference lastNumber
            = new AtomicReference();
    private final AtomicReference lastFactors
            = new AtomicReference();

    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);
            lastNumber.set(i);
            lastFactors.set(factors);
            encodeIntoResponse(resp, factors);
        }
    }

    void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
    }

    BigInteger extractFromRequest(ServletRequest req) {
        return new BigInteger("7");
    }

    BigInteger[] factor(BigInteger i) {
        // Doesn't really factor
        return new BigInteger[]{i};
    }
}

要保持状态的一致性 就需要在单个原子操作中更新所有相关的状态变量

内置锁
Java提供了一种内置的锁机制来支持原子性 同步代码块(Synchronized Block)
Java的内置锁相当于一种互斥体(或互斥锁) 这意味着最多只有一个线程能持有这种锁 当线程A尝试获取一个由线程B持有的锁时 线程A必须等待或者阻塞 直到线程B释放这个锁 如果B永远不释放锁 那么A也将永远地等下去

这个Servlet能正确地缓存最新的计算结果 但并发性却非常糟糕(不要这么做)

@ThreadSafe
public class SynchronizedFactorizer extends GenericServlet implements Servlet {
    @GuardedBy("this") private BigInteger lastNumber;
    @GuardedBy("this") 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);
        }
    }

    void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
    }

    BigInteger extractFromRequest(ServletRequest req) {
        return new BigInteger("7");
    }

    BigInteger[] factor(BigInteger i) {
        // Doesn't really factor
        return new BigInteger[] { i };
    }
}

重入
当某个线程请求一个由其他线程持有的锁时 发出请求的线程就会阻塞 然而 由于内置锁是可重入的 因此如果某个线程试图获得一个已经由它自己持有的锁 那么这个请求就会成功

如果内置锁不是可重入的 那么这段代码将发生死锁

public class Widget {
	public synchronized void doSomething() {
		...
	}
}

public class LoggingWidget extends Widget {
	public synchronized void doSomething() {
		System.out.println(toString() + ": calling doSomething");
		super.doSomething();
	}
}

用锁来保护状态
对于可能被多个线程同时访问的可变状态变量 在访问它时都需要持有同一个锁 在这种情况下 我们称状态变量是由这个锁保护的

每个共享的和可变的变量都应该只有一个锁来保护 从而使维护人员知道是哪一个锁

对于每个包含多个变量的不变性条件 其中涉及的所有变量都需要由同一个锁来保护

活跃性与性能
缓存最近执行因数分解的数值及其计算结果的Servlet

@ThreadSafe
public class CachedFactorizer extends GenericServlet implements Servlet {
    @GuardedBy("this") private BigInteger lastNumber;
    @GuardedBy("this") private BigInteger[] lastFactors;
    @GuardedBy("this") private long hits;
    @GuardedBy("this") 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);
    }

    void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
    }

    BigInteger extractFromRequest(ServletRequest req) {
        return new BigInteger("7");
    }

    BigInteger[] factor(BigInteger i) {
        // Doesn't really factor
        return new BigInteger[]{i};
    }
}

通常 在简单性与性能之间存在着相互制约因素 当实现某个同步策略时 一定不要盲目地为了性能而牺牲简单性(这可能会破坏安全性)

当执行时间较长的计算或者可能无法快速完成的操作时(例如 网络I/O或控制台I/O) 一定不要持有锁

你可能感兴趣的:(Java并发)