Java并发编程(一)线程安全的类的特性

线程安全的类是我们很希望达到的特性,那怎么才能判断以及设计一个线程安全的类,是我们需要关注的问题。
(一)线程安全的类的定义
首先我们关注如何定义一个线程安全的类,通俗的说,只要一个类能够在多线程环境中,无论怎样进行使用,都能够按照我们主观认为的方式“正确的”运行,那它就是线程安全的。《Java并发编程实战》中做了如下定义:
“当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的.”
定义想必都能理解,如果一个正确实现的类,在任何操作中都不会违背其功能设想,并且客户代码不需要任何同步措施,那么它就是线程安全的。可是,光有定义是不够的,我们希望的是找到可以分析的、可以理解的性质,或者说一个线程安全类到底应该具有哪些特性,只要保持了哪些特性我们就可以认为它是线程安全的。

(二)线程安全的类的特性
1、特性1——避免竞态条件(Race Condition)
竞态条件指的是在做一些操作的过程中,某个不具有原子性的操作在执行中,由于不恰当的执行顺序导致数据完整性的问题,常见的竞态条件类型有“读取-修改-写入”操作和“先检查后执行”操作。

“读取-修改-写入”操作:

count++

这句简单的代码是我们经常会使用的,但实际上它包含了三个独立的操作:读取count的值,将值加1,然后将计算结果写入count。那么,在某些不正确的执行顺序中,两个线程都读取到了当前的count值为5,接着依次执行了写入操作,那么数据就偏差了1,那么这个操作就不会按照我们设计的功能正确的执行了。

“先检查后执行”操作:

if(instance == null)
    instance = new Singleton();

这段代码在单线程情况下的单例模式中经常被使用,这就是一个典型的“先检查后执行”的操作,在多线程情况下,当你观测完成之后(执行if语句之后),观测的结果有可能随时会失效(另一个线程很有可能更改了instance),从而导致两个instance实例被创建。而且我们之前也说过DCL(Double Check Lock)的问题(详见我的另一篇博客),那就是一个更深层次的观测结果失效,有可能你观测的结果处于正在执行的阶段,但你却误以为它是完整的。

2、特性2——保证不变性条件
其实不变性条件的破坏也是由于竞态的产生而导致的,但为什么要单独归为一个特性,是因为它在线程安全中真的非常重要,很多多线程问题就是因为不变性条件得不到保持而出现的。许多类都定义了自身的一些不变性条件,比如一个存储范围的两个int值,那要保证存储min的值小于等于max的值。我们来看一个例子:

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

这是一个将输入数值进行因数分解的Servlet,我们通过将计算结果缓存起来从而提高性能,lastNumber缓存输入的值,lastFactors缓存因式分解结果。我们使用了能保证原子性的线程安全类来存储缓存值,可是这段代码中就隐含了一个不可变条件:

lastNumber.set(i);
lastFactors.set(factors);

这两句中我们是不是默认lastNumber对应的就是lastFactors中的结果?想必大家觉得这是理所当然的,可是在多线程情况下,线程A写入了lastNumber的值,这时切换到线程B,它写入了这两个值,再切换到线程A,它仍然认为lastNumber是自己之前写入的值,但其实这时候,不可变条件已经被破坏了。当下次进行计算时,由于错误的缓存,很有可能得到一个错误的结果!

3、特性3——保证共享对象的可见性
讲了前两点特性,我们已经意识到了线程安全类最重要的问题就是对象的共享,而1、2两点特性针对的主要是对象修改的问题(多个线程修改同一个对象,出现数据完整性问题),而我们这里的特性3针对的是当一个线程修改了对象,另一个线程读取时出现的可见性问题,让我们来看书上给出的例子:

public class NoVisibility{
    private static boolean ready;
    private static int number;
    private static class ReaderThread extends Thread {
        public void run() {
            while(!ready)
                Thread.yield();
            System.out.println(number);
        }
    }

    public static void main(String[] args){
        new RenderThread().start();
        number = 42;
        ready = true;
    }
}

我们看了这段代码觉得很容易理解,主线程先启动读者线程,读者线程一直循环直到ready为true,输出number,而主线程写入值并且将ready设为true。但事实上很有可能输出的值为0,而不是我们预想的42,因为读线程很有可能看到了ready为true,但却看不到number为42,这个原因称之为“重排序”。
虽然这是一个让人非常摸不着头脑的现象,但我们可以用一个简单的方法避免这个问题:只要存在共享数据,就使用正确的同步以保证其可见性。

4、特性4——保证对象安全的发布
之前的三点特性针对的都是在类内部的操作处理,虽然是处于共享状态的对象,但共享出去的对象的操作都是类内部进行定义了的,我们保证内部的方法进行了正确的同步就能保证线程安全性。但如果我们的对象能被外部代码所使用,那么如何保证安全性呢?

class UnsafeStates{
    private String[] states = new String[] { "AK", "AL" };
    public String[] getStates(){ return states; }
}

我们都知道引用在Java中的地位,当你返回引用时,你一定要注意,你很有可能做了一个不安全的发布!当你的引用对象被返回(上面代码中的states),得到此引用的线程很有可能进行一些操作,而这个操作是不可控的,那么竞态条件的风险就始终存在!那这个类就不是线程安全的类了。如果要做到线程安全,那么发布的对象必须是安全的,受保护的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值