java 并发编程实战 之 线程安全性

来源:互联网 发布:手机怎么给淘宝改评价 编辑:程序博客网 时间:2024/05/16 19:18

基础篇

第二章 线程安全性

在Java中同步的机制

volatile变量、显示的同步代码块(显示锁)、原子变量。

  • 编写线程安全的代码的关键:利用以上三个机制合理控制对象共享的且是可变的状态(即类的field)的读写操作。

什么是线程安全性

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

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

  • 自身没有任何域也不包含任何其他对象的引用。
  • 例: 无状态的servlet
package net.jcip.examples;import java.math.BigInteger;import javax.servlet.*;import net.jcip.annotations.*;/** * StatelessFactorizer * * A stateless servlet *  * @author Brian Goetz and Tim Peierls */@ThreadSafepublic 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 };    }}

原子性

原子性就是不可再分割,以count++为例,它是一个包含读取-修改-写入三个操作的复合操作,并且count++的结果的正确性依赖于这三个操作的执行顺序。倘若有AB两个线程同时执行该复合操作,有可能出现A读取-A修改-B读取-B修改-A写入-B写入的操作序列,这显然是错误的,除此之外还有很多错误的组合方式,我们唯一希望看到的序列如下:A读取-A修改-A写入B读取-B修改-B写入 或者 B读取-B修改-B写入A读取-A修改-A写入

