java 并发基础!

本贴最后更新于 2654 天前,其中的信息可能已经事过景迁

当一个对象或变量可以被多个线程共享的时候,就有可能使得程序的逻辑出现问题。 在一个对象中有一个变量 i=0,有两个线程 A,B 都想对 i 加 1,这个时候便有问题显现出来,关键就是对 i 加 1 的这个过程不是原子操作。要想对 i 进行递增,第一步就是获取 i 的值,当 A 获取 i 的值为 0,在 A 将新的值写入 A 之前,B 也获取了 A 的值 0,然后 A 写入,i 变成 1,然后 B 也写入 i,i 这个时候依然是 1. 当然 java 的内存模型没有上面这么简单,在 Java Memory Model 中,Memory 分为两类,main memory 和 working memory,main memory 为所有线程共享,working memory 中存放的是线程所需要的变量的拷贝(线程要对 main memory 中的内容进行操作的话,首先需要拷贝到自己的 working memory,一般为了速度,working memory 一般是在 cpu 的 cache 中的)。volatile 的变量在被操作的时候不会产生 working memory 的拷贝,而是直接操作 main memory,当然 volatile 虽然解决了变量的可见性问题,但没有解决变量操作的原子性的问题,这个还需要 synchronized 或者 CAS 相关操作配合进行。

多线程中几个重要的概念:

可见性

也就说假设一个对象中有一个变量 i,那么 i 是保存在 main memory 中的,当某一个线程要操作 i 的时候,首先需要从 main memory 中将 i 加载到这个线程的 working memory 中,这个时候 working memory 中就有了一个 i 的拷贝,这个时候此线程对 i 的修改都在其 working memory 中,直到其将 i 从 working memory 写回到 main memory 中,新的 i 的值才能被其他线程所读取。从某个意义上说,可见性保证了各个线程的 working memory 的数据的一致性。 可见性遵循下面一些规则:

  • 当一个线程运行结束的时候,所有写的变量都会被 flush 回 main memory 中。
  • 当一个线程第一次读取某个变量的时候,会从 main memory 中读取最新的。
  • volatile 的变量会被立刻写到 main memory 中的,在 jsr133 中,对 volatile 的语义进行增强,后面会提到
  • 当一个线程释放锁后,所有的变量的变化都会 flush 到 main memory 中,然后一个使用了这个相同的同步锁的进程,将会重新加载所有的使用到的变量,这样就保证了可见性。

原子性

还拿上面的例子来说,原子性就是当某一个线程修改 i 的值的时候,从取出 i 到将新的 i 的值写给 i 之间不能有其他线程对 i 进行任何操作。也就是说保证某个线程对 i 的操作是原子性的,这样就可以避免数据脏读。 通过锁机制或者 CAS(Compare And Set 需要硬件 CPU 的支持)操作可以保证操作的原子性。

有序性

假设在 main memory 中存在两个变量 i 和 j,初始值都为 0,在某个线程 A 的代码中依次对 i 和 j 进行自增操作(i,j 的操作不相互依赖)

i++;
j++;

由于,所以 i,j 修改操作的顺序可能会被重新排序。那么修改后的 ij 写到 main memory 中的时候,顺序可能就不是按照 i,j 的顺序了,这就是所谓的 reordering,在单线程的情况下,当线程 A 运行结束的后 i,j 的值都加 1 了,在线程自己看来就好像是线程按照代码的顺序进行了运行(这些操作都是基于 as-if-serial 语义的),即使在实际运行过程中,i,j 的自增可能被重新排序了,当然计算机也不能帮你乱排序,存在上下逻辑关联的运行顺序肯定还是不会变的。但是在多线程环境下,问题就不一样了,比如另一个线程 B 的代码如下

if(j == 1){
System.out.println(i);
}

按照我们的思维方式,当 j 为 1 的时候那么 i 肯定也是 1,因为代码中 i 在 j 之前就自增了,但实际的情况有可能当 j 为 1 的时候 i 还是为 0。这就是 reordering 产生的不好的后果,所以我们在某些时候为了避免这样的问题需要一些必要的策略,以保证多个线程一起工作的时候也存在一定的次序。JMM 提供了 happens-before 的排序策略。这样我们可以得到多线程环境下的 as-if-serial 语义。 这里不对 happens-before 进行详细解释了,详细的请看这里 http://www.ibm.com/developerworks/cn/java/j-jtp03304/,这里主要讲一下 volatile 在新的 java 内存模型下的变化,在 jsr133 之前,下面的代码可能会出现问题

Map configOptions;

char[] configText;

volatile boolean initialized = false;

// In Thread A

configOptions = new HashMap();

configText = readConfigFile(fileName);

processConfigOptions(configText, configOptions);

initialized = true;

// In Thread B

while (!initialized)

sleep();

// use configOptions

jsr133 之前,虽然对 volatile 变量的读和写不能与对其他 volatile 变量的读和写一起重新排序,但是它们仍然可以与对 nonvolatile 变量的读写一起重新排序,所以上面的 Thread A 的操作,就可能 initialized 变成 true 的时候,而 configOptions 还没有被初始化,所以 initialized 先于 configOptions 被线程 B 看到,就产生问题了。

JSR 133 Expert Group 决定让 volatile 读写不能与其他内存操作一起重新排序,新的内存模型下,如果当线程 A 写入 volatile 变量 V 而线程 B 读取 V 时,那么在写入 V 时,A 可见的所有变量值现在都可以保证对 B 是可见的。

结果就是作用更大的 volatile 语义,代价是访问 volatile 字段时会对性能产生更大的影响。这一点在 ConcurrentHashMap 中的统计某个 segment 元素个数的 count 变量中使用到了。

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3187 引用 • 8213 回帖
  • 并发
    75 引用 • 73 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...