2.3. Locking(锁机制)

2.3. Locking(锁机制)
We were able to add one state variable to our servlet while maintaining thread safety by using a thread-safe object to manage the entire state of the servlet. But if we want to add more state to our servlet, can we just add more thread-safe state variables?
我们已经给我们的servlet加入了一个由线程安全对象管理的状态变量,而且我们依然我保证了线程安全性。但是如果我们还想向我们的servlet中加入一个状态变量,是不是我们只需要加入一个线程安全的状态变量就足够了?
Imagine that we want to improve the performance of our servlet by caching the most recently computed result, just in case two consecutive clients request factorization of the same number. (This is unlikely to be an effective caching strategy; we offer a better one in Section 5.6.) To implement this strategy, we need to remember two things: the last number factored, and its factors.
想象一下,我们现在需要通过缓存最近经常计算的结果来改善servlet的性能,用于应对两个连续的客户端需要对同一个数值进行因式分解(这通常不是一个高效率的缓存策略,我们将会在第5.6节中得到一个更好的策略)。为了实现这样的策略,我们需要记录两件事情:最近的因子和他的因数。
We used AtomicLong to manage the counter state in a thread-safe manner; could we perhaps use its cousin, AtomicReference, [6] to manage the last number and its factors? An attempt at this is shown in UnsafeCachingFactorizer in Listing 2.5.
我们已经使用了一个AtomicLong类来以线程安全的模式管理计数器,那么我们是否可以使用AtomicReference其姐妹类来管理最新的输入值和其因子呢?Listing2.5中展示了这种尝试。
[6] Just as AtomicLong is a thread-safe holder class for a long integer, AtomicReference is a threadsafe holder class for an object reference. Atomic variables and their benefits are covered in Chapter 15.
与AtomicLong类一样,AtomicReference是对一个对象应用的线程安全的保存类。原子化变量的优点将会在第十五章讨论。
Listing 2.5. Servlet that Attempts to Cache its Last Result without Adequate Atomicity. Don't Do this.

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

Unfortunately, this approach does not work. Even though the atomic references are individually thread-safe, UnsafeCachingFactorizer has race conditions that could make it produce the wrong answer.
不幸的是,这种方式并不起作用。尽管这两种原子引用机制分别都是线程安全的,UnsafeCachingFactorizer内部还是存在条件竞争,这使得它会出现错误应答。
The definition of thread safety requires that invariants be preserved regardless of timing or interleaving of operations in multiple threads. One invariant of UnsafeCachingFactorizer is that the product of the factors cached in lastFactors equal the value cached in lastNumber; our servlet is correct only if this invariant always holds. When multiple variables participate in an invariant, they are not independent: the value of one constrains the allowed value(s) of the others. Thus when updating one, you must update the others in the same atomic operation.
线程安全的定义是在多线程环境中,即使在不考虑时机和执行时序的情况下变量也可以得到保护。UnsafeCachingFactorizer中不变的是在lastFactors中缓存的因子等于在lastNumber中缓存的值。我们的servlet只有在这种不变被保证的情况下才能保证正确性。当多个变量参与到这种不变性的时候,他们就不再是独立的了。其中一个约束值依赖于另外一个。这样就必须在更改其中一个的时候,更改另外一个。
With some unlucky timing, UnsafeCachingFactorizer can violate this invariant. Using atomic references, we cannot update both lastNumber and lastFactors simultaneously, even though each call to set is atomic; there is still a window of vulnerability when one has been modified and the other has not, and during that time other threads could see that the invariant does not hold. Similarly, the two values cannot be fetched simultaneously: between the time when thread A fetches the two values, thread B could have changed them, and again A may observe that the invariant does not hold.
To preserve state consistency, update related state variables in a single atomic operation.

