Java并发编程(一)_线程安全性

       从非正式的意义来说,对象的状态是指存储在状态变量(例如实例或静态域)中的数据,还可包括其他依赖对象的域。对于共享的,可变的状态的访问,就必须正确的使用线程和锁来进行管理。
共享就意味着变量可以由多个线程同时访问,而可变意味着变量的值在其生命周期中可以发生变化。

       如果当多个线程访问同一个可变的的状态变量时没有使用合适的同步,那么程序就会出现错误。有三种方式可以修复这个问题:
       1. 不在线程之间共享该状态变量
       2 将状态变量修改为不可变的变量
       3. 在访问变量的时候使用同步

       采用上述的方法对类进行修改时并不是那么容易,一个好的方法是在设计类的时候就需要考虑到并发的情况。

       另外,完全有线程安全类构成的程序并不一定就是线程安全的,而在线程安全类中也可以包含非线程安全的类

什么是线程安全性?

       要对安全性给出一个确切的定义是非常复杂的,定义越正式,就越难理解。一个比较简单的定义就是:

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

下面来看一个servlet

@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 };
    }
}

大多数的servlet相同,StatelessFactorizer 是无状态的。两个线程同时访问时,并没有共享状态,就好像它们都在访问不同的实例,因此无状态的对象一定是线程安全的

原子性

下面我们对上面的servlet进行修改:

@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 };
    }
}

       我们在其中添加了一个命中计数器(Hit Counter)来统计处理的请求数量,但是这种做法并不是线程安全的。
       看似紧凑的++count的语法,只是执行了一个操作,但是这个操作并非原子性的。实际上,包括了三个操作:读取count的值,将值加1,然后将结果写入到count,整个流程就是”读取-修改-写入”。
假如现在count默认为1,两个线程同时读取了值1,同时加1,那么命中计数器的值就将偏差1,所以说这是线程不安全的。

在并发编程中,由于不恰当的执行时序而出现不正确的结果,这有一个正式的名字,竞态条件(Race Condition)

竞态条件

概念:两个或多个线程对共享的数据进行读写操作时,最终的结果取决于线程的执行顺序,叫做竞态条件。
当某个计算的正确性取决于多个线程的交替执行时,那么就会发生竞态条件。

最常见的竞态条件的类型就是”先检查后执行”,
在实际情况中经常会遇到竞态条件:

       例如,假定你计划中午在University Avenue的星巴克与一位朋友会面。但当你到达那里时,发现在University Avenue上有两家星巴克,并且你不知道说好碰面的是哪一家。在12∶10时,你没有在星巴克A看到朋友,那么就会去星巴克B看看他是否在那里,但他也不在那里。这有几种可能:你的朋友迟到了,还没到任何一家星巴克;你的朋友在你离开后到了星巴克A;你的朋友在星巴克B,但他去星巴克A找你,并且此时正在去星巴克A的途中。我们假设是最糟糕的情况,即最后一种可能。现在是12∶15,你们两个都去过了两家星巴克,并且都开始怀疑对方是否失约了。现在你会怎么做?回到另一家星巴克?来来回回要走多少次?除非你们之间约定了某种协议,否则你们整天都在University Avenue上走来走去,倍感沮丧。

       在“我去看看他是否在另一家星巴克”这种方法中,问题在于:当你在街上走时,你的朋友可能已经离开了你要去的星巴克。你首先看了看星巴克A,发现“他不在”,并且开始去找他。你可以在星巴克B中做同样的选择,但不是同时发生。两家星巴克之间有几分钟的路程,而就在这几分钟的时间里,系统的状态可能会发生变化。

       在星巴克这个示例中说明了一种竞态条件,因为要获得正确的结果(与朋友会面),必须取决于事件的发生时序(当你们到达星巴克时,在离开并去另一家星巴克之前会等待多长时间……)。当你迈出前门时,你在星巴克A的观察结果将变得无效,你的朋友可能从后门进来了,而你却不知道。这种观察结果的失效就是大多数竞态条件的本质—基于一种可能失效的观察结果来做出判断或者执行某个计算。这种类型的竞态条件称为“先检查后执行”:首先观察到某个条件为真(例如文件X不存在),然后根据这个观察结果采用相应的动作(创建文件X),但事实上,在你观察到这个结果以及开始创建文件之间,观察结果可能变得无效(另一个线程在这期间创建了文件X),从而导致各种问题(未预期的异常、数据被覆盖、文件被破坏等)。

