一, 什么是线程安全性

在线程安全性的定义中,最核心的概念就是正确性。

a class is thread-safe when it continues to behave correctly when accessed from multiple threads, regardless of the scheduling or interleaving of the execution of those threads by the runtime environment, and with no additional synchronization or other coordination on the part of the calling code.

 

无状态对象总是线程安全的. For example:

package com.ivy.thread;

import java.math.BigInteger;
@ThreadSafe
public class StatelessFactorizer implements Servlet{

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

和大部分servlet一样,StatelessServlet也是无状态的:它没有fileds,也没有引用其它类对象。计算出来的临时状态也只保存在本地变量中,本地变量存储在当前thread的栈中,也只能被正在运行的thread访问。如果thread A访问StatelessFactorizer,并不会影响其它thread的运行结果,因为它们两个并没有共享状态,就好像它们访问的是两个不同的实例。因为一个线程访问无状态对象不会影响其他线程访问该对象的正确性,所以说无状态对象总是线程安全的。

只有当servlets要记住request的状态时,线程安全性就变成一个需要考虑的问题。

二, 原子性

如果为StatelessFactorizer添加一个计数器,统计request的个数,那这个类就变成线程不安全的了,因为count++并不是一个原子操作:

package com.ivy.thread;

import java.math.BigInteger;

@NotThreadSafe
public class UnsafeStatelessFactorizer implements Servlet{

    private long count = 0;
    
    public long getCount() {
        return count;
    }
    
    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factory(i);
        count++;
        encodeIntoResponse(resp, factors);
    }
}

由于线程执行时机导致结果不正确的问题叫 race condition.

1,竞争条件

当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。换句话说,就是正确结果要取决于lucky timing。

最典型的竞争条件是check-then-act和read-modify-write。

你check了某件事是真,然后根据这个check采取措施act,但是当你act的时候,这个check也许已经失效了。使用check-and-act的一个经典场景是Lazy initialization.

package com.ivy.thread;

@NotThreadSafe
public class LazyInitRace {

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

延时初始化存在竞争条件,这样就会破坏正确性。假设ThreadA和ThreadB同时执行getInstance,A看到instance是null, B看到也是null, 就有可能产生两个ExpensiveObject.

另一种存在竞争条件的场景是read-modify-write操作,比如UnsafeStatelessFactorizer类。

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

像延迟初始化(先检查后执行)和“读取-修改-写入”的概念操作统称为符合操作:包含了一组必须以原子方式执行的操作以确保线程安全性。

例1修改方案  使用AtomicLong类型来替换Long类型, AtomicLong类型是已经存在的线程安全的类。

@ThreadSafe
public class UnsafeCountingFactorizer implements Servlet {
    private final AtomicLong count = new AtomicLong(0);

    public long getCount() { return count;}

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

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

我们解决了在servlet中添加一个field的情况,那如果要添加多个呢?假设现在我们要提升factorizerServlet的性能,想要缓存最近计算的结果,这样如果两个连续的request想要factor相同的数字,那么第二个request就可以很快获得结果。所以现在我们需要记住两个值:the last number factored and its factors. 

我们使用AtomicReference来管理lastNumber和lastFactors:

@NotThreadSafe
public class UnsafeCountingFactorizer 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);
        }
    }
}    

AtomicReference本身是线程安全的,但是UnsafeCountingFactorizer仍然存在竞争条件,还是会产生错误的结果,因为lastNumber.set(i);和lastFactors.set(factors);并不是原子的。lastFactors 的值依赖于lastNumber,所以更新了lastNumber后就必须保证lastFactors也被更新。使用AtomicReference并不能保证这两个值同时被更新

如果在这个servlet在添加多的状态,并不是为每个状态都使用线程安全的对象就可以了,要保证所有的操作都在一个原子中完成。

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

2,内置锁(intrinsic locks)

同步代码块(synchronized block) 

Synchronized Block包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。(a reference to an object that will serve as the lock, and a block of code to be guarded by that lock)

Synchronized Method就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是调用这个方法那个对象。静态的synchronized方法以Class对象作为锁。

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

Java的内置锁相当于一种互斥锁,这意味着最多只有一个线程能持有这种锁。

如下是对FactorizerServlet的修改方案2,使用synchronized来修饰方法,可以保证线程安全性,但对于这个servlet来说会造成性能变差,因为一次只能有一个线程执行service方法。

例2修改方案  使用synchronized修饰方法。

package com.ivy.thread;

import java.math.BigInteger;

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.clone();
            encodeIntoResponse(resp, factors);
        }
    }

}

 

重入(Reentrancy)

如果一个线程要请求一个锁,但是这个锁被另外一个线程持有,那么这个线程就会block。但是内置锁是可重入的,也就是说当一个线程第二次请求被这个线程持有的锁时,可以获得该锁。

重入的意思是获取锁的操作粒度是“线程”,而不是“调用”。也就是说如果某个线程已经获得锁,当相同线程再次获取这个锁的时候,这个请求一样会成功。重入的一种实现方法是为每个锁关联一个获取计数值和一个所有者线程。当计数器为0时,表示没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1,如果同一个线程再次获取这个锁,计数器递增,而当线程退出同步代码块时,计数器会相应地递减。当计数器为0时,这个锁被释放。

一个子类overrides了父类的synchronized方法doSomething(),并且调用了super.doSomething(). 如果没有Reentrancy, 就会造成死锁。

package com.ivy.thread;

public class LoggingWidget extends Widget{

    public synchronized void doSomething() {
        super.doSomething();
    }
}

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

《Java并发编程实战》的翻译是这样的:

在例子中,子类覆盖了父类的synchronized 类型的方法,并调用父类中的方法。如果没有可重入的锁,子类中可能就会产生死锁,因为Widget和LoggingWidget中的dosomething方法都是synchronized 类型的,都会在处理前试图获得Widget的锁。” 

