您的位置:首页 > 编程语言 > Java开发

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 并发 多线程