【Java 并发】线程安全性

来源:互联网 发布:淘宝怎么搜店铺客服 编辑:程序博客网 时间:2024/06/05 06:49

【Java 并发】线程安全性

一个对象是否需要线程安全的,取决于他是否被多个线程访问,要对象是线程安全的,需要采用同步机制来协同对象可变状态的访问。

一,如果多个线程访问一个可变的状态变量时没有使用合适的同步机制,可以使用以下方法解决
1,不共享该状态变量。
2,将可变修改为不可变的变量。
3,在访问变量时使用同步。

线程安全的定义:多个线程访问某个类,这个类始终都能表现出正确的行为。

二,原子性
原子性一种很通俗的理解是。要么一次做完,不会中断,要么就不做。
1,原子操作
原子操作是不能被线程调度所中断的操作,一旦开始,那么它一定在可能发生线程切换之前完成。原子操作有除long和double之外的所有基本类型操作。因为JVM可以将64位的读写操作当做两个分离的32位操作来执行,这就可能产生在读写的过程中出现线程的切换。对域中的值做赋值和返回操作通常是原子操作。
看代码理解

public class UnsafeCountingFactorizer 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;虽然看上去像是一个操作,但是这个操作非原子性,它不是一个不可分割的操作,实际上有三个独立的操作:读取-修改-写入,三种操作的结果都是依赖于前一个操作。举个栗子:A线程读取count=1,在要进行加1操作时,B线程正好读取count,此时的count=1,结果A,B线程运行的结果都是count=2。如果A,B线程是按顺序执行时,则A的结果是count=2,B的结果是count=3。整个计算正确性取决于多个线程的交替执行时序时,就会发生竞态条件。也就是说正确的结果取决于运气。

2,竞态条件
本质:基于一个可能失败的观察结果来做出判断或者执行操作。简单来说就是“先检查后执行”。++count也存在竞态,要根据前一个结果,再执行加1操作。
再看个例子

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

程序中的初始化操作推迟到实际使用时才进行,延迟初始化。程序中也存在竞态。假设A和B线程都调用 getInstance() ,A判断instance为空,于是new ExpensiveObject()实例。B线程也同样要判断instance是否为空,instance是否为空就取决于A线程创建对象的时间,若B线程判断判断instance为空,那么又new ExpensiveObject()实例,就存在两个实例了。

3,复合操作
之前两个程序++count和instance = new ExpensiveObject()都是复合操作,当然还有许多复合操作,这些操作都包含一组必须以原子方式执行的操作以确保线程安全性。先介绍一种方式:使用线程安全类。

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

在java.util.concurrent.atomic包中包含一些原子变量类,用于实现在数值和对象引用上的原子状态转换。 AtomicLong 代替Long。能够确保所有对计数器状态的访问操作都是原子的。
在平时编程时很少使用原子变量类,但是在性能调优时就大有用武之地。

三,加锁机制
先看段程序

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

如果程序正常运行,程序中的lastNumber缓存的值应该等于lastFactors缓存的因数之积。所以这两个变量不是彼此独立的,是互相依赖的,lastNumber缓存的值改变则lastFactors缓存的因数也要同时做出相应改变。那么问题就来了,如果只修改的一个值,那么其他线程运行时就出问题了。还有一种可能,A线程正在读取两个值,而B线程在修改,那么就无法保证同时获取两个值。

Java提供了一种锁机制:同步代码块来解决这个问题。它包含两部分:锁的对象引用,有这个锁保护的代码块。用synchronized修饰的方法就是一个同步代码块。其中同步代码块的锁就是方法调用所在的对象。

synchronized(lock){//代码块}

修改成同步代码块

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

锁机制还有一个重入问题
重入的一种实现方法是,为每个锁关联一个获取计数器和一个所有者线程,计数器为0时,这个锁没被任何线程持有,当线程请求一个未被持有的锁时,JVM记录锁的持有者,计数器加1,如果通过线程再次获取锁,则计数器相应增加。当线程退出同步代码块时,计数器递减。当计数器为0时,锁被释放。

使用锁机制时,要注意同个同步代码块只能由同一个所对象来保护。

以上都是阅读《Java编程思想》-并发和《Java并发编程实践》的笔记,不喜勿喷!

原创粉丝点击