线程的安全性

来源:互联网 发布:csgo awp 数据 编辑:程序博客网 时间:2024/05/17 03:28

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

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

下面的程序给出了一个简单的因数分解Servlet。这个Servlet从请求中提取出数值,执行因数分解。然后将结果封装到该Servlet的响应中。

public class StatelessFactorizer implements Servlet{    public void service(ServletRequest req, ServletResponse resp){        BigInteger i = extractFromRequest(req);        BigInteger[] factors = factor(i);        encodeIntoResponse(resp,factors);        }    }

与大多数Servlet相同,StatelessFactorizer是无状态的;它既不包含任务域,也不包含任务对其他类中域的引用。计算过程中的临时状态仅存在与县城栈上的局部变量中,并且只能由正在执行的线程访问。访问StatelessFactorizer的线程不会影响另一个访问同一个StatelessFactorizer的线程的计算结果,因为这两个线程并没有共享状态,就好像他们都在访问不同的实例。由于线程访问无状态对象的行为并不会影响其他线程中操作的正确性,因此无状态对象是线程安全的。

2. 原子性

假设我们在上面的程序中加入一个计数器,每处理一个请求,将计数器加1,那么还会是线程安全的么?

public class StatelessFactorizer 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);        }    }

上面的++count实际上,包含了三个独立的操作:读取count的值,将值加1,然后将计算结果写入count,这是一个“读取-修改-写入”的操作序列,并且其结果以来于之前的状态。假如两个线程在没有同步的情况下同时对一个计数器进行了递增操作,每个线程读取到的值都是0,那么执行完递增操作豆浆计数器的值设为1,显然,计数器的值就偏差1,当前也就不是线程安全的了。

3. 竞态条件

当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。换句话说,就是正确地结果取决于运气。最常见的静态条件就是“先检查后执行”,既通过一个可能失效的结果来决定下一步的动作。下面的程序展示了延迟加载中的竞态条件。

public class LazyInitRace{    private TestObject instance = null;    public TestObject getInstance()    {        if(instance == null)        {            instance = new TestObject();        }        return instance;    }}

在LazyInitRace中包含了一个竞态条件,它可能会破坏这个类的正确性。假定线程A和线程B同时执行getInstance。A看到instance为空,因而创建了一个新的实例。B同样需要判断instance是否为空,此时B是否创建新的实例取决于instance的状态,若A创建完成实例,则返回相同的实例,若A未创建完成实例,则instance实例则通过A、B线程创建了两次,那么A、B线程通过getInstance可能会返回不同的结果。

4. 复合操作

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

在java.util.concurrnet.auomic包中包含了一些原子变量类,用于实现在数值和对象引用上的原子状态转换。通过使用AtomicLong来代替long类型的计数器,能够确保了代码的线程安全性。当在无状态的勒种添加一个状态时,如果该状态完全由线程安全的对象来管理,那么这个类仍然是线程安全的。下面的程序展示了这一特性。

public class CountFactorizer 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(req, factors);    }}

但是,当状态的变量由一个变成多个时,并一定说程序是线程安全的。当在不性条件中设计多个变量时,各个变量之间并不是彼此独立的,而是某个变量的值会对其他变量的值产生约束。因此,当更新某一个变量时,需要在同一个原子操作中对其他变量同时进行更新。下面的错误示例展示了多个变量,线程不一定是安全的。

public class CountFactorizer 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(req, factors);        }        else        {            BigInteger[] factors = factor(i);            lastNumber.set(i);            lastFactors.set(factors);            encodeIntoResponse(req, factors);        }    }}

4. 重入
当某个线程请求一个由其他线程只有的锁时,发出请求的线程就会阻塞。然后,由于内置锁时可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会重入成功。如果内置锁不是可重入的,那么下面的代码将会发生死锁。

publc class Widget{    public synchronized void doSomething()    {        ...    }}public class LoggingWidget extends Widget{    public synchronized void doSomething()    {        System.out.println(": calling doSomething");        super.doSomething();    } }
0 0
原创粉丝点击