探索并发编程(三)------Java存储模型和共享对象

很多程序员对一个共享变量初始化要注意可见性和安全发布(安全地构建一个对象,并其他线程能正确访问)等问题不是很理解,认为Java是一个屏蔽内存细节的平台,连对象回收都不需要关心,因此谈到可见性和安全发布大多不知所云。其实关键在于对Java存储模型,可见性和安全发布的问题是起源于Java的存储结构。

Java存储模型原理

有很多书和文章都讲解过Java存储模型,其中一个图很清晰地说明了其存储结构:

由上图可知, jvm系统中存在一个主内存(Main Memory或Java Heap Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。 每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。

这个存储模型很像我们常用的缓存与数据库的关系,因此由此可以推断JVM如此设计应该是为了提升性能,提高多线程的并发能力,并减少线程之间的影响。

Java存储模型潜在的问题

一谈到缓存 我们立马想到会有缓存不一致性问题,就是说当有缓存与数据库不一致的时候,就需要有相应的机制去同步数据。同理,Java存储模型也有这个问题,当一个线程在自己工作内存里初始化一个变量,当还没来得及同步到主存里时,如果有其他线程来访问它,就会出现不可预知的问题。另外,JVM在底层设计上,对与那些没有同步到主存里的变量,可能会以不一样的操作顺序来执行指令,举个实际的例子:

由于,变量x,y,a,b没有安全发布,导致会不以规定的操作顺序来执行这次四次赋值操作,有可能出现以下顺序:

出现这个问题也可以理解,因为既然这些对象不可见,也就是说本应该隔离在各个线程的工作区内,那么对于有些无关顺序的指令,打乱顺序执行在JVM看来也是可行的。

因此,总结起来,会有以下两种潜在问题:

  • 缓存不一致性
  • 重排序执行

解决Java存储模型潜在的问题

为了能让开发人员安全正确地在Java存储模型上编程,JVM提供了一个happens-before原则,有人整理得非常好,我摘抄如下:

  • 在程序顺序中, 线程中的每一个操作, 发生在当前操作后面将要出现的每一个操作之前.
  • 对象监视器的解锁发生在等待获取对象锁的线程之前.
  • 对volitile关键字修饰的变量写入操作, 发生在对该变量的读取之前.
  • 对一个线程的 Thread.start() 调用 发生在启动的线程中的所有操作之前.
  • 线程中的所有操作 发生在从这个线程的 Thread.join()成功返回的所有其他线程之前.

有了原则还不够,Java提供了以下工具和方法来保证变量的可见性和安全发布:

  • 使用 synchronized来同步变量初始化。此方式会立马把工作内存中的变量同步到主内存中
  • 使用 volatile关键字来标示变量。此方式会直接把变量存在主存中而不是工作内存中
  • final变量。常量内也是存于主存中

另外,一定要明确只有共享变量才会有以上那些问题,如果变量只是这个线程自己使用,就不用担心那么多问题了

搞清楚Java存储模型后,再来看共享对象可见性和安全发布的问题就较为容易了

共享对象的可见性

当对象在从工作内存同步到主内存之前,那么它就是不可见的。若有其他线程在存取不可见对象就会引发可见性问题,看下面一个例子:

按照正常逻辑,应该会输出42,但其实际结果会非常奇怪,可能会永远没有输出(因为ready为false),可能会输出0(因为重排序问题导致ready=true先执行)。再举一个更为常见的例子,大家都喜欢用只有set和get方法的pojo来设计领域模型,如下所示:

但是,当有多个线程同时来存取某一个对象时,可能就会有类似的可见性问题。

为了保证变量的可见性,一般可以用锁、 synchronized关键字、 volatile关键字或直接设置为final

共享变量发布

共享变量发布和我们常说的发布程序类似,就是说让本属于内部的一个变量变为一个可以被外部访问的变量。发布方式分为以下几种:

  • 将对象引用存储到公共静态域
  • 初始化一个可以被外部访问的对象
  • 将对象引用存储到一个集合里

安全发布和保证可见性的方法类似,就是要同步发布动作,并使发布后的对象可见。

线程安全

其实当我们把这些变量封闭在本线程内访问,就可以从根本上避免以上问题,现实中存在很多例子通过线程封闭来安全使用本不是线程安全的对象,比如:

  • swing的可视化组件和数据模型对象并不是线程安全的,它通过将它们限制到swing的事件分发线程中,实现线程安全
  • JDBC Connection对象没有要求为线程安全,但JDBC的存取模式决定了一个Connection只会同时被一个线程使用
  • ThreadLocal把变量限制在本线程中共享


  • 3
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 21
    评论
真正的Addison-Wesley 出品的Java Concurrency in Practice 中文版 目录回到顶部↑ 代码清单 序 第1章 介绍 1.1 并发的(非常)简短历史 1.2 线程的优点 1.3 线程的风险 1.4 线程无处不在 第1部分 基础 第2章 线程安全 2.1 什么是线程安全性 2.2 原子性 2.3 锁 2.4 用锁来保护状态 2.5 活跃度与性能 第3章 共享对象 3.1 可见性 3.2 发布和逸出 3.3 线程封闭 3.4 不可变性 3.5 安全发布 . 第4章 组合对象 4.1 设计线程安全的类 4.2 实例限制 4.3 委托线程安全 4.4 向已有的线程安全类添加功能 4.5 同步策略的文档化 第5章 构建块 5.1 同步容器 5.2 发容器 5.3 阻塞队列和生产者一消费者模式 5.4 阻塞和可中断的方法 5.5 synchronizer 5.6 为计算结果建立高效、可伸缩的高速缓存 第2部分 构建并发应用程序 第6章 任务执行 6.1 在线程中执行任务 6.2 executor 框架 6.3 寻找可强化的并行性 第7章 取消和关闭 7.1 任务取消 7.2 停止基于线程的服务 7.3 处理反常的线程终止 7.4 jvm关闭 第8章 应用线程池 8.1 任务与执行策略问的隐性耦合 8.2 定制线程池的大小 8.3 配置threadpoolexecutor 8.4 扩展threadpoolexecutor 8.5 并行递归算法 第9章 gui应用程序 9.1 为什么gui是单线程化的 9.2 短期的gui任务 9.3 耗时gui任务 9.4 共享数据模型 9.5 其他形式的单线程子系统 第3部分 活跃度,性能和测试 第10章 避免活跃度危险 第11章 性能和可伸缩性 第12章 测试并发程序 第4部分 高级主题 第13章 显示锁 第14章 构建自定义的同步工具 第15章 原子变量与非阻塞同步机制 第16章 java存储模型 附录a 同步annotation 参考文献 索引

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值