广告
返回顶部
首页 > 资讯 > 精选 >如何解读Java多线程与并发模型中的共享对象
  • 935
分享到

如何解读Java多线程与并发模型中的共享对象

2023-06-02 18:06:19 935人浏览 独家记忆
摘要

本篇文章为大家展示了如何解读Java多线程与并发模型中的共享对象,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。以下内容如无特殊说明均指代Java环境。共享对象使用Java编写线程安全的程序关键在于正

本篇文章为大家展示了如何解读Java多线程并发模型中的共享对象,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。

以下内容如无特殊说明均指代Java环境。

共享对象

使用Java编写线程安全的程序关键在于正确的使用共享对象,以及安全的对其进行访问管理。在第一章我们谈到Java的内置可以保障线程安全,对于其他的应用来说并发的安全性是在内置锁这个“黑盒子”内保障了线程变量使用的边界。谈到线程的边界问题,随之而来的是Java内存模型另外的一个重要的含义,可见性。Java对可见性提供的原生支持是volatile关键字。

volatile关键字

volatile 变量具备两种特性,其一是保证该变量对所有线程可见,这里的可见性指的是当一个线程修改了变量的值,那么新的值对于其他线程是可以立即获取的。其二 volatile 禁止了指令重排。

虽然 volatile 变量具有可见性和禁止指令重排序,但是并不能说 volatile 变量能确保并发安全。

public class VolatileTest {

public static volatile int a = 0;

public static final int THREAD_COUNT = 20;

public static void increase() {a++;

}

public static void main(String[] args) 

throws InterruptedException

 {

Thread[] threads = new Thread[THREAD_COUNT];

for (int i = 0;

 i < THREAD_COUNT; i++)

 {

threads[i] = new Thread(new Runnable() 

{

public void run() {for (int i = 0;

 i < 1000;

 i++) {increase();

}

}

}

);

threads[i].start();

}

while (Thread.activeCount() > 2)

 {

Thread.yield();

}

System.out.println(a);

}

}

按照我们的预期,它应该返回 20000 ,但是很可惜,该程序的返回结果几乎每次都不一样。

问题主要出在 a++ 上,复合操作并不具备原子性, 虽然这里利用 volatile 定义了 a ,但是在做 a++ 时, 先获取到最新的 a 值,比如这时候最新的可能是 50,然后再让 a 增加,但是在增加的过程中,其他线程很可能已经将 a 的值改变了,或许已经成为 52、53 ,但是该线程作自增时,还是使用的旧值,所以会出现结果往往小于预期的 2000。如果要解决这个问题,可以对 increase() 方法加锁。

volatile 适用场景

volatile 适用于程序运算结果不依赖于变量的当前值,也相当于说,上述程序的 a 不要自增,或者说仅仅是赋值运算,例如 boolean flag = true 这样的操作。

volatile boolean shutDown =false;

public voidshutDown()

 {

shutDown =true;

}

public voiddoWork()

 {while(!shutDown)

 {

System.out.println("Do work "+ Thread.currentThread().getId());

}

}

代码2.1:变量的可见性问题

在代码2.1中,可以看到按照正常的逻辑应该打印10之后线程停止,但是实际的情况可能是打印出0或者程序永远不会被终止掉。其原因是没有使用恰当的同步机制以保障线程的写入操作对所有线程都是可见的。

我们一般将volatile理解为synchronized的轻量级实现,在多核处理器中可以保障共享变量的“可见性”,但是不能保障原子性。关于原子性问题在该章节的程序变量规则会加以说明,下面我们先看下Java的内存模型实现以了解JVM和计算机硬件是如何协调共享变量的以及volatile变量的可见性。

Java内存模型

我们都知道现代计算机都是冯诺依曼结构的,所有的代码都是顺序执行的。如果计算机需要在CPU中运算某个指令,势必就会涉及对数据的读取和写入操作。由于程序数据的大部分内容都是存储在主内存(RAM)中的,在这当中就存在着一个读取速度的问题,CPU很快而主内存相对来说(相对CPU)就会慢上很多,为了解决这个速度阶梯问题,各个CPU厂商都在CPU里面引入了高速缓存优化主内存和CPU的数据交互。针对上面的技术我特意整理了一下,有很多技术不是靠几句话能讲清楚,所以干脆找朋友录制了一些视频,很多问题其实答案很简单,但是背后的思考和逻辑不简单,要做到知其然还要知其所以然。如果想学习Java工程化、高性能及分布式、深入浅出。微服务springmybatisNetty源码分析的朋友可以加我的Java进阶群:591240817,群里有大牛直播讲解技术,以及Java大型互联网技术的视频免费分享

此时当CPU需要从主内存获取数据时,会拷贝一份到高速缓存中,CPU计算时就可以直接在高速缓存中进行数据的读取和写入,提高吞吐量。当数据运行完成后,再将高速缓存的内容刷新到主内存中,此时其他CPU看到的才是执行之后的结果,但在这之间存在着时间差。

看这个例子:

int counter = 0; counter = counter + 1;复制代码

代码2.2:自增不一致问题

