【并发基础】Java内存模型、synchronized、volatile与多线程关系

前面已经单独介绍了Java内存模型,synchronized关键字,volatile关键字

【并发基础】Java内存模型基础知识

【并发基础】synchronized原理

【并发基础】volatile原理

接下来我们将这些知识串在一起,并从并发、多线程的角度来综合介绍

1. 最重要的是内存模型

1.1 从内存模型到Java内存模型

这得从计算机的CPU、缓存、内存发展史开始讲起,我将通过罗列转折点的方式长话短说

  1. 每条计算机指令都是在CPU中执行的,执行的时候肯定要和数据打交道,而数据是存在主内存当中的,也就是电脑里的内存条;
  2. 可以看出内存只是个存数据的地方,CPU是决定处理结果的地方,也因此,CPU是决定计算机运行速度的主要标准,所以CPU发展比内存发展的快;
  3. 随着时间的发展,CPU越来越快,内存已经跟不上CPU的速度了,但CPU又不能等着内存慢慢发展,由此CPU想出了一个办法,就是在CPU和内存之间增加高速缓存,他的特点是速度快、内存小;
  4. 人们设计的CPU、高速缓存、内存之间的执行过程就称为内存模型。
  5. 该模型描述:当程序在运行过程中,会将运算需要的数据从主内存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中;
  6. 速度的问题解决后,CPU还在不断发展,时间一长,高速缓存又满足不了CPU的发展需求了,这时CPU有衍生出了多级缓存(直至现在,已经发展出3级内存了);
  7. 再后来CPU还在发展,为了提高计算能力,大家觉得CPU不应该一次只运算一个任务,而是可以同时运行多个任务,由此又衍生了多核心、多进程、多线程等概念;
  8. 此时出现了多个程序同时访问内存的情况,为了解决内存冲突,缓存一致性问题,微软搞了一套协议叫MESI;
  9. 之后各大软件厂商因为市场需求,程序运行速度也需要越来越快,导致一个软件被拆分成多个进程,或者细化成多个线程,而Java作为一种制作程序的工具,他开发出来的东西就是运行在JVM上的进程,因此在多线程领域有了JVM内存结构和Java内存模型。

1.2 为什么需要知道内存模型

由此可以看出,如果不存在需要多个程序运行同时运行,每个程序一次又只需要运行一样计算功能的话,其实软件是那么的简单,代码你随便写(当然随便写不是随意写),只要能运行出正确解决,你运行一百次都不会有错,但有并发就不行,因为各个进程、线程会去抢夺资源,共享资源,这就导致数据会被多处使用,由于种种原因导致数据错误。

这其实也是互联网公司和传统行业区别(不绝对,大部分情况是),互联网公司的用户多,而且用户活动的时间段又比较集中,这就导致了会有多个请求一起提交到服务器,需要操作同样的数据,为了避免冲突,会需要许多策略来解决问题,而传统行业,比如OA、ERP等系统,一个系统能有一万人用就不错了,然后有关联业务的人,一个业务能关联100人也算顶天了,这一条业务能同时有20个人操作,网速、工作效率等因素一穿插在里面,其实几乎没有并发,而就算有20、100人的并发在,我可以告诉你,Spring他本身也有一定的扛并发的能力,再加上Tomcat、MySQL,一层层下去都会有分担并发的能力,最后再不行,你上个Redis+Nginx+消息队列+分库分表+前后分离+微服务,最后再根据业务需求,在并发点上加上锁,几乎不会有问题。而互联网行业不并发和传统行业没啥区别,出了并发的话,少说也是1000,再看看双十一,我估计并发十万应还是有的,如果还用传统行业的那一套,直接就都能产生DDoS的效果,到时别说提交了,访问都困难,这里多扯两句互联网大厂还会用到什么技术,其实他们也是用Spring、Tomcat、MySQL、Redis、Nginx、消息队列、分库分表、微服务等等这些技术打造出来的平台,那么为什么他们能抗住这么大的访问量、并发量、数据量呢?最主要还是有钱,多搞点服务器整个集群比啥都来得快,哈哈哈,这只是一部分原因,另一部分原因就是他们对程序、机器的理解,然后再加上先进的理念,让他们做到即使咱俩的设备硬件是一样的,我的程序一样比你强这种效果。

1.3 多线程操作的三大问题

先来看下计算机发展史上都出现了哪几个转折点

  1. 在CPU和主内存之间增加了缓存,之后引入了多线程就导致了缓存一致性问题,就是线程A操作了共享变量,但线程B不能立马看到修改结果;
  2. CPU为了运行速度更快,除了本身架构的升级,为了充分利用运算单元,他们还会对程序的代码进行乱序处理,在满足单线程结果一致的情况下,优化代码结构,这又叫处理器优化
  3. 同样的,很多编程语言的编译器也开始这么操作,对开发者的代码进行编译后的乱序优化,同样是保证单线程结果一致,这个在编译层面叫做指令重排序

1.4 并发编程

为了解决上述的问题,并发编程定义了一套规范,并以这套规范来解决三大问题,这种规范我们长叫他并发的三大特性。

  1. 原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。(解决处理器优化问题)

  2. 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。(解决缓存一致性问题)

  3. 有序性即程序执行的顺序按照代码的先后顺序执行。(解决指令重排序问题)

因此,需同时满足这三个特性才能够实现线程安全。

1.3 理念介绍(Tree New Bee)

我要开始Tree New Bee了,你可以不看这一节,当我仍然要Tree

