首页 > java并发编程实战:第十六章----Java内存模型

java并发编程实战:第十六章----Java内存模型

一、什么是内存模型,为什么要使用它

如果缺少同步,那么将会有许多因素使得线程无法立即甚至永远看到一个线程的操作结果

  • 编译器把变量保存在本地寄存器而不是内存中
  • 编译器中生成的指令顺序,可以与源代码中的顺序不同
  • 处理器采用乱序或并行的方式来执行指令
  • 保存在处理器本地缓存中的值,对于其他处理器是不可见

在单线程中,只要程序的最终结果与在严格串行环境中执行的结果相同,那么上述所有操作都是允许的

在多线程中,JVM通过同步操作来找出这些协调操作将在何时发生

JMM规定了JVM必须遵循一组最小保证,这组保证规定了对变量的写入操作在何时将对其他线程可见

1、平台的内存模型

每个处理器都拥有自己的缓存,并且定期地与主内存进行协调,在不同的处理器架构中提供了不同级别的缓存一致性,即允许不同的处理器在任意时刻从同一个存储位置上看到不同的值。JVM通过在适当的位置上插入内存栅栏来屏蔽在JMM与底层平台内存模型之间的差异。Java程序不需要指定内存栅栏的位置,而只需通过正确地使用同步来找出何时将访问共享状态

2、重排序

  各种使操作延迟或者看似乱序执行的不同原因,都可以归为重排序,内存级的重排序会使程序的行为变得不可预测

1 Thread one = new Thread(new Runnable() {
2             public void run() {
3                 a = 1;
4                 x = b;
5             }
6         });

上述代码也会有以下结果:

3、Java内存模式简介

  Java内存模型是通过各种操作来定义的,包括变量的读/写操作,监视器的加锁和释放操作,以及线程的启动和合并操作

  JMM为程序中所有的操作定义了一个偏序关系,称为Happens-Before,使在正确同步的程序中不存在数据竞争(缺乏Happens-Before关系,那么JVM可以对它们任意地重排序)

  • 程序顺序规则。如果程序中操作A在操作B之前,那么在线程中A操作将在B操作之前执行
  • 监视器锁规则。在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前执行。(显式锁和内置锁在加锁和解锁等操作上有着相同的内存语义) 
  • volatile变量规则。对volatile变量的写入操作必须在对该变量的读操作之前执行。(原子变量与volatile变量在读操作和写操作上有着相同的语义) 
  • 线程启动规则。在线程上对Thread.start的调用必须在该线程中执行任何操作之前执行
  • 线程结束规则。线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行,或者从Thread.join中成功返回,或者在调用Thread.isAlive时返回false
  • 中断规则。当一个线程在另一个线程上调用interrupt时,必须在被中断线程检测到interrupt调用之前执行(通过抛出InterruptException,或者调用isInterrupted和interrupted)
  • 终结器规则。对象的构造函数必须在启动该对象的终结器之前执行完成
  • 传递性。如果操作A在操作B之前执行,并且操作B在操作C之前执行,那么操作A必须在操作C之前执行。

4、借助同步

”借助(Piggyback)“现有同步机制的可见性属性,对某个未被锁保护的变量的访问操作进行排序(不希望给对象加锁,而又想维护它的顺序)

 

Happens-Before排序包括:

  • 将一个元素放入一个线程安全容器的操作将在另一个线程从该容器中获得这个元素的操作之前执行
  • 在CountDownLatch上的倒数操作将在线程从闭锁上的await方法返回之前执行
  • 释放Semaphore许可的操作将在从该Semaphore上获得一个许可之前执行
  • Future表示的任务的所有操作将在从Future.get中返回之前执行
  • 向Executor提交一个Runnable或Callable的操作将在任务开始执行之前执行
  • 一个线程到达CyclicBarrier或Exchange的操作将在其他到达该栅栏或交换点的线程被释放之前执行。如果CyclicBarrier使用一个栅栏操作,那么到达栅栏的操作将在栅栏操作之前执行,而栅栏操作又会在线程从栅栏中释放之前执行

 

