JAVA并发编程(一)理解线程安全与并发
2018-01-24 20:47
190 查看
进程与线程
记得大二的时候学操作系统课,考试必考进程和线程的区别,当时只顾着硬背这两个“学术化的官方定义”,结果还是没怎么深入理解这两个概念的含义。到了这个学期又学了并行程序设计课,对多线程并发理解又多了一些,打算连续写几篇文章来分析整理关于JAVA并发编程的问题。什么是进程?
如果你想要官方式的背诵进程的定义,我也不反对。但是在下面我会用很大的篇幅做一个进程的定义,可能不够简洁,但应该是很深入很形象的。在计算机系统中有关于信息的定义:信息=位+上下文。这里的信息指二进制01代表的信息,而这一串二进制01位串所代表的信息,由其中表示数据的位和表示上下文的位组成。与之类似的,我们的计算机为了达到多个程序同时进行的效果,CPU一直在进行:读取一个程序的上下文——>执行计算——>保存此程序上下文——>读取下一个程序上下文——>执行计算——>…这样的操作,用多个程序之间的快速切换模拟同时进行多程序的效果。为了让CPU不空闲,在不断的切换上下文读取信息并计算的过程中,我们要读写的上下文就是进程的资源与空间的状态,执行计算的内容就是进程的运行内容。CPU读取上下文、执行计算、保存上下文这三步操作的对象放在一起组成了进程的概念,进程是关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。所以我们很好理解,在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。
有了进程我们为什么还需要线程?
有了进程的概念,我们可以从概念上让CPU同时执行多个程序,既然进程已经能实现并行,为什么还有线程出现呢?实际上进程是一个粗粒度的划分,每个进程有自己的一套空间和资源,进程之间是相对独立的,在独立的进程中可能需要很多部分也能够拥有并行的效果。在此时我们用线程的概念来解决这个问题,进程是线程们的容器。线程之间可以并发执行,拥有独立的程序计数器和空间,但是一个进程下的线程能够共享进程的资源,这也就是说,不同的线程在CPU上运行是不需要切换上下文的。如果使用得当,线程可以有效的降低程序开发的难度和维护成本。在很多现如今的操作系统中,线程是CPU的基本的调度单位而不再是进程,或许我们可以把线程理解为轻量级的进程。线程安全问题
线程是高效的,但会带来并发访问的安全性问题,如何解决线程访问冲突是我们讨论的所有问题之源。什么样的程序不是线程安全的?
如何快速判断一段程序会不会发生线程安全问题?我们可以注意观察以下2点,只要有一点符合下文所述,那这段程序就必须进行处理:
有关共享数据的操作不是原子性操作
含有由多个共享数据的原子操作组合而成的复合操作
用下面的代码为例,在servlet服务中它的作用是计算输入数据的所有因数并输出,我们想要做的是让它增加一个计数功能,能够完全的统计所有使用过此方法的次数。
import java.math.BigInteger; /** * Title: CountingFactorizer * Description: * Company: www.QuinnNorris.com * * @date: 2018/1/24 下午5:32 星期三 * @author: quinn_norris * @version: 1.0 */ public class CountingFactorizer implements Servlet { public void service(ServletRequest req , ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = factor(i); encodeIntoResponse(resp,factors); } }
简单的,下面我们仅仅添加了一个count计数器,但是随之而来的问题是,当多个线程同时访问此程序时,在同时对count进行修改时可能会发生错误,一个线程正在修改,而另一线程读取了旧的count数据,导致count数据出现错误。此时我们需要一种方法保证count++;这步操作是不可被打断的,当进行count++时需要保证只能有一个线程在一个时间进行这一步操作。
import java.math.BigInteger; /** * Title: CountingFactorizer * Description: * Company: www.QuinnNorris.com * * @date: 2018/1/24 下午5:32 星期三 * @author: quinn_norris * @version: 1.0 */ public class CountingFactorizer implements Servlet { public 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); } }
为了改进这种问题,下面介绍一种很多人比较陌生的方法,在JAVA的util包中的concurrent.atomic包中提供了各种基本类型和引用类型数据的原子话操作,在上面的改进代码中我们将count的类型变为AtomicLong类型,用此类的方法incrementAndGet来替换++操作。这样能够确保此步操作是原子操作,原理也很容易理解,这个方法是sychronize同步的方法。
import java.math.BigInteger; import java.util.concurrent.atomic.AtomicLong; /** * Title: CountingFactorizer * Description: * Company: www.QuinnNorris.com * * @date: 2018/1/24 下午5:32 星期三 * @author: quinn_norris * @version: 1.0 */ public class CountingFactorizer implements Servlet { public 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); } }
通过使得对于数据的操作原子化,我们避免了线程冲突问题,这种错误也是很容易被发现的。很多类(例如Vector类)就是采用内部封住的方法,让我们在使用的过程中的每一步都是原子操作。
是不是全部使用这些都是原子操作的语句就能保证正确呢?实际上并不是这样的,即使使用的都是原子操作,我们不能保证多个原子操作形成的复合操作的原子性的。也就是说,两个原子操作之间可能会被打断,被其他线程插入,这种错误是隐蔽的不容易被发现的。
比如下面的这段代码,功能是为CountingFactorizer添加一个记忆功能,让他能够记住最后一个计算的因数情况,如果下一个输入的因数和之前输入的是一样的,就直接输出,提高工作效率。
import java.math.BigInteger; import java.util.concurrent.atomic.AtomicReference; /** * Title: CountingFactorizer * Description: * Company: www.QuinnNorris.com * * @date: 2018/1/24 下午5:32 星期三 * @author: quinn_norris * @version: 1.0 */ public class CountingFactorizer implements Servlet { private final AtomicReference<BigInteger> lastNumber = new AtomicReference<>(); private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<>(); public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); if (i == lastNumber.get()) encodeIntoResponse(resp, lastFactors.get()); else { BigInteger[] factors = factor(i); lastFactors.set(factors); lastNumber.set(i); encodeIntoResponse(resp, factors); } } }
上面的代码是有问题的。
在多个原子操作过程中会被打断,一条分析程序是否会引发线程问题的准则:如果所有与顺序相关的代码都处于临界区内,代码一定是正确的。分析逻辑后发现上述功能需要整体的顺序性很强,我们采用synchronized的方法让整个方法处于临界区中,写法如下。
import java.math.BigInteger; /** * Title: CountingFactorizer * Description: * Company: www.QuinnNorris.com * * @date: 2018/1/24 下午5:32 星期三 * @author: quinn_norris * @version: 1.0 */ public class CountingFactorizer implements Servlet { BigInteger lastNumber; BigInteger[] lastFactors; public synchronized void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); if (lastNumber.equals(i)) encodeIntoResponse(resp, lastFactors); else { BigInteger[] factors = factor(i); lastFactors = factors; lastNumber = i; encodeIntoResponse(resp, factors); } } }
使用synchronized关键字修饰了方法,使得整个方法都是临界区,那么在这其中再使用Atomic来保持数据的原子性是没必要的,我们用原来的BigInteger来替换掉Atomic类型的数据。
相关文章推荐
- 我理解的Java并发基础(三):线程安全与锁
- 深入理解线程 以及线程并发的线程安全问题及处理方法
- Netty : writeAndFlush的线程安全及并发问题
- java线程安全理解
- [深入理解Java虚拟机]第十三章 线程安全与锁优化-线程安全
- 深入理解Java并发机制(5)--线程、中断、Runnable、Callable、Future
- 高并发下最全线程安全的单例模式几种实现
- JAVA 并发编程随笔【七】线程安全与共享资源
- java线程安全理解
- Java Web并发访问的线程安全问题
- java并发-线程安全与共享资源(4)
- Java 并发之线程安全
- 深入理解JVM学习笔记——第十三章 线程安全与锁优化
- 线程安全和线程不安全理解
- 深入理解java虚拟机--线程安全与优化
- java并发编程:线程安全管理类--原子操作类--AtomicStampedReference<V>
- Java并发编程的暗自努力(四)线程安全与共享资源
- C#并发处理-锁OR线程安全?
- 深入理解Java并发机制(5)--线程、中断、Runnable、Callable、Future
- 线程安全和线程不安全理解