package net.jcip.examples;import java.math.BigInteger;import javax.servlet.*;import net.jcip.annotations.*;/** * UnsafeCountingFactorizer * * Servlet that counts requests without the necessary synchronization * * @author Brian Goetz and Tim Peierls */@NotThreadSafepublic 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++`的例子,由于不切当的执行时序导致错误的执行结果。我们把这种情况称作**竞态条件**。最常见的静态条件就是**先检查后执行**。例如延迟加载:
package net.jcip.examples;import net.jcip.annotations.*;/** * LazyInitRace * * Race condition in lazy initialization * * @author Brian Goetz and Tim Peierls */@NotThreadSafepublic class LazyInitRace {    private ExpensiveObject instance = null;    public ExpensiveObject getInstance() {        if (instance == null)            instance = new ExpensiveObject();        return instance;    }}class ExpensiveObject { }

上面的代码中instance可能会被创建多次,而创建每一个实例的花销是特别大的,或者实例占用的资源是稀少的,比如数据库连接池、redis连接池。当然在某些需求中instance需要保持唯一,这时候如果在并发环境中创建了多个实例,并被传递给其他对象,则会引发严重错误。

复合操作

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

与数据库中事务的原子性有异曲同工之意。事物之间互不影响,要么全部执行成功,要么全部执行失败。

  • 使用AtomicLong来解决 count++的线程安全问题。
package net.jcip.examples;import java.math.BigInteger;import java.util.concurrent.atomic.*;import javax.servlet.*;import net.jcip.annotations.*;/** * CountingFactorizer * * Servlet that counts requests using AtomicLong * * @author Brian Goetz and Tim Peierls */@ThreadSafepublic 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; }}

AtomicLong 变量能够保证,在多个线程对该变量操作时,能够保证个线程之间的操作是原子性的。

在实际情况中,应尽量使用线程安全的对象(例如 AtomicLong)
来管理对象的状态。

加锁机制

在上一个例子中我们使用`AtomicLong`解决了线程安全问题,

这是因为我们的Servlet中只持有了一个状态。如果持有多个状态会怎样呢?我们希望将最后一次AtomicLong因式分解的计算结果缓存起来。这里用到的是AtomicReference (代替对象引用的线程安全类,后面会详细介绍各种原子变量。)在这里虽然能够保证对lastNumberlastFactors 上的操作是原子性的,但是它们两个之间存在着一对一的映射关系,当改变一个的时候,另一个也需要相应的做出改变,它们之间不是相互独立的,只保证它们两个相互独立的操作的原子性是不够的,而是需要保证 同时改变它们两个的一组操作 的原子性。

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

package net.jcip.examples;import java.math.BigInteger;import java.util.concurrent.atomic.*;import javax.servlet.*;import net.jcip.annotations.*;/** * UnsafeCachingFactorizer * * Servlet that attempts to cache its last result without adequate atomicity * * @author Brian Goetz and Tim Peierls */@NotThreadSafepublic class UnsafeCachingFactorizer extends GenericServlet 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);        }    }    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};    }}

内置锁

内置锁机制:同步代码块 Synchronized Block,包括两部分,一个作为锁的对象引用,一个作为由这个锁保护的代码块。 - 同步代码块的锁     - 加在类的成员方法上时锁就是方法调用所在的对象,即`this`指向的对象,如下例中的`lastNumber`、`lastFactors`被`this`守护,即访问这两个状态的线程必须首先获取 以当前实例对象作为的锁     - 加在类的静态方法上的锁就是该类的Class对象。     - `Synchronized (lock) {}` 显示的制定锁,lock可以使任何对象。         > 每个java对象都可以作为一个实现同步的锁,这些锁被称为内置锁,或监视锁。
package net.jcip.examples;import java.math.BigInteger;import javax.servlet.*;import net.jcip.annotations.*;/** * SynchronizedFactorizer * * Servlet that caches last result, but with unnacceptably poor concurrency * * @author Brian Goetz and Tim Peierls */@ThreadSafepublic 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(){        ...        super.doSomething();    }}

用锁来保护状态

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

  • 见一个反例,下例中的count++虽然被synchronized修饰,但它并不是线程安全的,欢迎大家留言来解释这个问题,提示:原因见上面用锁来保护状态 的解析。
public class Problem1 {    public static int count;    public static  class TestThread implements Runnable{        @Override        public  void run() {            doCount();        }        private synchronized void doCount(){            count++;        }    }    public static void main(String[] args) throws InterruptedException {        Thread t1 = new Thread(new TestThread(),"T1");        Thread t2 = new Thread(new TestThread(),"T2");        t1.start();        t2.start();    }}

每个共享的可变状态变量都应该只由一个锁来保护。

再举一个反例,下面的代码虽然使用了vector,但并不是线程安全的,为什么呢,因为在vector内部的synchronized方法 保证了,vector自己的域由内置锁也就是vector的这个实例守护,但是element 这个可变的状态变量却不是由vector的内置锁守护,所以其他线程是可以改变element变量的,从而形成竞态条件。

if(!vector.contains(element))    vector.add(element)

vector 的contains、add方法如下

    public synchronized boolean add(E e) {        modCount++;        ensureCapacityHelper(elementCount + 1);        elementData[elementCount++] = e;        return true;    }    public boolean contains(Object o) {        return indexOf(o, 0) >= 0;    }    public synchronized int indexOf(Object o, int index) {        if (o == null) {            for (int i = index ; i < elementCount ; i++)                if (elementData[i]==null)                    return i;        } else {            for (int i = index ; i < elementCount ; i++)                if (o.equals(elementData[i]))                    return i;        }        return -1;    }

活跃性与性能

过分使用或滥用Synchronized代码块,会导致严重的性能问题。上面的例子中的 SynchronizedFactorizer 他继承了Servlet,我们都知道Servlet是单例的,他需要为所有的请求提供服务,并且他的service方法被synchronized修饰,这导致了所有的线程必须一个的按顺序执行,就像一大堆人坐缆车下山,只提供了一辆容纳一人的缆车,尤其是当下山的路线特别长的时候,下山的效率可想而知。为了解决不良并发问题,提出了缓存机制,代码如下。

package net.jcip.examples;import java.math.BigInteger;import javax.servlet.*;import net.jcip.annotations.*;/** * CachedFactorizer * <p/> * Servlet that caches its last request and result * * @author Brian Goetz and Tim Peierls */@ThreadSafepublic 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};    }}

当执行较长时间的计算或者无法快速完成的工作时(例,网络IO、或控制台IO),一定不要持有锁。

0 0
原创粉丝点击