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

基础篇

第二章 线程安全性

在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
 */
@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 };
    }
}

原子性

原子性就是不可再分割,以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
 */
@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++`的例子,由于不切当的执行时序导致错误的执行结果。我们把这种情况称作**竞态条件**。
最常见的静态条件就是**先检查后执行**。
例如延迟加载:
package net.jcip.examples;

import net.jcip.annotations.*;

/**
 * LazyInitRace
 *
 * Race condition in lazy initialization
 *
 * @author Brian Goetz and Tim Peierls
 */

@NotThreadSafe
public 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
 */
@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; }
}

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
 */

@NotThreadSafe
public 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
 */

@ThreadSafe
public 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
 */
@ThreadSafe
public 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),一定不要持有锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值