1、竞态条件
当计算结果的正确性取决于相对时间或者调度器控制的多线程交叉时,就会发生竞态条件。这句话可能对初次接触线程的读者来说不太好理解,其实竞态条件有两个相对比较好理解的描述,一个是check-then-act,另外一个是read-modify-write。
check-then-act通常指的是用"过时"的状态去决定下一步的动作。比如看一个例子:if(number == 100) mynumber = number * 10;假设其中一个线程ThreadA在执行完number == 100的判断为真后,马上要执行mynumber = number * 10的时候,ThreadA被调度器暂停了,此时线程ThreadB执行了number = 20的操作,当ThreadA再次恢复执行的时候,mynumber的结果已经不是1000了。这个结果的前提是number和mynumber是成员属性而不是局部属性,因为每个线程都有自己的局部属性拷贝,不会出现竞态条件。
read-modify-write问题通常指的是多个线程同步执行导致读、写、改同一个数据时并没有按照一个原子动作去执行。比如看一个例子:public int getNumber( ){ return number++; }这个方法看起来是一个动作,其实number++是三个独立的操作:读取number的值,为number加1,最后把更新之后的值赋给number。假设线程ThreadA调用了getNumber方法并在读取了number的值后,线程ThreadB开始运行了,同样调用了getNumber方法并读取了number的值,在为number加1之后返回。此时线程ThreadA恢复了运行,同样在原来读取的number数值之上加了1,返回到调用处。此时线程ThreadA在结果上实际撤销了线程ThreadB的操作,系统错过了一次加1的操作。
2、数据竞争
数据竞争指的是在同一个应用中多个线程并发访问同一块内存区,其中至少有一个线程是进行写操作。而且这些线程没有对这块内存区的访问进行协调,导致每次运行都会产生不一样的运行结果。为了描述数据竞争,我们看一个例子:
假设线程ThreadA调用了getMyIns方法,由于my对象是空值,ThreadA会为其创建实例并赋值给my变量。此时线程ThreadB调用getMyIns方法,它可能会检测到非空值my并返回,也可能会检测到my为空值,于是也创建了一个新的My类对象。由于线程ThreadA和线程ThreadB之间没有happens-before ordering(动作先后)的保证,这时就发生了数据竞争。
3、缓存变量
缓存变量是由于系统要提升性能带来的问题,通常JVM会为每个线程提供一个自己的缓存区用来存放自己的变量拷贝,而不是依赖于系统主存。当线程对这个缓存区进行变量操作时,其他线程是不可见的。为了描述缓存变量,我们看一个实例:
例子中的类属性my演示了缓存变量的问题。线程thread完成对my属性的操作,然后主线程执行输出语句,输出结果。问题是,线程thread能够将返回值存储到自己的my变量的拷贝中,主线程很可能无法看到赋值的结果。主线程很可能将本地的null结果打印出来。