线程安全的理论基础(一)

线程安全的理论基础(一)

1.前言

Java里面的并发编程是其重大特色之一,是一把威力重大的双刃剑,使用得当则会给程序性能上带来很大的提升,但是使用不当也会带来很多的风险,因此也对程序员有更高的要求来规避这些可能出现的问题。
这一系列的博客主要是楼主对正在学习的java多线程的知识的一个回顾,反思和总结。本篇主要是说明用于避免并发危险,构建线程安全类的一些规则。

2.并发编程的安全性

由于在并发中,其底层JVM基础的理论是类似于OS 中时间片轮转算法和流水线的工作方式,从而充分利用多CPU的性能。由于并发存在不可预测性(JVM的指令重排等导致(Reordering)),所以想要得到正确的结果,需要靠程序员增加一些控制,避免让底层JVM产生二义性。
线程安全性:当某个线程访问某个类时,这个类始终都能表现出正确的行为。正确性的意思大概是指所见即所得(we know it when we see it),个人认为可以理解为预期的正确的结果。

3.怎么样保证并发编程的安全性

在WEB应用中基础的Servlet框架,被用来部署网页应用程序以及分发HTTP 客户端的请求。因为多个客户端可以同时访问一个Servlet服务,因此Servlet需要满足多线程安全。
首先一个普通的Servlet伪代码如下:


public void service(ServletRequest req, ServletResponse resp) { 
        BigInteger bigInteger = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        encodeIntoResponse(req,factors);
 }
-----------------------------------------------------------

因为这是一个无状态的Servlet 服务,不包含任何域,也不包含任何对其他类中的域的引用,计算过程中的临时状态仅存在线程栈的局部变量中,并且能由正在执行的线程访问。所以正在访问的线程不会影响到另外一个访问该servlet服务的线程,所以这种无状态的类是线程安全的。

3.1 竞态条件

接下来假设我们需要统计被响应的请求的数量,在单线程中我们可能只用考虑在处理完请求之后对计数器count加1,伪代码如下:

private long count = 0;
public void service(ServletRequest req, ServletResponse resp) {
    BigInteger bigInteger = extractFromRequest(req);
    BigInteger[] factors = factor(i);
    count++;
     /*因为实际上这种自加运算,在底层需要只要对应三个指令执行,读取(一
     般从寄存器到运算器)---> 运算(运算结果完把数据放到计算寄存器)-->
     存储(在将这些数据存回计数器)*/
    encodeIntoResponse(req, factors);
}

由于多个线程之间可能存在一个线程a刚取count =1的当前值去进行运算,另一个线程也取出这个count = 1从而导致两次执行完的结果仍然是2(正确的为3)。这种由于不恰当的执行时序而出现不正确的结果的现象被称做竞态条件(Race Condition)。

PS:出现以上的原因是并发中JVM(或者说OS)对++这种运算会对应几个不可再分的指令(一般对应一个最小的时间元操作,取数据,进行CPU 运算和存数据)操作。因此在几个时间元内同时执行,可能会出现所谓的“脏”数据。而在OS中称这些不可再分的运算具有原子性。

另外一种常见的竞态条件“先竞争后执行”的情况延迟初始化等情况,通常表现为在要执行某个动作时,需要对相关条件进行判断,即if(condition){do something},但是因为条件condition 的条件可能被另外一个线程改变,从而产生不同的情况,在实际运行中就会产生不可预测的现象。
在以上的情况中,其实要避免线程危险,都包含一组不可分割的原子操作,即需要一定的手段来避免某个线程修改变量的时候,通过某种方式防止其他线程使用这个变量。

3.2原子类型

针对第一种的++情况,可以使用AtomicLong 这种类型来保证++的并发操作正确性。在Java.util.concurrent.atomic包中包含了一些原子变量类,用于实现数值和对象引用上的原子状态转换。

Ps:当使用两种不同的同步机制时,常常会带来混乱,也不会带来性能和安全性上带来任何好处,因此更推荐使用更普遍的synchronized方式来实现。

3.3 加锁机制

如上可以看到通过AtomicLong类型来保证一个共享变量的安全性,但是试想,如果一个类中存在多个共享变量,这时简单的AtomicLong就没办法满足线程安全,就需要用其他机制来解决这个问题——锁机制。
Java提供一种内置机制来支持原子性和内存可见性:同步代码块(Synchronized Block)。

同步代码块相当于互斥锁,这意味这最多只有一个线程能持有这种锁,而持有这个锁的线程才有资格进入同步代码块,从而也就保证了整个线程操作的原子性。
同步代码块的实现机制是重入的方式。重入的的一种实现方式是:为每个锁关联一个获取计数值和一个拥有这个锁的线程,当计数值为0时,这个锁就被认为是没有被任何线程持有。当线程请求一个未持有的锁时,就记下该锁的持有者,并将计数值加1.如果本线程再次获得这个锁时(像循环等),计数值将增加;线程退出时,这个锁将被释放。这种方式也解决了继承的锁可能产生的问题。

在java中实现的方式是关键字synchronized,它可以用来修饰类,可以避免的显式的创建锁对象,常见的是:

synchronized(class) {
    Do something
}

使用synchronized的几个注意点:1. 对于一个可变状态变量,在访问(写入和读取)它时都需要持有同一个锁,以保证原子性和内存可见性。2.对象的内置锁应该尽量保证并发的粒度不要太大,以保证整个程序的性能。

4.小结

本篇主要说明并发中的原子性,原子性控制的情况——竞态条件和实现方式。


参考书籍:Java 并发实战(Java Concurrency in Practice)—- Brian Goetz / Tim Peierls / Joshua Bloch / Joseph Bowbeer / David Holmes / Doug Lea

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值