1. 并发编程的两个关键问题

并发是让多个线程同时执行,若线程之间是独立的,那并发实现起来很简单,各自执行各自的就行;但往往多条线程之间需要共享数据,此时在并发编程过程中就不可避免要考虑两个问题:通信 与 同步。

  • 通信
    通信是指消息在两条线程之间传递。
    既然要传递消息,那接收线程 和 发送线程之间必须要有个先后关系,此时就需要用到同步。通信和同步是相辅相成的。

  • 同步
    同步是指,控制多条线程之间的执行次序。

2. 通信的方式

2.1 通信方式的种类

线程之间的通信一共有两种方式:共享内存 和 消息传递。

  • 共享内存
    共享内存指的是多条线程共享同一片内存,发送者将消息写入内存,接收者从内存中读取消息,从而实现了消息的传递。
    但这种方式有个弊端,即需要程序员来控制线程的同步,即线程的执行次序。

这种方式并没有真正地实现消息传递,只是从结果上来看就像是将消息从一条线程传递到了另一条线程。

  • 消息传递
    顾名思义,消息传递指的是发送线程直接将消息传递给接收线程。
    由于执行次序由并发机制完成,因此不需要程序员添加额外的同步机制,但需要声明消息发送和接收的代码。

综上所述:对于共享内存的通信方式,需要进行显示的同步,隐式的通信;
而对于消息传递的通信方式,需要隐式的同步,显示的通信。

2.2 Java使用的通信方式

Java使用共享内存的方式实现多线程之间的消息传递。因此,程序员需要写额外的代码用于线程之间的同步。

PS:其实共享内存的方式从实现过程来看,跟消息传递一点关系都没有:一条线程将消息存入共享内存,另一条线程从共享内存中读这条消息。
但从结果来看,整个过程就好像是一条消息被从线程A传递到了线程B。
这种方式之所以能实现消息传递,依托于两点:

  • 必须有一片共享的内存
  • 必须要实现多线程的同步

3. Java多线程的内存模型(简化版)

所有线程都共享一片内存,用于存储共享变量;
此外,每条线程都有各自的存储空间,存储各自的局部变量、方法参数、异常对象。

4. volatile是什么?

Java采用共享内存的方式实现消息传递,而共享内存需要依托于同步。Java提供了synchronized、volatile关键字实现同步。此外volatile关键字还拥有一些额外的功能。

5. volatile的使用

在成员变量前加上该关键字即可。

public volatile boolean flag;
  • 1

6. volatile的特性

6.1 重排序

重排序是计算机为了提高程序执行效率而对代码的执行顺序进行调整。你以为代码是一行行顺序执行的,但实际并非如此,重排序详解请移步至:Java并发编程的艺术(二)——重排序

若两行指令之间没有依赖关系,那么计算机可以对他们的顺序进行重排序,但若两行之间的某个变量被volatile修饰后,重排序规则会发生变化。

在以下情况下,即使两行代码之间没有依赖关系,也不会发生重排序:

  • volatile读

    • 若volatile读操作的前一行为volatile读/写,则这两行不会发生重排序
    • volatile读操作和它后一行代码都不会发生重排序
  • volatile写

    • volatile写操作和它前一行代码都不会发生重排序;
    • 若volatile写操作的后一行代码为volatile读/写,则这两行不会发生重排序。

6.2 可见性

什么是内存可见性?

“内存可见性”指的是一条线程修改完一个共享变量后,另一个线程若访问这个变量将会访问到修改后的值。即:一条线程对共享变量的修改,对其他线程立即可见。

但如果未对共享变量采用同步机制,那么共享变量的修改不会对其他线程立即可见。

为什么会出现内存不可见的情况?

通过上文可知,在Java中每条线程都有各自独立的存储空间,此外还有一个所有线程共享的内存空间。
当开启线程时,系统会将共享内存中的所有共享变量拷贝一份到线程专属的存储空间中。接下来该线程在结束前的所有操作都是基于自己的存储空间进行的。因此,若一条线程改变了一个共享变量,仅仅改变的是这条线程专属存储空间中的变量值;此时若其他线程访问这个变量,访问的仍然是先前从共享存储空间读出来的值。

