多线程编程从来都是一件比较困难的事情,调试多线程程序也相当困难,这种困难来自于线程对共享资源操作的复杂性 ( 包括对于资源操作的线程间的先后顺序 ) 。对于 Java 来说,它封装了底层硬件和操作系统之间很多的细节,对于线程之间的调度底层细节我们大多数时候不用关心,然而真正编写 java 多线程程序时有一些东西我们却是不得不知道的。

在 java 中,

1、  多个线程之间数据交换是依靠内存来实现的。

2、  缓存:为了获得较高的性能,处理器读取内存中的数据后,可能会存储在自身缓存中,计算得到的新的结果值也可能直接写到自身缓存中,等待合适的时机再刷新到内存中去,在数据刷新到内存中之前,别的处理器是看不到这个更新的值的。 ( 这个造成的问题可能是缓存与主存中的数据不一致,这也就引出了值的可见性的问题 )

3、  次序:同样,为了获得最优性能, java 允许编译器在不修改程序语义的前提下,可以随意的排序某些指令的执行顺序,甚至允许处理器以颠倒的次序执行一些操作,例如,允许缓存以程序写入变量时不同的顺序把变量刷新到主存中。

注意以上提到的几点,理解他们在 java 多线程编程中至关重要,再继续之前,我来举两个实际的例子来说明一下上面的 2 和 3 点。

对于第 2 点,先来 看这一段代码:

(1)y=5; (2)b=y+3;

在单线程中执行这一段代码,处理器将 y 读入缓存,并且在执行 y+3 后,将计算到的结果再次存入缓存中,在某个时候 ( 可能立即,也可能在之后的某个合适的时间 ) 再将这个 b 值刷新到内存中。

换成多线程,线程 A 执行 (1)(2) ,而从另外一个线程 B 来读取 b 的值会有什么样的结果呢?答案是不确定。为什么呢 ? 即使是 A 线程先执行了 (1)(2) ,线程 B 再读取 b 的值,也有可能读取不到,因为有可能 b 的真正的值可能还在缓存中,而线程 B 只能从自己的缓存或者从内存中去读取所要的值,这就会造成 B 读到的可能是一个过期的内存值。

对于第 3 点,排序是什么意思呢?同样来看一段代码

(3) a=5 (4)b=6 (5)c=7 (6)d=8 (6)e=a+b (7)f=c+d

我们直观会觉得,处理器会依次执行上面的代码,但是答案也是不确定的,因为不同的编译器或处理器为了获得最高性能,很可能会调整最终代码的执行顺序,只要最终不影响程序语义即可,例如,这里可以先执行 (3)(4)(6), 再执行 (5)(6)(7) ,这都是不影响最终结果和语义的,怎么调整都可以。更甚至对于这样的顺序操作 A. 从内存读取数值到缓存 B. 执行得到结果并放入缓存 C. 将缓存数据刷新到内存,这么几步操作都有可能被编译器给颠倒执行,本来正常应该 ABC 的顺序,最后真正执行的可能是 ACB 的顺序。

对于只在线程内执行的操作和访问的变量来讲,上面的几点都不会有问题,而对于会在多线程中来访问和操作的变量来说上面的优化可能会变成了灾难。

为了解决资源争用的问题, java 引入了 synchornized 关键字,同时它还有另外一层语义,那就是解决了值可见性问题。

Synchornized 关键字保证了:

1、  在进入同步块时,失效缓存,强制从内存读取最新值。

2、  在退出同步块时,将缓存值强制刷到内存中。

以上两点保证了同一个监视器保护下的多个线程都可以看到最新值,而不会读取到过期值,如果线程 A 进入同步块,执行后得到的所有共享变量值,在它退出后,对紧接着进入同一个同步块的线程 B 都是可见的,即线程 B 可以保证读取到线程 A 在同步块中计算到的共享变量的最新值。

在 java 中还定义另一个关键字,也一样可以保证变量的在跨线程中的可见性,那就是 volatile ,它保证读写直接在主存而不是寄存器或者本地处理器缓存中进行。即使用 volatile 修改的变量可以保证在其他线程中读取该变量时可以读取到,例如,如果上面 (2) 中 b 加上 volatile 关键字,那么在线程 B 中就可以立马看到该变量修改的最新值。

如果既没有使用 synchornized ,也没有使用 volatile 的共享资源,那么在 java 中是不保证线程之间对最新值是可见的。

上面还谈到了编译器对内存操作重排序的问题,这有什么影响呢?看如下代码,

char[] config; (8)boolean initialized = false; // In Thread A (9)config = readConfigFile(fileName); (10)initialized = true; // In Thread B while (!initialized) sleep(); // use config

