【Java 并发】线程安全性

【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并发编程实践》的笔记,不喜勿喷!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值