二、发布

造成不正确发布的真正原因:"发布一个共享对象"与"另一个线程访问该对象"之间缺少一种Happens-Before的关系

1、不安全的发布

  除了不可变对象以外,使用被另一个线程初始化的对象通常都是不安全的,除非对象的发布操作是在使用该对象的线程开始使用之前执行

 

 1 public class UnsafeLazyInitialization {2     private static Object resource;3     4     public static Object getInstance(){5         if (resource == null){6             resource = new Object(); //不安全的发布7         }8         return resource;9     }
10 }

 

原因一:线程B看到了线程A发布了一半的对象

原因二:即使线程A初始化Resource实例之后再将resource设置为指向它,线程B仍可能看到对resource的写入操作将在对Resource各个域的写入操作之前发生。因为线程B看到的线程A中的操作顺序,可能与线程A执行这些操作时的顺序并不相同

2、安全发布

例:BlockingQueue的同步机制保证put在take后执行,A线程放入对象能保证B线程取出时是安全的

  借助于类库中现在的同步容器、使用锁保护共享变量、或都使用共享的volatile类型变量,都可以保证对该变量的读取和写入是按照happens-before排序的

  happens-before事实上可以比安全发布承诺更强的可见性与排序性

3、安全初始化模式

方式一:加锁保证可见性与排序性,存在性能问题

 1 public class UnsafeLazyInitialization {2     private static Object resource;3     4     public synchronized static Object getInstance(){5         if (resource == null){6             resource = new Object(); //不安全的发布7         }8         return resource;9     }
10 }

 

方式二:提前初始化,可能造成浪费资源

1 public class EagerInitialization {
2     private static Object resource = new Object();
3     public static Object getInstance(){
4         return resource;
5     }
6 }

方式三:延迟初始化,建议

1 public class ResourceFactory {
2     private static class ResourceHolder{
3         public static Object resource = new Object();
4     }
5     public static Object getInstance(){
6         return ResourceHolder.resource;
7     }
8 }

方式四:双重加锁机制,注意保证volatile类型,否则出现一致性问题

 1 public class DoubleCheckedLocking {2     private static volatile Object resource;3     public static Object getInstance(){4         if (resource == null){5             synchronized (DoubleCheckedLocking.class){6                 if (resource == null){7                     resource = new Object(); 8                 }9             }
10         }
11         return resource;
12     }
13 }

 

三、初始化过程中的安全性

  • 如果能确保初始化过程的安全性,被正确构造的不可变对象在没有同步的情况下也能安全地在多个线程之间共享
  • 如果不能确保初始化的安全性,一些本应为不可变对象的值将会发生改变

初始化安全性只能保证通过final域可达的值从构造过程完成时可见性。对于通过非final域可达的值,或者在构成过程完成后可能改变的值,必须采用同步来确保可见性

转载于:https://www.cnblogs.com/linghu-java/p/9053155.html

更多相关:

  • 一次手贱更新了上面这个驱动,结果: Fn+F2/F3不能控制亮度win+X弹出来的下面这个窗口“显示器亮度”不见了 电源选项里面底部“屏幕亮度”也不见了 我研究了三天,各种骚操作都试过了,最后找到解决方案: win+R 输入 devmgmt.msc 回车,按照下图操作,瞬间亮度恢复正常 ...

  • 第一种:需要刷新当前页面重复执行的操作——使用场景:刷点击率 、秒杀活动、抢沙发   //自动化循环操作方法 var doLoop = function (dom) {dom || (dom = document);//这里写循环操作的代码dom.querySelector("sel").innerHTML = "舒工的骚操作!!...

  • Ubuntu常用操作外观操作修改应用icon图标终端美化内存管理内存查询分区管理日志清理操作执行自动执行多条命令...

  • 文章目录前言工业级 LRU Cache1. 基本架构2. 基本操作2.1 insert 操作2.2 高并发下 insert 的一致性/性能 保证2.3 Lookup操作2.4 shard 对 cache Lookup 性能的影响2.4 Erase 操作2.5 内存维护3. 优化...

  • 最近利用vim做一些文本处理时 发现vim 支持的键盘宏是一个好东西啊,高效优雅得处理大量需要重复性操作的文本,让人爱不释手!!! 希望接下来对键盘宏的分享能够实际帮助到大家。 后文中描述的一些vim操作会汇集成指令字符串,方便大家参考。 1. 场景描述 在分享键盘宏之前,先看这样一个文本操作需求: 对如下文本增加ma...

  • 多线程有什么好处?提高CPU的利用率,更好地利用系统资源,使用Monitor类可以同步静态/实例化的方法的全部代码或者部分代码段,使用不同的同步类创建自己的同步机制。多线程指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程提升整体处理性能。多线程是指程序中包含多个执行流,即...

  • Step1:在界面主函数的构造函数中初始化多线程 auto mythread = new QThread(); //新建connect(mythread , &QThread::finished, mythread, &QObject::deleteLater);//线程运行结束后释放内存object1->moveToThread...

  • 一、thread的基本用法 参见C++使用thread类多线程编程 。 二、类外使用多线程,访问类的成员 这几种方式,新建线程都是在类外,然后通过把友元函数或者成员函数作为thread参数。 #include #include #include using namesp...

  • 本博文是根据中科大信息学院谭立湘老师的课件加上自己的理解整理出来的 ************************************************************************************ NVIDIA在2007年推出CUDA这个统一计算架构 CUDA的基本思想是支持大量的线程级并...

  • 一、parallel communication patterns   并行通信模式 Map:映射,在特定的位置读取和写入。 Gather:收集,从多个不同的位置读入,写入一个位置。 Scatter:分发,写入多个位置。 Transpose转置 结构数组缩写为AOS,数组结构缩写为SOA 转置运算是指任务重新排序内存中的数...

  • 在某些情况下(例如通过网络访问数据),常常不希望程序卡住而占用太多时间以至于造成界面假死。在这时、我们可以通过Thread、Thread + Invoke(UI)或者是 delegate.BeginInvoke 来避免界面假死,但是这样做时,某些代码或者是某个方法的执行超时的时间还是无法操控的。那么我们又是否有一种比较通用的方法、来设...

  • 传参 如果程序执行的时候需要加入参数,如 ./sample aa bb 使用 gdb 的时候可以使用如下方式 gdb --args ./sample aa bb 执行 启动 gdb 之后,直接使用 r,就是 run 的意思,或者可以使用 b 加一个断点进行调试。处理信号 如果在使用的时候,遇到类似下面的报错 Threa...

  • 进程的图文形象表示 阮一峰–进程与线程的一个简单解释 多进程实质 现在,多核CPU已经非常普及了,但是,即使过去的单核CPU,也可以执行多任务。由于CPU执行代码都是顺序执行的,那么,单核CPU是怎么执行多任务的呢? 答案就是操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务...

  • redis 事物: Redis 事物的实现: 首先 wath监控键值 myKey开启批量执行 multi,执行命令入列,执行 exec 。如果监控的键值mykey 没有被修改过,则exec 中批量执行的命令成功,否则执行失败。无论执行成功与否,都会执行取消wath的执行  Redis multi 批量执行,是先把批量中的命令放入队列...

  • 一、准备工作: 1.登录服务器,切换到root用户(su - root,然后输入密码,按enter),进入根目录:cd / 2.进入要安装jdk的目录,自己可以创建一个java目录,执行命令如下: cd /usr/local/ mkdir java 二、下载安装包 1.打开官网下载界面:https://www.oracle.com/...