代码2.2在运行时,CPU会从主内存中读取counter的值,复制一份到当前CPU核心的高速缓存中,在CPU执行完成加1的指令之后,将结果1写入高速缓存中,最后将高速缓存刷新到主内存中。这个例子代码在单线程的程序中将正确的运行下去。

但我们试想这样一种情况,现在有两个线程共同运行该段代码,初始化时两个线程分别从主内存中读取了counter的值0到各自的高速缓存中,线程1在CPU1中运算完成后写入高速缓存Cache1,线程2在CPU2中运算完成后写入高速缓存Cache2,此时counter的值在两个CPU的高速缓存中的值都是1。

此时CPU1将值刷新到主内存中,counter的值为1,之后CPU2将counter的值也刷新到主内存,counter的值覆盖为1,最终的结果计算counter为1(正确的两次计算结果相加应为2)。这就是缓存不一致性问题。这会在多线程访问共享变量时出现。

解决缓存不一致问题的方案:

通过总线锁LOCK#方式。

通过缓存一致性协议。

如何解读Java多线程与并发模型中的共享对象

图2.1 :缓存不一致问题

图2.1中提到的两种内存一致性协议都是从计算机硬件层面上提供的保障。CPU一般是通过在总线上增加LOCK#锁的方式,锁住对内存的访问来达到目的,也就是阻塞其他CPU对内存的访问,从而使只有一个CPU能访问该主内存。因此需要用总线进行内存锁定,可以分析得到此种做法对CPU的吞吐率造成的损害很严重,效率低下。

随着技术升级带来了缓存一致性协议,市场占有率较大的Intel的CPU使用的是MESI协议,该协议可以保障各个高速缓存使用的共享变量的副本是一致的。其实现的核心思想是:当在多核心CPU中访问的变量是共享变量时,某个线程在CPU中修改共享变量数据时,会通知其他也存储了该变量副本的CPU将缓存置为无效状态,因此其他CPU读取该高速缓存中的变量时,发现该共享变量副本为无效状态,会从主内存中重新加载。但当缓存一致性协议无法发挥作用时,CPU还是会降级使用总线锁的方式进行锁定处理。

一个小插曲:为什么volatile无法保障的原子性

我们看下图2.2,CPU在主内存中读取一个变量之后,拷贝副本到高速缓存,CPU在执行期间虽然识别了变量的“易变性”,但是只能保障最后一步store操作的原子性,在load,use期间并未实现其原子性操作。

如何解读Java多线程与并发模型中的共享对象

图2.2:数据加载和内存屏障

JVM为了使我们的代码得到最优的执行体验,在进行自我优化时,并不保障代码的先后执行顺序(满足Happen-Before规则的除外),这就是“指令重排”,而上面提到的store操作保障了原子性,JVM是如何实现的呢?其原因是这里存在一个“内存屏障”的指令(以后我们会谈到整个内容),这个是CPU支持的一个指令,该指令只能保障store时的原子性,但是不能保障整个操作的原子性。

从整个小插曲中,我们看到了volatile虽然有可见性的语义,但是并不能真正的保证线程安全。如果要保证并发线程的安全访问,需要符合并发程序变量的访问规则。

并发程序变量的访问规则

原子性

程序的原子性和数据库事务的原子性有着同样的意义,可以保障一次操作要么全部执行成功,要不全部都不执行。

可见性

可见性是微妙的,因为最终的结果总是和我们的直觉大相径庭,当多个线程共同修改一个共享变量的值时,由于存在高速缓存中的变量副本操作,不能及时将数据刷新到主内存,导致当前线程在CP中的操作结果对其他CPU是不可见状态。

有序性

有序性通俗的理解就是程序在JVM中是按照顺序执行的,但是前面已经提到了JVM为了优化代码的执行速度,会进行“指令重排”。在单线程中“指令重排”并不会带来安全问题,但在并发程序中,由于程序的顺序不能保障,运行过程中可能会出现不安全的线程访问问题。

综上,要想在并发编程环境中安全的运行程序,就必须满足原子性、可见性和有序性。只要以上任何一点没有保障,那程序运行就可能出现不可预知的错误。最后我们介绍一下java并发的“杀手锏”,Happens-Before法则,符合该法则的情况下可以保障并发环境下变量的访问规则。

happens-before语义

Java内存模型使用了各种操作来定义的,包括对变量的读写,监视器的获取释放等,JMM中使用了

happens-before

语义阐述了操作之间的内存可见性。如果想要保证执行操作B的线程看到操作A的结构(无论AB是否在同一线程),那么A,B必须满足

happens-before

关系。如果两个操作之间缺乏

happens-before

Happens-Before法则:

程序次序法则:线程中的每个动作A都Happens-Before于该线程中的每一个动作B,在程序中,所有的动作B都出现在动作A之后。

Lock法则:对于一个Lock的解锁操作总是Happens-Before于每一个后续对该Lock的加锁操作。

volatile变量法则:对于volatile变量的写入操作Happens-Before于后续对同一个变量的读操作。

