2.3 锁

阅读更多
当我们需要给类增加单个状态变量时,可以使用线程安全对象来保证线程安全。但是当变量超过一个时,可能会出问题。
下面这个例子说明了当我们需要在servlet中使用缓存提高性能时可能导致的问题。我们使用lastNumber和lastFactors两个AtomicReference对象来保证线程安全。
@NotThreadSafe
public class UnsafeCachingFactorizer 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);
         }
     }
}

虽然这两个变量自身都是线程安全的,但是对两个对象的操作依然会导致竞争条件的问题。例如:当A线程执行了lastNumber.set(i)且还没有执行lastFactors.set(factors)时,B线程执行了encodeIntoResponse(resp,  lastFactors.get(),显然这会导致bug。
从这个例子我们可以看到单个对象的线程安全并不能保证整体的线程安全。 为了保证整体状态一致性,对状态变量的改变必须在单个原子性操作中完成。

2.3.1 固有锁
Java提供了内嵌的锁机制来强制原子性:synchronized 操作。synchronized 修饰的代码段有两个部分组成:一个对对象的引用作为锁,以及被这个锁保护的代码段。
synchronized (lock) {
    // Access or modify shared state guarded by lock
}

同步方法是同步代码段的另外一种形式,锁对象是当前方法对象(如果是静态方法,则是类对象)
Java的固有锁是一种互斥锁,它使得只有一个线程能够进入同步代码段。当A线程进入代码段后,B线程必须等待A释放锁。如果A线程不释放,B将永远等待。有了这种机制,同步代码段里的代码能够保证操作原子性。
下面的例子中,我们给方法加上了synchronized修饰,这样就解决了线程安全性的问题。但是这样并不是一种好的方式,它带来了性能问题。其他线程必须等待某个线程释放锁才能进入方法,这种低效的操作是不太好的,可以改进。
@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 可重入
当一个线程持有某个对象的锁时,对于同步代码段是可以重入的。这种机制对于编写OO程序非常方便。下面这个例子中,子类和父类都有doSomething方法并且设为同步。如果固有锁没有可重入特性,依次调用子类/父类中的同名方法可能会导致线程永远等待(类似死锁)。可重入特性就可以避免这种灾难发生。
public class Widget {
    public synchronized void doSomething() {
        ...
    }
}

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

你可能感兴趣的:(java,threadsafe,多线程)