Java 存储模型和共享对象详解
作者:lqh 发布时间:2023-11-16 14:48:46
Java 存储模型和共享对象详解
很多程序员对一个共享变量初始化要注意可见性和安全发布(安全地构建一个对象,并其他线程能正确访问)等问题不是很理解,认为Java是一个屏蔽内存细节的平台,连对象回收都不需要关心,因此谈到可见性和安全发布大多不知所云。其实关键在于对Java存储模型,可见性和安全发布的问题是起源于Java的存储结构。
Java存储模型原理
有很多书和文章都讲解过Java存储模型,其中一个图很清晰地说明了其存储结构:
由上图可知, jvm系统中存在一个主内存(Main Memory或Java Heap Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。 每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。
这个存储模型很像我们常用的缓存与数据库的关系,因此由此可以推断JVM如此设计应该是为了提升性能,提高多线程的并发能力,并减少线程之间的影响。
Java存储模型潜在的问题
一谈到缓存, 我们立马想到会有缓存不一致性问题,就是说当有缓存与数据库不一致的时候,就需要有相应的机制去同步数据。同理,Java存储模型也有这个问题,当一个线程在自己工作内存里初始化一个变量,当还没来得及同步到主存里时,如果有其他线程来访问它,就会出现不可预知的问题。另外,JVM在底层设计上,对与那些没有同步到主存里的变量,可能会以不一样的操作顺序来执行指令,举个实际的例子:
public class PossibleReordering {
static int x = 0, y = 0;
static int a = 0, b = 0;
public static void main(String[] args)
throws InterruptedException {
Thread one = new Thread(new Runnable() {
public void run() {
a = 1;
x = b;
}
});
Thread other = new Thread(new Runnable() {
public void run() {
b = 1;
y = a;
}
});
one.start(); other.start();
one.join(); other.join();
System.out.println("( "+ x + "," + y + ")");
}
}
由于,变量x,y,a,b没有安全发布,导致会不以规定的操作顺序来执行这次四次赋值操作,有可能出现以下顺序:
出现这个问题也可以理解,因为既然这些对象不可见,也就是说本应该隔离在各个线程的工作区内,那么对于有些无关顺序的指令,打乱顺序执行在JVM看来也是可行的。
因此,总结起来,会有以下两种潜在问题:
缓存不一致性
重排序执行
解决Java存储模型潜在的问题
为了能让开发人员安全正确地在Java存储模型上编程,JVM提供了一个happens-before原则,有人整理得非常好,我摘抄如下:
在程序顺序中, 线程中的每一个操作, 发生在当前操作后面将要出现的每一个操作之前.
对象监视器的解锁发生在等待获取对象锁的线程之前.
对volitile关键字修饰的变量写入操作, 发生在对该变量的读取之前.
对一个线程的 Thread.start() 调用 发生在启动的线程中的所有操作之前.
线程中的所有操作 发生在从这个线程的 Thread.join()成功返回的所有其他线程之前.
有了原则还不够,Java提供了以下工具和方法来保证变量的可见性和安全发布:
使用 synchronized来同步变量初始化。此方式会立马把工作内存中的变量同步到主内存中
使用 volatile关键字来标示变量。此方式会直接把变量存在主存中而不是工作内存中
final变量。常量内也是存于主存中
另外,一定要明确只有共享变量才会有以上那些问题,如果变量只是这个线程自己使用,就不用担心那么多问题了
搞清楚Java存储模型后,再来看共享对象可见性和安全发布的问题就较为容易了
共享对象的可见性
当对象在从工作内存同步到主内存之前,那么它就是不可见的。若有其他线程在存取不可见对象就会引发可见性问题,看下面一个例子:
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
public void run() {
while (!ready)
Thread.yield();
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
按照正常逻辑,应该会输出42,但其实际结果会非常奇怪,可能会永远没有输出(因为ready为false),可能会输出0(因为重排序问题导致ready=true先执行)。再举一个更为常见的例子,大家都喜欢用只有set和get方法的pojo来设计领域模型,如下所示:
@NotThreadSafe
public class MutableInteger {
private int value;
public int get() { return value; }
public void set(int value) { this.value = value; }
}
但是,当有多个线程同时来存取某一个对象时,可能就会有类似的可见性问题。
为了保证变量的可见性,一般可以用锁、 synchronized关键字、 volatile关键字或直接设置为final
共享变量发布
共享变量发布和我们常说的发布程序类似,就是说让本属于内部的一个变量变为一个可以被外部访问的变量。发布方式分为以下几种:
将对象引用存储到公共静态域
初始化一个可以被外部访问的对象
将对象引用存储到一个集合里
安全发布和保证可见性的方法类似,就是要同步发布动作,并使发布后的对象可见。
线程安全
其实当我们把这些变量封闭在本线程内访问,就可以从根本上避免以上问题,现实中存在很多例子通过线程封闭来安全使用本不是线程安全的对象,比如:
swing的可视化组件和数据模型对象并不是线程安全的,它通过将它们限制到swing的事件分发线程中,实现线程安全
JDBC Connection对象没有要求为线程安全,但JDBC的存取模式决定了一个Connection只会同时被一个线程使用
ThreadLocal把变量限制在本线程中共享
感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!
来源:http://blog.csdn.net/qq_35101189/article/details/59110694


猜你喜欢
- 正则表达式是一种描述词素的重要表示方法。虽然正则表达式并不能表达出所有可能的模式(例如“由等数量的 a 和 b 组成的字符串”),但是它可以
- 微信公众号提供了微信支付、微信优惠券、微信H5红包、微信红包封面等等促销工具来帮助我们的应用拉新保活。但是这些福利要想正确地发放到用户的手里
- springboot上传文件大小的配置我这里记录两种,一种是设置在配置文件里只有两行代码,一种是加个Bean首先第一种:applicatio
- java 代码块与静态代码块加载顺序public abstract class ClassLoadingTest {public stati
- 前言公司最近在开发中遇到一个问题,在弄帖子的发布与回复问题,然后再iOS端和Android端添加表情的时候都会出错Caused by: ja
- springcloud集成nacos遇到的问题1.获取不到配置文件信息有时候新建了配置文件后浏览器访问发现获取不到里面的值,原来spring
- 随着kotlin在Android开发领域越来越火,协程在各个项目中的应用也逐渐变得广泛但是协程到底是什么呢?协程其实是个古老的概念,已经非常
- 引言本文是关于Spark优化性能与内存使用的最佳实践,翻译整理自Tuning - Spark 3.3.2 Documentation。由于s
- Activity回顾activity是android程序中最重要的组件之一,它是用户与android用户交互的主要组件,类似于桌面程序的图形
- 效果如下: 布局文件activity_main.xml如下:<RelativeLayout xmlns:android="h
- 周末了,觉得我还有很多作业没有写,针对目前大家对OOM的类型不太熟悉,那么我们来总结一下各种OOM出现的情况以及解决方法。我们把各种OOM的
- 目录一、概述二、环境配置及代码步骤1. 环境配置2. 代码步骤一、概述PDF打印小册子是指将PDF格式文档在打印成刊物前需要提前进行的页面排
- 1 数据响应  数据响应一般分为两种:页面响应和数据响应,一般来说页面响应是用来开发一些单体项目(也就是
- 前言在RequestMappingHandlerAdapter对request进行了适配,并且调用了目标handler之后,其会返回一个Mo
- 1.概述数据库开发一直是JAVA开发的核心之一,作为现在JAVA EE的基石框架,Spring Boot自身携带了一个JDBCTemplat
- 目录第一种第二种第三种随机数的产生在一些代码中很常用,也是我们必须要掌握的。而java中产生随机数的方法主要有三种:第一种:new Rand
- 我就废话不多说了,大家还是直接看代码吧~import java.util.concurrent.ExecutorService; impor
- 背景项目中我们经常会用搜索功能,普通的搜索我们可以用一个SQL的like也能实现匹配,但是搜索的核心需求是全文匹配,对于全文匹配,数据库的索
- 本文实例讲述了C#中的try catch finally用法。分享给大家供大家参考。具体分析如下:try中的程序块是有可能发生错误的程序块,
- 在Activity类的子类中直接复写下面三个方法://复写onCreateOptionsMenu()方法,弹出菜单栏