以上是竞态条件的一个例子,下面来看一段代码

@NotThreadSafe
public class LazyInitRace {
    private ExpensiveObject instance = null;

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

class ExpensiveObject { }

       在LazyInitRace中包含了一个竞态条件,可能会破坏这个类的正确性。假定线程A和线程B同时执行getInstance。A看到instance 为空,因而创建一个新的ExpensiveObject实例。B同样需要判断instance是否为空。此时的instance是否为空,要取决于不可预测的时序,包括线程的调度方式,以及A需要花多长时间来初始化ExpensiveObject并设置instance。如果当B检查时,instance为空,那么在两次调用getInstance时可能会得到不同的结果,即使getInstance通常被认为是返回相同的实例。

与大多数并发错误一样,竞态条件并不总会是产生错误,还需要某种不恰当的执行时序

复合操作

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

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

       为了确保线程安全性,“先检查后执行”(例如延迟初始化)和“读取-修改-写入”(例如递增运算)等操作必须是原子的。我们将“先检查后执行”以及“读取-修改-写入”等操作统称为复合操作.就目前而言,我们先采用另一种方式来修复这个问题,即使用一个现有的线程安全类。

@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; }
}

在java.util.concurrent.atomic包中包含了一些原子变量类,用于实现在数值和对象引用上的原子状态转换。通过用AtomicLong来代替long类型的计数器,能够确保所有对计数器状态的访问操作都是原子的。当在无状态的类中添加一个状态时,如果该状态完全由线程安全的对象来管理,那么这个类仍然是线程安全的。

加锁机制

       在Servlet中添加一个状态变量时,可以通过线程安全的变量来管理,但如果添加更多的状态,就没那么简单了。
       设我们希望提升Servlet的性能:将最近的计算结果缓存起来,当两个连续的请求对相同的数值进行因数分解时,可以直接使用上一次的计算结果,而无须重新计算。)要实现该缓存策略,需要保存两个状态: 最近执行因数分解的数值,以及分解结果。
       我们曾通过AtomicLong以线程安全的方式来管理计数器的状态,那么,在这里是否可以使用类似的AtomicReference来管理最近执行因数分解的数值及其分解结果?UnsafeCachingFactorizer实现了这种思想。

@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};
    }
}

上面的代码为什么是线程不安全的呢?我们都知道,在lastNUmber中缓存的数值应该等于在lastFactors中的因数之积,就是说两个数是存在关系的,彼此受到了约束,不是相互独立的。如果不能同时更新两个变量,而只修改了一个变量,那么就会发生错误。所以要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。

内置锁

Java提供了内置的锁机制来支持原子性:同步代码块(Synchronized Block),它包括了两部分:一个作为锁的对象引用,一个作为锁保护的代码块。

synchronized (lock){
    //由锁保护的代码块
} 

要想访问由锁保护的代码块,必须持有锁。Java的内置锁相当于一种互斥锁,这意味着每次最多只有一个线程能够拿到锁,当线程A拿到锁时,线程B想访问由锁保护的代码块,就必须等待或者阻塞,直到线程A释放锁,线程B才能拿到锁,从而访问又锁保护的代码块。

@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 };
    }
}

这样做的话就是线程安全的,因为每一次都只能有一个线程访问service方法,因此两个变量的更新都是由一个线程来同时做的。但是这段代码的并发性相仿糟糕,因为多个请求无法同时使用service方法,必须等待,这就又出现了性能问题。

重入

当一个线程请求由其他线程持有的锁时,该线程就会阻塞。但是由于内置锁可以重入,当一个线程请求由自己所持有的锁时,这是可以的。重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。当计数值为 0 时,这个锁就被认为是没有被任何线程所持有,当线程请求一个未被持有的锁时,JVM 将记下锁的持有者,并且将获取计数值置为 1,如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应地递减。当计数值为 0 时,这个锁将被释放。

public class Test {
    public static void main(String[] args) {
        test1 t = new test1();
        t.dosomething();
    }
    public synchronized void dosomething(){
        System.out.println("aaa");
    }
}

class test1 extends Test{
    public synchronized void dosomething(){
        System.out.println("bbb");
        super.dosomething();
    }
}

因为内置锁是可重入的,所以当调用super.dosomething()时,主线程就会获得Test的锁。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值