我在这里的疑问是:子类调用super.doSomething()时锁对象到底是子类对象还是父类对象。

下面是网上找到的答案,参考http://www.debugease.com/j2se/1279242.html 和 http://bbs.csdn.net/topics/390416405

在子类中,调用父类的synchronized方法,锁住的是子类的对象,而不是父类的对象这应该是个比较基础的问题啊,super这个东西的含义是什么?不是指的父类,而是:一个用来引用继承而来的成员的引用。那么super.doSomething()的含义是,通过super引用调用从父类继承而来的doSomething()方法,那么明显锁的还是当前的子类对象

锁对象确实是LoggingWidget,也许这里原文是个笔误,也许是用父类引用泛指子类对象这样来描述的,《Java Concurrency in Practise》的这一处确实给不少人带来了疑惑。

为了检测到底锁住的对象是什么,我用了synchronized(this)来检测:

package com.ivy.thread;

public class LoggingWidget extends Widget{

    public void doSomething() {
        synchronized (this) {
            System.out.println(this);
            System.out.println("LoggingWidget.doSomething");
            super.doSomething();
        }
    }
    
    public static void main(String[] args) {
        LoggingWidget widget = new LoggingWidget();
        System.out.println(widget);
        widget.doSomething();
    }
}

class Widget{
    public void doSomething() {
        synchronized (this) {
            System.out.println(this);
            System.out.println("Widget.doSomething");
        }
    }
}

输出结果:

com.ivy.thread.LoggingWidget@1670cc6
com.ivy.thread.LoggingWidget@1670cc6
LoggingWidget.doSomething
com.ivy.thread.LoggingWidget@1670cc6
Widget.doSomething

所以LoggingWidget在执行自己的doSomething和super.doSomething时锁住的都是new LoggingWidget()这个对象。如果内置锁不可重入,那LoggingWidget对象在执行super.doSomething()的时候就需要重新申请LoggingWidget的锁,但是这个锁已经被自己持有了并且不会释放,这样就进入死锁状态。

 

用锁来保护状态

锁可以确保被保护的代码以串行方式访问,所以可以通过锁来构造协议实现对共享状态的独占访问,只要遵守这些协议,就能确保状态的一致性。

Holding a lock for the entire duration of a compound action can make that compound action atomic. 但是只把复合操作包装在synchronized块中并不够,如果对一个变量的访问需要使用同步,那所有访问该变量的地方都要加上同步。而且在使用锁来实现对变量的同步时,所有访问该变量的地方都要使用同一把锁。

每个可变状态的变量都有可能被多个线程访问,所有的访问都需要申请同一把锁,这样我们就说这把锁保护着这个变量的状态。

获取一个对象关联的锁并不能阻止其他线程访问该对象,只有所有线程都获取的是相同的锁才能确保该对象被串行访问。所以每个共享的可变变量要被同一把锁保护。并非所有数据都需要锁的保护,只有被多个线程同时访问的可变数据才需要通过锁来保护。一种常见的加锁约定是,将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码进行同步,使得在该对象上不会发生并发访问。

对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。可以在单个原子操作中访问或更新这些变量。

 

活跃度和性能(Liveness & Performance)

应该尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去,从而在这些操作的执行过程中,其他线程可以访问共享状态。

通常,在简单性与性能之间存在着相互制约因素。当实现某个同步策略时,一定不要盲目地为了性能而牺牲简单性。

当执行时间较长的计算或者可能无法快速完成的操作时,一定不要持有锁。

改进后的CachedFactorizer如下:

CachedFactorizer􀀃restructures􀀃the􀀃servlet􀀃to􀀃use􀀃two􀀃separate􀀃synchronized􀀃blocks,􀀃each􀀃limited􀀃to􀀃a􀀃
short􀀃section􀀃of􀀃code.􀀃One􀀃guards􀀃the􀀃check􀍲then􀍲act􀀃sequence􀀃that􀀃tests􀀃whether􀀃we􀀃can􀀃just􀀃return􀀃the􀀃cached􀀃result,􀀃
and􀀃the􀀃other􀀃guards􀀃updating􀀃both􀀃the􀀃cached􀀃number􀀃and􀀃the􀀃cached􀀃factors.􀀃As􀀃a􀀃bonus,􀀃we've􀀃reintroduced􀀃the􀀃hit􀀃
counter􀀃and􀀃added􀀃a􀀃"cache􀀃hit"􀀃counter􀀃as􀀃well,􀀃updating􀀃them􀀃within􀀃the􀀃initial􀀃synchronized􀀃block.􀀃Because􀀃these􀀃
counters􀀃 constitute􀀃 shared􀀃mutable􀀃 state􀀃 as􀀃 well,􀀃 we􀀃 must􀀃 use􀀃 synchronization􀀃 everywhere􀀃 they􀀃 are􀀃 accessed.􀀃 The􀀃
portions􀀃of􀀃code􀀃that􀀃are􀀃outside􀀃the􀀃synchronized􀀃blocks􀀃operate􀀃exclusively􀀃on􀀃local􀀃(stack􀍲based)􀀃variables,􀀃which􀀃
are􀀃not􀀃shared􀀃across􀀃threads􀀃and􀀃therefore􀀃do􀀃not􀀃require􀀃synchronization.􀀃

package com.ivy.thread;

import java.math.BigInteger;

import com.sun.org.apache.bcel.internal.generic.IF_ACMPEQ;

@ThreadSafe
public class CachedFactorizer 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 getCachedHits() {
        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);
    }
}

 

posted on 2017-04-27 18:21  coder为  阅读(502)  评论(0编辑  收藏  举报