然而我们希望一条线程将某个共享变量修改后,其他线程能立即访问到这个最新的值,而不是失效值。
这时就需要同步机制来解决这个问题。

如何确保共享变量的可见性?

要确保所有共享变量对所有线程是可见的,就需要给所有共享变量使用同步。在Java中你可以选择将共享变量用同步代码块包裹或用volatile修饰共享变量。

为什么volatile能保证共享变量的内存可见性?

volatile修饰了一个成员变量后,这个变量的读写就会比普通变量多一些步骤。

  • volatile变量写
    当被volatile修饰的变量进行写操作时,这个变量将会被直接写入共享内存,而非线程的专属存储空间。

  • volatile变量读
    当读取一个被volatile修饰的变量时,会直接从共享内存中读,而非线程专属的存储空间中读。

通过对volatile变量读写的限制,就能保证线程每次读到的都是最新的值,从而确保了该变量的内存可见性。

volatile变量赠送的附加功能

进行volatile写操作时,不仅会将volatile变量写入共享内存,系统还会将当前线程专属空间中的所有共享变量写入共享内存。
进行volatile读操作时,系统也会一次性将共享内存中所有共享变量读入线程专属空间。
这就意味着,如果普通变量在volatile写操作之前被修改,那么在volatile读操作之后就能正确读到他们。
但是,在volatile写操作之后被修改的普通变量 和 在volatile读操作之前被访问的普通变量 都不具有内存可见性。

6.3 原子性

什么是原子性?

原子性指的是一组操作必须一起完成,中途不能被中断。

volatile能确保long、double读写的原子性

在Java中的所有类型中,有long、double类型比较特殊,他们占据8字节(64比特),其余类型都小于64比特。在32位操作系统中,CPU一次只能读取/写入32位的数据,因此对于64位的long、double变量的读写会进行两步。在多线程中,若一条线程只写入了long型变量的前32位,紧接着另一条线程读取了这个只有“一半”的变量,从而就读到了一个错误的数据。

为了避免这种情况,需要在用volatile修饰long、double型变量。

在内存可见性与原子性上,volatile就相当于是同步的setter和getter函数。但并不具有volatile的重排序规则,同步块只确保同步块内部的指令不发生重排序,并不确保同步块以外的指令的重排序。

PS1:Java中的byte竟然是字节,bit才是比特(位)。
PS2:char和short-2字节、int和float-4字节、long和double-8字节、byte-1字节

QA:在同步块中调用wait函数是否会破坏原子性?