原始想法是线程A 如果完成了对config 的初始化,设置initialized 为true表示初始化完成,B 线程如果检测到初始化完成,则执行use config 。然而这段代码可能并不会像我们想的那样运行,前面说过了,有可能线程B 永远都看不到initialized=true 的那一天,因为这里没有任何保证线程B能够看到initialized读取到最新值, 如果initialized 加上volatile 关键字会怎么样呢?将(8) 修改成 volatile boolean initialized = false; 就可以保证线程B 可以看到initialized 的最新值。在JDK1.5之前,解决了这个可见性问题,但是又有一个问题出现了,因为JDK1.5之前编译器对 volatile 变量的读和写不能与对其他 volatile 变量的读和写一起重新排序,但是它们仍然可以与对不是 volatile 变量的读写一起重新排序,意思是说,这里 (9) 和 (10) 的执行顺序有可能被编译器给颠倒了,此时如果线程 B 检测到 initialized 为true ,准备执行config 时,却因为config 没有被初始化导致代码出现严重错误,杯具。可见,这里编译器调换了执行顺序对于多线程来说有时候是多么可怕。但是在JDK1.5 中, volatile 又增加了一个语义,那就是申明了 volatile 的变量告诉编译器不能和其他非 volatile 变量一起排序,同时volatile 变量自身的所有内存操作也必须按照顺序执行,不能颠倒。因此 volatile 的变量其实是关闭了编译器对其的优化。

前面也讲了,在线程内编译器对操作进行排序优化,只要其中不要涉及到公共资源的操作,并不会引起什么问题,但是一旦进行了排序,而我们在大多数时候又无法预料线程与线程之间的操作执行顺序,就可能会引起程序 crash.

在 java 中,新的 java 内存模型定义了一部分线程与线程之间操作的执行顺序,叫做 happen-before ,它保证只要满足 happen-before 关系,那么后面的操作可以看到前面操作的结果。

  • 线程内的每个操作happen-before稍后按程序顺序传入的该线程中的每个操作。
  • 一个解除锁监视器的(synchronized阻塞或方法退出)happen-before相同监视器的每个后续锁(synchronized
    阻塞或方法进入)。并且因为 happen-before 关系是可传递的,所以解除锁定之前的线程的所有操作 happen-before 锁定该监视器的任何线程后续的所有操作。
  • 写入volatile字段happen-before每个后续读取相同字段。volatile字段的读取和写入与进入和退出监视器具有相似的内存一致性效果,但不需要互斥锁。
  • 在线程上调用start() happen-before 在启动的线程中的所有操作。
  • 线程中的所有操作 happen-before 从该线程上的 join 成功返回的任何其他线程。

当有一个变量被多个线程读、被至少一个线程写、并且读和写不是按 hanppens-before 关系排序的时,程序就称为有 数据争用 ,因而不是一个 “ 正确同步 ” 的程序。

明白了以上几点,就可以解释一个经典 DCL 问题,例如:

public class Singleton { private static Singleton instance=null; public static Singleton getInstance() { if (instance == null) { synchronized(Singleton.class) { //1 if (instance == null) //2 instance = new Singleton(); //3 } } return instance; } }

 这段代码有问题么?标准的double check.instance = new Singleton()根据以上几点分析,
可能执行执行了下列伪代码: 
mem = allocate();             //Allocate memory for Singleton object.
instance = mem;               //Note that instance is now non-null, but
//has not been initialized.
ctorSingleton(instance);      //Invoke constructor for Singleton passing
//instance. 

注意,当线程A执行到instance = mem时,线程B 正好执行到外部的instance == null,此时,这个引用已经不为null,但是这个statnce
还没有构造完成,线程B的操作立即返回使用该instance,这是不安全的。这是从操作次序被重新排序得到的分析结果,从另外happen-before的角度来看,这里多个线程操作共享变量instance之间并没有明显的happen-before关系,因此多个线程对instanc的读写可能发生不可见的情况。instance变量申明为volatile即可,既保证了可见性,又保证了操作不会被排序。然而,使用volatile来实现毕竟有性能损耗,因此如果要实现单例,完全可以避免使用DCL,而采用static方式。例如:
要解决上面提到的问题,将该

public class Singleton { private static class Singleton Holder{ private static Singleton instance = new Singleton (); } public static Singleton getInstance(){ return SingletonHolder.instance ; } }

这种实现方式既保证了足够的惰性,又避免了同步或者保持可见性带来的性能损耗。

71java并发编程不得不知道的几件事相关推荐

  1. 关于多线程编程您不知道的 5 件事 有关高性能线程处理的微妙之处