线程启动法则:在一个线程里,对Thread.start()函数的调用会Happens-Before于每一个启动线程中的动作。

线程终结法则:线程中的任何动作都Happens-Before于其他线程检测到这个线程已经终结或者从Thread.join()函数调用中成功返回或者Thread.isAlive()函数返回false。

中断法则:一个线程调用另一个线程的interrupt总是Happens-Before于被中断的线程发现中断。

终结法则:一个对象的构造函数的结束总是Happens-Before于这个对象的finalizer(Java没有直接的类似C的析构函数)的开始。

传递性法则:如果A事件Happens-Before于B事件,并且B事件Happens-Before于C事件,那么A事件Happens-Before于C事件。

当一个变量在多线程竞争中被读取和存储,如果并未按照Happens-Before的法则,那么他就会存在数据竞争关系。

总结

给大家关于Java的共享变量的内容就介绍到这里,现在你已经明白Java的volatile关键字的含义了,了解了为什么volatile不能保障原子性的原因了,了解了Happens-Before规则能让我们的Java程序运行的更加安全。

在这里给大家提供一个学习交流的平台,java架构师群1017599436

具有1-5工作经验的,面对目前流行的技术不知从何下手,需要突破技术瓶颈的可以加群。

在公司待久了,过得很安逸,但跳槽时面试碰壁。需要在短时间内进修、跳槽拿高薪的可以加群。

如果没有工作经验,但基础非常扎实,对java工作机制,常用设计思想,常用java开发框架掌握熟练的可以加群。

通过这节内容希望可以帮助你更深入的了解Java的并发概念中的内置锁和共享变量。Java的并发内容还有很多,例如在某些场景下比synchronized效率要更高的Lock,阻塞队列,同步器等。

上述内容就是如何解读Java多线程与并发模型中的共享对象,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注编程网精选频道。

--结束END--

本文标题: 如何解读Java多线程与并发模型中的共享对象

本文链接: https://www.lsjlt.com/news/230822.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 如何解读Java多线程与并发模型中的共享对象
    本篇文章为大家展示了如何解读Java多线程与并发模型中的共享对象,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。以下内容如无特殊说明均指代Java环境。共享对象使用Java编写线程安全的程序关键在于正...
    99+
    2023-06-02
  • 怎么深入理解Java多线程与并发框中的顺序一致性模型
    怎么深入理解Java多线程与并发框中的顺序一致性模型,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。一、竞态条件(Race Condition)计算的正确性取决于 多个线程 执行...
    99+
    2023-06-05
  • 如何深入理解Java多线程与并发框中线程的状态
    本篇文章给大家分享的是有关如何深入理解Java多线程与并发框中线程的状态,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。1. 新建状态(New)万事万物都不是凭空出现的,线程也一...
    99+
    2023-06-05
  • 如何深入理解Java多线程与并发框中的CAS
    如何深入理解Java多线程与并发框中的CAS,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。CAS实现原理CAS 是 CompareAndSwap 的缩写,意思是...
    99+
    2023-06-05
  • 如何深入理解Java多线程与并发框中线程和进程的区别
    如何深入理解Java多线程与并发框中线程和进程的区别,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。线程和进程的区别1. 资源调度单位在计算机中,进程是程序运行所...
    99+
    2023-06-05
  • 如何深入理解Java多线程与并发框中的并发辅助工具类
    如何深入理解Java多线程与并发框中的并发辅助工具类,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。一、Exchanger 交换器(两线程间的通信)使用场景:用于 有且仅有两个线...
    99+
    2023-06-05
  • 如何深入理解Java多线程与并发框中的volatile关键字
    本篇文章为大家展示了如何深入理解Java多线程与并发框中的volatile关键字,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。概念把对 volatile变量的单个读/写,看成是使用 同一个监视器锁 ...
    99+
    2023-06-05
  • 如何深入理解Java多线程与并发框中的队列同步器AQS
    如何深入理解Java多线程与并发框中的队列同步器AQS,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。一、 AbstractOwnableSynchronizer 抽象的、可...
    99+
    2023-06-05
  • Java多线程高并发中如何解决ArrayList与HashSet和HashMap不安全的问题
    这篇文章主要为大家展示了“Java多线程高并发中如何解决ArrayList与HashSet和HashMap不安全的问题”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“Java多线程高并发中如何解决...
    99+
    2023-06-25
  • 如何深入理解Java多线程与并发框中重排序、屏障指令、as-if-serial规则
    这篇文章将为大家详细讲解有关如何深入理解Java多线程与并发框中重排序、屏障指令、as-if-serial规则,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。一、重排序前篇文章已经讲了Java...
    99+
    2023-06-05
  • Python中如何实现一个线程安全的并发缓存对象,保证读写一致性和数据安全性
    Python中如何实现一个线程安全的并发缓存对象,保证读写一致性和数据安全性在多线程的环境下,对共享数据进行读写操作需要考虑到线程安全的问题。当多个线程同时对一个缓存对象进行读写操作时,可能会导致数据不一致或者数据丢失的问题。为了解决这个问...
    99+
    2023-10-22
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作