当前位置: 首页 > 后端技术 > Java

并发编程:JMM

时间:2023-04-01 22:21:05 Java

大家好,我是小黑,一名生活在网络上的农民工。上一期分享了Java中线程的一些基础知识。在线程终止的例子中,第一种方法提到了如果要终止一个线程,可以使用标志位的方法,我们再回顾一下代码。classMyRunnableimplementsRunnable{//volatile关键字保证主线程修改后当前线程能看到改变的值(可见性)privatevolatilebooleanexit=false;@Overridepublicvoidrun(){while(!exit){//循环判断是否退出System.out.println("Thisismycustomthread");}}publicvoidsetExit(booleanexit){this.exit=exit;}}publicclassThreadDemo{publicstaticvoidmain(String[]args){MyRunnablerunnable=newMyRunnable();新线程(可运行).start();runnable.setExit(真);//modifyflag,exitthread}}这段代码中,标志位exit字段声明了volatileshutdown字修饰,目的是为了保证当前线程能感知到其他线程修改后的变化,那么到底是做什么的呢?这个关键词呢?本期我们来详细聊一聊。在开始讲volatile关键字之前,有必要跟大家讲讲计算机的内存模型。计算机内存模型所谓内存模型,英文描述是MemoryModel,这个东西是比较底层的东西,是一个跟计算机硬件相关的概念。我们都知道,计算机在执行程序时,最终都是在CPU中一条条执行指令,在执行过程中经常会发生数据传输。而数据是存储在主存上的,没错,就是你的记忆棒。一开始CPU的执行速度不够快的时候是没有问题的,但是随着CPU技术的不断发展,CPU的计算速度越来越快,但是,从主存中读写数据速度有点慢,跟不上,导致CPU每次操作主存都要花很多等待时间。技术总是要向前发展的,CPU不能因为内存读写慢就停止发展,更不能让主存的读写速度成为瓶颈。想必大家看到这里应该已经想到了,那就是在CPU和主存之间加一个缓存,把需要的数据拷贝到这个缓存上。缓存中的数据与主存同步。这里问题解决了吗?太年轻了,太简单了,这种结构在线程的情况下是没有问题的。随着计算机能力的不断提升,已经开始支持多线程了,CPU再厉害也支持多核了。到目前为止,它有4核、8核和16核。core,这样的话会出现一些问题,我们分析一下。单核多线程情况:多个线程同时访问一个共享数据,CPU将数据从主存加载到缓存中,多个线程会访问缓存中同一个地址,所以即使当线程被切换后,缓存的数据是不会失效的,因为在单核CPU上一次只能执行一个线程,所以不会出现数据访问冲突。多核多线程情况:每个CPU核都会复制一份数据到自己的缓存中,这样不同核上的两个线程是并行的,会造成两个核缓存的数据不一致。这个问题称为缓存一致性问题。除了上面提到的缓存一致性问题之外,为了充分利用CPU的计算能力,计算机会对输入的指令进行乱序处理,这就是所谓的处理器优化。为了提高执行效率,很多编程语言还会对代码的执行顺序进行重新排序。例如,我们的Java虚拟机的即时编译器(JIT)也会这样做。此操作称为指令重排。整数=1;整数b=2;intc=a+b;intd=a-b;比如我们写的代码中,第三行和第四行的执行顺序可能会发生变化,这在单线程中是没有问题的,但是在多线程的情况下,就会产生和我们预期不同的结果.其实上面提到的缓存一致性问题、处理器优化、指令重排等问题,对应的就是我们并发编程中的可见性问题、原子性问题、顺序问题。带着这些问题,我们来看看Java是如何解决的。因为这些问题,必须要有机制来解决。该解决方案的机制是内存模型。内存模型定义了一个规范来确保共享内存的可见性、顺序和原子性。内存模型是怎么解决的?主要有两种方法:限制处理器优化和内存屏障。在这里,我们不会深入研究基本原理。JMM前面我们知道,内存模型是为了解决并发情况下的一些问题而制定的规范。不同的编程语言对这个规范都有相应的实现。那么JMM(JavaMemoryModel)就是Java语言对这个规范的具体实现。那么JMM具体是如何解决这个写法问题的呢?我们先来看下图。让我们一一来看内存可见性问题。一、如何解决可见性问题?如上图所示,在JMM中,一个线程对一个数据的操作分为6个步骤。它们是:读取、加载、使用、分配、写入、存储。如果声明变量时没有使用volatile关键字,那么两个线程将各自复制一份到工作内存中,线程B会将标志赋值为true。线程A是不可见的。那么如果想让线程A可见,就需要在声明变量flag的时候加上volatile关键字。那么JMM在添加关键字之后做什么呢?有读和写两种情况。当线程读取volatile变量时,JMM会将工作内存中的变量失效,重新从主内存中读取;当一个线程写入一个volatile变量时,它会立即将工作内存中的值刷新到主存中间。也就是说,对于被volatile关键字修饰的变量,read、load、use操作必须一起执行;分配、写入和存储操作必须一起执行。这样就可以解决内存可见性的问题。对于指令重排和指令重排的问题,对于编译器来说,只要对象声明为volatile,就不会针对指令重排进行优化。volatile禁止指令重排的规则符合一个叫做happens-before的规则。happens-before除了volatile变量规则外,还有一些其他规则。程序顺序规则:一段代码在线程中的执行结果是有序的。就是重新排列指令,但是无论它做什么,结果是我们代码的顺序不会改变。监听锁规则:无论是单线程环境还是多线程环境,对于同一个锁,一个线程解锁锁后,另一个线程获取锁后可以看到前一个线程的操作结果!(monitor是一个通用的同步原语,synchronized是monitor的实现)volatile变量规则:如果一个线程先写一个volatile变量,然后一个线程读这个变量,那么写操作的结果一定是read这个线程的可见性。线程启动规则:主线程A执行期间,启动子线程B,则线程A对启动子线程B前共享变量的修改结果对线程B可见。线程终止规则:线程B执行期间主线程A,如果子线程B终止,那么终止前线程B对共享变量的修改结果在线程A中是可见的。也称为线程join()规则。线程中断规则:被中断的线程代码检测到中断事件发生时,首先调用线程interrupt()方法,可以通过Thread.interrupted()检测是否发生中断。传递规则:happens-before原则是传递的,即hb(A,B),hb(B,C),然后hb(A,C)。对象终结规则:一个对象的初始化完成,即构造函数的执行结束必须happen-before它的finalize()方法。racecondition来了,是不是觉得问题解决了?emmm,我们来看下面这个场景:假设上图中的线程A和线程B在两个CPU核心上并行执行,他们一起读取i等于0的值,然后各自加1,然后一起写入主存。如果线程A和线程B顺序执行,最后i的值应该等于2,但是并行的话,可以同时操作,写回主存的值只增加一次.这就好比你的银行卡收到了两笔100元的转账,但是账户里只剩下100元了。对于这种问题,volatile解决不了,volatile也不会保证变量操作的原子性。那我们应该怎么解决呢?我们需要使用synchronized来给这个操作加锁,保证同一时间只能有一个线程操作。总结由于CPU和内存之间存在缓存,在多线程并发的情况下可能存在缓存一致性问题;CPU会对输入的指令做一些处理器优化,一些高级语言编译器也会做指令。改编。因为这些问题,我们在并发的情况下会出现内存可见性问题和顺序问题,Java中出现了JMM来解决这些问题。通过volatile关键字保证内存可见性,禁止指令重排。但是volatile只能保证操作的顺序,不能保证操作的原子性。因此,为了安全,我们需要对共享变量的并发处理进行加锁。好了,今天就到这里,我们下次再见。喜欢的朋友可以关注小黑的公众号,定期分享干货。

猜你喜欢