Java并发编程的艺术(三)——volatile的更多相关文章

  1. Java并发编程的艺术(六)——线程间的通信

    多条线程之间有时需要数据交互,下面介绍五种线程间数据交互的方式,他们的使用场景各有不同. 1. volatile.synchronized关键字 PS:关于volatile的详细介绍请移步至:Java ...

  2. 那些年读过的书《Java并发编程实战》和《Java并发编程的艺术》三、任务执行框架—Executor框架小结

    <Java并发编程实战>和<Java并发编程的艺术>           Executor框架小结 1.在线程中如何执行任务 (1)任务执行目标: 在正常负载情况下,服务器应用 ...

  3. Java并发编程底层实现原理 - volatile

    Java语言规范第三版中对volatile的定义如下: Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致性的更新,线程应该确保通过排他锁 单独获得这个变量. volatile有时候 ...

  4. 读《Java并发编程的艺术》(一)

    离开博客园很久了,自从找到工作,到现在基本没有再写过博客了.在大学培养起来的写博客的习惯在慢慢的消失殆尽,感觉汗颜.所以现在要开始重新培养起这个习惯,定期写博客不仅是对自己学习知识的一种沉淀,更是在督 ...

  5. 读书笔记之《Java 并发编程的艺术》

    一.多线程语义 即使是单核处理器也支持多线程执行代码,CPU 通过给每个线程分配 CPU 时间片来执行任务,当前任务执行一个时间片后会切换到下一个任务,所以 CPU 通过不停的切换线程执行. 并发执行 ...

  6. 《Java并发编程的艺术》读书笔记:二、Java并发机制的底层实现原理

    二.Java并发机制底层实现原理 这里是我的<Java并发编程的艺术>读书笔记的第二篇,对前文有兴趣的朋友可以去这里看第一篇:一.并发编程的目的与挑战 有兴趣讨论的朋友可以给我留言! 1. ...

  7. Java并发编程的艺术读书笔记(2)-并发编程模型

    title: Java并发编程的艺术读书笔记(2)-并发编程模型 date: 2017-05-05 23:37:20 tags: ['多线程','并发'] categories: 读书笔记 --- 1 ...

  8. 《Java并发编程的艺术》留给自己以后看的笔记

    <Java并发编程的艺术>这本书特别好,和<深入了解JAVA虚拟机>有一拼,建议做java的都看看,下面全部都是复制书中的部分内容,主要目的是做个笔记,方便以后遇到问题能找到. ...

  9. Java并发编程的艺术,解读并发编程的优缺点

    并发编程的优缺点 使用并发的原因 多核的CPU的背景下,催生了并发编程的趋势,通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升. 在特殊的业务场景下先天的就适合于并发编程. 比如在 ...

随机推荐

  1. 关于CCR测评器的自定义校验器(Special Judge)

    引言 有时我们需要使用CCR测评器(CCR-Plus是一个开源的信息学竞赛测评软件,Github链接https://github.com/sxyzccr/CCR-Plus)进行SpecialJudge ...

  2. Video.js 简单的使用介绍

    vedio.js 是一款视频播放插件,它会自动检测浏览器对 HTML5 的支持情况,如果不支持 HTML5 则自动使用 Flash 播放器.下面来介绍下它的使用: 引用video-js.cs样式文件和 ...

  3. nginx log 错误502 upstream sent too big header while reading response header from upstream

    cookies的值超出了范围我是说 看看了一下日志 错误502 upstream sent too big header while reading response header from upst ...

  4. 洛谷P2894 [USACO08FEB]酒店Hotel [线段树]

    题目传送门 酒店 题目描述 The cows are journeying north to Thunder Bay in Canada to gain cultural enrichment and ...

  5. 002.SSH日常命令

    一 远程登陆 ssh 用户名@远程主机ip:首次登陆需要下载对方公钥. 实例:ssh 192.168.10.129 二 远程复制 scp root@远程主机ip:[远程主机文件绝对路径] [需要保存的 ...

  6. C#并行编程(4):基于任务的并行

    C#中的任务Task 在C#编程中,实现并行可以直接使用线程,但使用起来很繁琐:也可以使用线程池,线程池很大程度上简化了线程的使用,但是也有着一些局限,比如我们不知道作业什么时候完成,也取不到作业的返 ...

  7. 初识thinkphp(4)

    这次内容是数据库的使用方法 因为在第一章讲过就是拿index这个文件写的数据库的访问,这次实验我使用文件是系统默认的首页 配置的内容也在那里有提过就不重发戳图了. 数据库按照手册上的建议建了3行的内容 ...

  8. javascript 正则限制文本输入框只允许输入数字,简单实现。

    <input type="text" id="memberId" lay-verify="title" autocomplete=&q ...

  9. 接口开发-基于SpringBoot创建基础框架

    说到接口开发,能想到的开发语言有很多种,像什么Java啊..NET啊.PHP啊.NodeJS啊,太多可以用.为什么选择Java,究其原因,最后只有一个解释,那就是“学Java的人多,人员招聘范围大,有 ...

  10. LocalCache

    public static class LocalCacheHelper { ; //5分钟过期 public static T GetCache<T>(string cacheKey) ...