技术开发 频道

Java并发编程:问题

  【IT168 技术文章】线程之间共享数据引起了并发执行程序中的同步问题。那些数据是可能需要同步访问的呢?很简单,线程之间能够共享的数据,也就是对多个线程可见的数据。

  Java的数据有两种基本类型内存分配模式(不算虚拟机内部类型,详细内容参见虚拟机规范):运行时栈和堆两种。由于运行时栈是线程所私有的,它主要用来保存局部变量和中间运算结果,因此它们的数据是不可能被线程之间所共享的。内存堆是创建类对象和数组地方,它们是被虚拟机内各个线程所共享的,因此如果一个线程能获得某个堆对象的引用,那么就称这个对象是对该线程可见的。

  线程之间通信基本上通过共享对象引用来达到共享对象的简单类型字段和引用字段。由于不涉及I/O操作,这种模式的共享比IPC共享要高效的多。但也使得两类错误成为可能:线程干扰和内存一致性错误。防止此类问题发生的线程编程技术称作同步。我们详述一下这两个错误的概念。

  线程干扰

  考察下面的一段代码:

1 class Counter {
2     private int c = 0;
3
4     public void increment() {
5         c++;
6     }
7
8     public void decrement() {
9         c--;
10     }
11
12     public int value() {
13         return c;
14     }
15
16 }
17
18

  Counter的目的是对increment方法的调用将c增加1,对decrement的调用将c减去1。然而如果一个Counter对象被多个线程所引用,那么这些线程之间的干扰就让我们经常得不到期望的结果。

  当运行在不同线程中对同一对象进行访问的两个操作发生时,干扰就会产生。这是因为这两个操作往往是由多步组成的,而且它们的执行顺序是可以互相交织的。

  表面看来,Counter对象的increment和decrement操作是不可能交织的,每个操作都是一个简单的Java语句。实际上这些语句都已经被虚拟机翻译成了好几步的指令。我们不再详细描述虚拟机所采用的指令步骤,只需知道一个c++操作可能被虚拟机分为三步:

  1.获取c的当前值

  2.将该值加上1

  3.将加的结果保存回变量c

  c--也是同样三步,除了第二步进行减操作外。

  假设A线程调用increment方法的同时B线程调用decrement方法,而c的初始值为0,那么它们的交织的指令动作可能依着下面的顺序进行:

  1. 线程A: 获取c.

  2. 线程B: 获取c.

  3. 线程A: 获取的值加1,结果1

  4. 线程B: 获取的值减1,结果-1

  5. 线程A: 将1保存到c变量中,c结果是1

  6. 线程B: 将-1保存到c变量中,c结果是-1

  线程A的结果丢失了,被线程B的覆盖了。这种特殊的交织顺序只产生一种结果。但在另一种情况下,也可能B的结果被覆盖,或者干脆没有错误。由于它们执行顺序的不确定性,线程干扰的错误将很难定位和修改。

  内存一致性错误

  当不同线程对同一个数据看到不同视图时,内存一致性错误就发生了。内存一致性错误发生的原因很复杂,不是一两句话能解释得清楚的,在这儿就不再详述。我们只需知道防备这种错误发生的策略就行了。

  避免内存一致性错误的关键是理解“发生过”(happens-before)关系,这个关系是保证被某语句写过内存的结果对其他某个语句是可见的。为理解这一点,考虑下面的例子,假设有个简单的int字段这样定义:

  int counter=0;

  这个counter字段在两个线程A和B之间共享。假设线程A增加counter:

  counter++;

  紧接着,线程B打印出counter:

  System.out.println(counter);

  如果这两句话在同一个线程内执行,那么假设打印结果为1是安全的。但如果当这两个语句是在不同线程中执行的,那么打印出的值完全有可能是0。因为线程A对于counter的改变不一定能对线程B可见,除非程序在这两条语句之间建立了“发生过”关系。

  有几种动作能建立“发生过”关系,目前我们已经接触的能产生“发生过”关系的动作包括:

  * 当语句调用Thread.start方法,任何和Thread.start建立有“发生过”关系的语句和新启动线程执行的每条语句都有“发生过”关系。创建新线程代码的效果对于这个新线程是可见的。

  * 当线程结束并导致另一个线程的Thread.join返回,那么结束线程所执行的所有语句和join后面的语句都有“发生过”关系。

  当然建立这种“发生过”关系的方法除了上面两种之外,还有以后文章将详细讲述的互斥-同步技术。

  目前我们描述了两种因为数据共享而发生的问题:线程干扰和内存一致性错误。线程干扰经常在“写”和“写”操作之间,当着两个动作需要产生叠加的效果时。而内存一致性错误经常发生在“写”和“读”之间,当读发生在写之后需要看到写的效果时。这两种错误是互斥-并发技术要解决的两大类问题。这两大类问题分别对应前文所讲的互斥和同步问题。其中避免线程干扰往往不需要保证操作的顺序,只要保证两个操作互斥进行就行了。而避免内存一致性错误除了需要操作要互斥以外,还需要规定动作的发生顺序,即需要同步。

0
相关文章