实践验证理论,理论指导实践。

说一说我都知道哪些理念和技术,有几个是我用过的,其他大多我都只知道原理和使用场景,并没有用过,但即使这样,在业务处理上已经能帮助我太多太多了。

微服务(SpringCloud、Dubbo)、消息队列(KafKa、RabbitMQ)、分库分表(水平拆分、垂直拆分)、权限设计、领域驱动、渐进式框架、Java虚拟机、SQL优化、反向代理、网络协议(TCP、UDP、socket、HTTP、HTTPS)、Redis(缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级)、容器技术(Docker、k8s、k3s)、分布式事务、Linux优化等等。

而我现在在写的就是关于Java虚拟机并发处理的一些理论知识,即使已经写了4篇了,但Java虚拟机仍然还有太多太多的东西没有讲到,也由此可以看到BAT那些大厂强在哪,为什么一个研发团队动辄几百上千人,一个人的力量始终有限(当然了,人口增长速率远低于技术发展速率,当产品能满足现有人口数、用户量的时候,其实只需要有人维护现有产品就行,也就不需要那么多开发人员了,所以大厂开始裁员了。。。哎,资本爸爸是莫得感情的啊)

2. JVM内存结构、Java内存模型

Java作为一种面向对象的跨平台语言,其对象、内存等一直是比较难的知识点。而且很多概念的名称看起来又那么相似,比如JVM内存结构、Java内存模型,这就是两个截然不同的概念。

2.1 JVM内存结构

之前文章中有一幅简单的JVM内存结构图,后来我又去翻了些资料,甚至利用Google翻译查阅了官方的Java虚拟机规范,最后得到了如下图形

JVM内存结构

关于每个区域是做什么的,综合第一篇文章可以得知,现在需要知道以下几点

  1. 这其实是一种Java虚拟机规范,因此并不是硬性要求,不同的Java虚拟机可能会有不同的实现形式,但一般都会遵守规范;
  2. 上面这些区域只是一种逻辑区域,说明这些区域是做什么的,不同的Java版本可能会导致区域位置不同,甚至是区域功能的重合;
  3. 除了以上介绍的JVM运行时内存外,还有一块内存区域可供使用,那就是直接内存。Java虚拟机规范并没有定义这块内存区域,所以他并不由JVM管理,是利用本地方法库直接在堆外申请的内存区域。

总的来说,JVM内存结构由Java虚拟机规范定义,描述的事Java程序运行过程中,JVM管理的不同区域的数据。

2.2 Java内存模型

Java内存模型简称JMM,在Java中,JMM是一个非常重要的概念,他并不像JVM内存结构那样真实存在,他只是一种抽象概念。他与多线程相关,他描述了一组规则或规范,这个规范定义了一个线程对共享变量的写入时对另一个线程是可见的。

JMM内存模型

可以看出,两个线程之间通过主内存,复制变量然后进行共享变量的操作最后在刷新到主内存中,若没有JMM控制,两个线程就有可能同时操作一个共享变量,随后导致只有一个值刷新到主内存中。

2.4 小结

  1. JVM内存结构,和Java虚拟机的运行时区域有关。
  2. Java内存模型,和Java的并发编程有关。

2.5 Java内存模型与并发

原子性在Java中,为了保证原子性,提供了synchronized关键字 ,他能实现两个高级的字节码指令monitorenter和monitorexit。因此,在Java中可以使用synchronized来保证方法和代码块内的操作是原子性的。

可见性Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的这种依赖主内存作为传递媒介的方式来实现的。

Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。

除了volatile,Java中的synchronized和final两个关键字也可以实现可见性。只不过实现方式不同。

有序性在Java中,可以使用synchronized和volatile来保证多线程之间操作的有序性。实现方式有所区别:

volatile关键字会禁止指令重排。synchronized关键字保证同一时刻只允许一条线程操作。

好了,这里简单的介绍完了Java并发编程中解决原子性、可见性以及有序性可以使用的关键字。但好像synchronized关键字是万能的,他可以同时满足以上三种特性,这其实也是很多人滥用synchronized的原因。但是synchronized其实是比较影响性能的,虽然编译器提供了很多锁优化技术,但是也不建议过度使用。

3. synchronized、volatile与三大特性

3.1 原子性

synchronized:通过monitorenter和monitorexit指令,可以保证被synchronized修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。因此,在Java中可以使用synchronized来保证方法和代码块内的操作是原子性的。

volatile:无法保证原子性。

3.2 可见性

synchronized:被synchronized修饰的代码,在开始执行时会加锁,执行完成后会进行解锁。而为了保证可见性,有一条规则是这样的:对一个变量解锁之前,必须先把此变量同步回主存中。这样解锁后,后续线程就可以访问到被修改后的值。所以,synchronized关键字锁住的对象,其值是具有可见性的。

volatile:volatile对于可见性的实现,内存屏障起着至关重要的作用。因为内存屏障相当于一个数据同步点,他要保证在这个同步点之后的读写操作必须在这个点之前的读写操作都执行完之后才可以执行。并且在遇到内存屏障的时候,缓存数据会和主存进行同步,或者把缓存数据写入主存、或者从主存把数据读取到缓存。

内存屏障:是一种同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。

3.3 有序性

synchronized:由于synchronized修饰的代码,同一时间只能被同一线程访问。那么也就是单线程执行的。所以,可以保证其有序性。

volatile:volatile是通过内存屏障来来禁止指令重排的。