    虽然很少有 Java™ 开发人员能够忽视多线程编程和支持它的 Java 平台库,更少有人有时间深入研究线程.相反地,我们临时学习线程,在需要时向我们的工具箱添加新的技巧和技术.以这种方式构建和运行适当 ...

  2. 关于ERP系统,你可能不知道的10件事

    谈到ERP系统,大多数人只是考虑ERP日常管理的核心功能,即财务.销售.采购.库存.生产和分销.保持对这些关键领域的控制对任何企业的成功都是不可或缺的.但这些只是冰山一角,如果深入挖掘ERP系统,你可 ...

  3. 佳能eosr控制环能否计算机控制,关于全画幅微单相机 你可能不知道的十件事

    01关于微单相机你不知道的十件事 2018年之前,全画幅微单领域没有竞争,索尼一家独大,徕卡只走奢侈路线.为了满足专业摄影师和资深发烧玩家更多的使用需求,佳能.尼康和松下纷纷加入战局,在2018年开启 ...

  4. 阴阳师服务器维护结界卡暂停吗,阴阳师BUFF暂停功能你不能不知道的六件事

    阴阳师BUFF暂停功能已经正式上线啦,哪些种类的加成可以暂停?在哪里找暂停按钮?暂停有时间限制吗?有次数限制吗?关于BUFF暂停你不能不知道的六件事! 一.在哪里暂停? 在庭院处点击头像右边的&quo ...

  5. 关于 Java Collections API 您不知道的 5 件事--转

    第 1 部分 http://www.ibm.com/developerworks/cn/java/j-5things2.html 对于很多 Java 开发人员来说,Java Collections A ...

  6. 关于PHP你可能不知道的10件事

    小编之前也曾报导过PHP开发人员容易忽略的几点精华,除了一些精华技术方法外,很多细微之处也是程序员们容易忽略的,下面我们为您总结了10个关于PHP你可能不知道的事情. 关于PHP更多内容,欢迎访问:P ...

  7. 我的世界java版移除猪灵了吗_我的世界:猪灵拥有三种类型?关于猪灵,你可能不知道的7件事...

    猪灵是1.16下界更新第二个快照版本20w07a加入的下界新生物,虽然上线时间已经有两个月之久,但对于大部分国内玩家来说却还是一个陌生的存在.为了让玩家更加的了解猪灵,迷恋整理了7件,你可能不知道的关 ...

  8. 关于 java.util.concurrent 您不知道的 5 件事--转

    第 1 部分 http://www.ibm.com/developerworks/cn/java/j-5things4.html Concurrent Collections 是 Java™ 5 的巨 ...

  9. 关于 Java 对象序列化您不知道的 5 件事

    数年前,当和一个软件团队一起用 Java 语言编写一个应用程序时,我体会到比一般程序员多知道一点关于 Java 对象序列化的知识所带来的好处. 关于本系列 您觉得自己懂 Java 编程?事实上,大多数 ...

最新文章

  1. 开启注册丨全国社交媒体处理大会(SMP 2020)召开,98场报告日程全公开
  2. 图灵访谈系列之一:陈世欣谈产品经理与社区
  3. 循序渐进Python3(二) -- 数据类型
  4. Zabbix服务器性能优化记录
  5. Leetcode刷题(6)有效的括号
  6. 2、组件注册-@Configuration@Bean给容器中注册组件
  7. ggplot2 | ggplot2作图语法入门
  8. 92.与上游服务建立连接
  9. 专升本计算机专业是理工类吗,理工类专接本有些专业
  10. java案例教程_JAVA基础案例教程 PDF 下载
  11. 文华财经彩波均线主图指标公式(指标公式源码)破解加密
  12. 前端常用PS技巧总结之将图片上的LOGO(水印)去掉
  13. 第三方应用微信登录接口
  14. git(2)Git 基础
  15. Springcloud笔记超级详细
  16. 幕后产品_版本控制:幕后
  17. CSS模块化基本思想
  18. 经典编程书籍大全-python
  19. Anntec ZKUXFT XT2 FGPA卡DPDK使用方法
  20. 2022年上海落户全部方式!落户上海政策变化及条件汇总!

热门文章

  1. 房地产数据-python爬虫+数据可视化
  2. [Darktable]dt源码分析(未完)
  3. qt控制程序打开记事本_基于QT记事本应用程序开发.doc
  4. python的多线程使用setDaemon有什么意义?
  5. 数组双指针和数组窗口
  6. 关于wifi portal认证--为浏览器添加wifi认证功能
  7. 基于python的数字图像处理--学习笔记(三)
  8. MySQL备份恢复之mysqldump备份_SQL语句模式
  9. Resistors in Parallel(找规律+大数)
  10. Java进行图像缩放