当有“霉运陷阱”存在的时候,UnsafeCachingFactorizer有可能破坏这种不变性。使用多个原子引用,即使每次调用都是原子的,我们还是无法同时修改lastNumber和lastFactors。如果其中一个被修改而另一个没有被修改,这时候其他线程就会看到这种不变性没有被保持住。同样,如果两个值也没有办法被同时读取到,当在线程A获取两个值的中间时间段,线程B可能会修改它们,这样A会再次观察到不变性没有保持住。
为了维护状态的一致性,必须要在一个单独的原子操作中同时修改相关的状态变量。
2.3.1. Intrinsic Locks(固有锁机制)
Java provides a built-in locking mechanism for enforcing atomicity: the synchronized block. (There is also another critical aspect to locking and other synchronization mechanisms visibility which is covered in Chapter 3.) A synchronized block has two parts: a reference to an object that will serve as the lock, and a block of code to be guarded by that lock. A synchronized method is a shorthand for a synchronized block that spans an entire method body, and whose lock is the object on which the method is being invoked. (Static synchronized methods use the Class object for the lock.)
synchronized (lock) {
    // Access or modify shared state guarded by lock
}
为了实现强制的原子性,java提供了一种内在的锁机制:同步块(在第三章中,我们还将会看到锁机制的另外重要方面以及其他的同步机制)。一个同步块包含两部分:一个对对象的应用作为锁,还有一个被锁所保护的块。同步方法是同步块实现的快捷方式,同步方法将同步块扩展到整个方法体,同步方法所使用的锁是方法所在的对象。(静态的同步方法使用所在的Class对象作为锁)
Every Java object can implicitly act as a lock for purposes of synchronization; these built-in locks are called intrinsic locks or monitor locks. The lock is automatically acquired by the executing thread before entering a synchronized block and automatically released when control exits the synchronized block, whether by the normal control path or by throwing an exception out of the block. The only way to acquire an intrinsic lock is to enter a synchronized block or method guarded by that lock.
为了同步,每个Java对象都可以隐式的作为锁使用。这种内在的锁被称之为内在锁或者镜像锁。这种锁会在运行的线程在进入同步块之前自动的去获取,并且无论是通过正规的控制路径还是通过抛出异常,都会在离开同步块之前自动释放锁。获得某个显式锁的唯一方式是进入被这个显示锁所保护的同步块或同步方法。
Intrinsic locks in Java act as mutexes (or mutual exclusion locks), which means that at most one thread may own the lock. When thread A attempts to acquire a lock held by thread B, A must wait, or block, until B releases it. If B never releases the lock, A waits forever.
Java中的内在锁是互斥的,这意味着最多只有一个线程会拥有该锁。当线程A试图获取被线程B拥有的锁时,A必须要等待,知道B释放该锁。如果B永远都不去释放锁的时候,A只能一直等一下。
Since only one thread at a time can execute a block of code guarded by a given lock, the synchronized blocks guarded by the same lock execute atomically with respect to one another. In the context of concurrency, atomicity means the same thing as it does in transactional applications that a group of statements appear to execute as a single, indivisible unit. No thread executing a synchronized block can observe another thread to be in the middle of a synchronized block guarded by the same lock.
由于一次只能有一个线程运行被同步锁所保护的代码块,在线程互相看来,被同一把锁所保护的代码是原子性的。在同步语境中,原子性意味着在一个事物中的事件,其一组状态作为一个单独的不可分割的单元执行。没有一个执行同步代码块的线程可以看到别的线程在被同一把锁所保护同步代码块中。
The machinery of synchronization makes it easy to restore thread safety to the factoring servlet. Listing 2.6 makes the service method synchronized, so only one thread may enter service at a time. SynchronizedFactorizer is now thread-safe; however, this approach is fairly extreme, since it inhibits multiple clients from using the factoring servlet simultaneously at all resulting in unacceptably poor responsiveness. This problem which is a performance problem, not a thread safety problem is addressed in Section 2.5.
同步机制的引入使得让我们的servlet很容易重新变成线程安全的状态。Listing2.6中,把service方法变成同步方法,这样一次只能有一个线程运行该方法,这样SynchronizedFactorizer就是线程安全的。然而,这种方法是很极端的,由于这实际上是组织了多个客户端同时使用因数分解servlet,这将导致很差的响应性。这是一个性能问题而不是一个线程安全的问题,这个问题将会在2.5节讨论。
Listing 2.6. Servlet that Caches Last Result, But with Unnacceptably Poor Concurrency. Don't Do this.

@ThreadSafe
public class SynchronizedFactorizer 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);
        }
    }
}

2.3.2. Reentrancy(可重入性)
When a thread requests a lock that is already held by another thread, the requesting thread blocks. But because intrinsic locks are reentrant, if a thread tries to acquire a lock that it already holds, the request succeeds. Reentrancy means that locks are acquired on a per-thread rather than per-invocation basis. [7] Reentrancy is implemented by associating with each lock an acquisition count and an owning thread. When the count is zero, the lock is considered unheld. When a thread acquires a previously unheld lock, the JVM records the owner and sets the acquisition count to one. If that same thread acquires the lock again, the count is incremented, and when the owning thread exits the synchronized block, the count is decremented. When the count reaches zero, the lock is released.
当一个线程请求一把被另外一个线程持有的锁时,请求线程会被阻塞。但是由于内在锁是可重入的,如果一个线程想要再次请求一把已经拥有的锁时,这种请求将会成功。可重入性意味着锁是被整个线程所得到的,而不是被某一次调用所得到。可重入性通过把一个锁被请求的次数和它所拥有的线程关联来实现的。当数值为0的时候,锁会被认为没有被持有,当一个线程请求请求一把没有被持有的锁时,JVM会记录拥有锁的线程,并且把请求数量加1,当相同的线程再次请求该锁时,数量会递增,当拥有锁的线程退出的时候,数量会被递减。当数量为0的时候,锁就是被释放的状态了。
[7] This differs from the default locking behavior for pthreads (POSIX threads) mutexes, which are granted on a per-invocation basis.
这不同于pthreads (POSIX threads) mutexes的默认锁行为,pthreads的默认锁是以调用为单位被授予的。
Reentrancy facilitates encapsulation of locking behavior, and thus simplifies the development of object-oriented concurrent code. Without reentrant locks, the very natural-looking code in Listing 2.7, in which a subclass overrides a synchronized method and then calls the superclass method, would deadlock. Because the doSomething methods in Widget and LoggingWidget are both synchronized, each tries to acquire the lock on the Widget before proceeding. But if intrinsic locks were not reentrant, the call to super.doSomething would never be able to acquire the lock because it would be considered already held, and the thread would permanently stall waiting for a lock it can never acquire. Reentrancy saves us from deadlock in situations like this.
可重用机制封装了锁的行为,这样就简化了面向对象的并发代码。如果没有可重用锁,Listing2.7的代码虽然看上去很自然,但是有可能会发生死锁。因为Widget中的doSomething()方法和LoggingWidget的doSomething()方法都是同步的,每一个都会在处理之前试图请求Widget中的锁。但是如果内在锁不是可重入的,线程就会在一直等待一把它永远都得不到的锁。可重入性把我们从这种情形中拯救了出来。
Listing 2.7. Code that would Deadlock if Intrinsic Locks were Not Reentrant.
public class Widget {
    public synchronized void doSomething() {
        ...
    }
}

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



你可能感兴趣的:(jvm,多线程,thread,servlet,performance)