Java并发编程实战

admin 2018-07-14 阅读


一 线程安全性

1.1 什么是线程安全性

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

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

示例:一个无状态的Servlet

/**
 * @author Brian Goetz and Tim Peierls
 */
@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相同,这个类是无状态的:它既不包含任何域,也不包含任何对其他类中域的引用。计算过程中的临时状态仅存于线程栈上的局部变量中,并且只能由正在执行的进程访问。由于线程访问无状态对象的行为并不会影响其他线程中操作的正确性,因此无状态对象是线程安全的。

1.2 原子性

一个线程不安全的示例

@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 };
    }
}

count域(实例变量)可以在多个线程中共享,++count又不是一个原子操作,它包含了读取-修改-写入等操作。当两个线程在没有同步的情况下同时对count进行操作将会导致不正确的结果,这两个线程很有可能读到相同的值并且同时都执行了递增操作。

这种由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,它有一个正式的名字:竞态条件。

要避免竞态条件问题,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或之后读取和修改状态,而不是在修改状态的过程中。每个线程执行的都是原子操作,要么这个线程完全执行完,要么完全不执行。

下面我们修改之前的UnsafeCountingFactorizer类,使它成为一个线程安全的类:

@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; }
}

java.util.concurrent.atomic包中有一些原子变量类,用于实现在数值和对象引用上的原子状态转换。通过使用AtomicLong来替代long类型的计数器,能够确保所有对计数器状态的访问操作都是原子的。

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

1.2 加锁机制

当在Servlet中添加一个状态变量时,可以通过线程安全的对象来管理。如果想添加更多的状态,那么是否只需添加更多的线程安全状态变量就足够了?

答案是否定的,要想保持状态一致性,就需要在单个原子操作中更新所有相关的状态变量。

Java提供了一种内置的锁机制来支持原子性:同步代码块。它包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。以关键字synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。静态的synchronized方法以Class对象作为锁。

synchronized (lock) {
    // 访问或修改由锁保护的共享状态
}

每个Java对象都可以作为一个用来实现同步的锁,这些锁被称为内置锁或监视器锁。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,无论是正常退出还是通过从代码块中抛出异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。

Java的内置锁相当于一种互斥锁,这意味着最多只能有一个线程持有这种锁。如果某个线程持有这种锁由于某种原因没有释放锁,那么别的线程将永远等待下去。虽然这个锁保护的同步代码块会以原子方式执行,保证了线程安全,然而这种方法过于极端,因为多个客户端无法同时使用这个Servlet,服务的响应性非常低,无法令人接受。

幸运的是,通过缩小同步代码块的范围,我们很容易做到既确保Servlet的并发行,同时又维护线程的安全性。要确保同步代码块不要过小,并且不要将本应是原子的操作拆分到多个同步代码块。应该尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去,从而在这些操作的执行过程中,其他线程可以访问共享状态。

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

二 对象的共享

2.1 非原子的64位操作

当线程在没有同步的情况下读取变量时,可能会得到一个实效值,但至少这个值是由之前某个线程设置的值,而不是一个随机值。这种安全保证被称为最低安全性。

最低安全性适用于绝大多数变量,但是存在一个例外:非volatile类型的64位数值变量(doublelong)。JVM允许将64位的读写操作分解为两个32位的操作。如果对某个非volatile类型的long变量的读和写操作在不同的线程中执行,那么很可能读到某个值的高32位和另一个值的低32位。所以在多线程程序中使用共享且可变的64位数值变量是不安全的,最低安全性也无法保证。除非我们使用关键字volatile来声明它们,或者使用锁保护起来。

2.2 加锁与可见性

加锁的含义不仅仅局限与互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读写操作的线程都必须在同一个锁上同步。

2.3 Volatile变量

Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总是会返回最新写入的值。

仅当volatile变量能简化代码的实现以及对同步策略的验证时才应该使用它们。如果在验证正确性时需要对可见性进行复杂的判断,那么就不要使用volatile变量。volatile变量的正确使用方式包括:

  • 确保它们自身的可见性。
  • 确保它们所引用对象的状态的可见性
  • 标识一些重要的程序生命周期事件的发生(例如,初始化或者关闭)

加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。

声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。