并发基础之Java内存模型JMM

目录

前言

线程通信

内存模型

重排序

并发编程三要素

1、原子性

2、可见性

3、顺序性

线程安全

1、保证原子性

2、保证可见性

3、保证顺序性

备注

as-if-serial语义

happens-before原则


前言

在我们实际工作开发场景中,很多时候都会用到多线程来提升系统运行效率的情况。而对于多线程的编码而言,我们会用到Java工具包中的很多编程API,其中不仅仅有wait()、join()、notify()等线程通信方法,还有很多如CountDownLatch、ReentrantLock等并发工具类,通过这些工具类和方法我们可以任意编写多线程业务规则而达到我们想要的结果。那么,童鞋们有没有思考过为什么这些方法和工具类可以保证多线程正常访问内存呢?答案就是今天的主角——Java内存模型。

线程通信

说到Java内存模型,我们不得不先说说线程通信方式。线程通信方式有两种,一种消息通信机制,也就是所谓的通过消息发送来保证两个乃至多个线程的通信;另一种则是共享内存的方式,就是多个线程共享公共内存, 每个线程的操作取数都是基于公共内存。而我们的JVM中对于堆的操作就是基于第二种方式,也就是共享内存,也就是为什么我们要探讨Java内存模型。

内存模型

Java内存模型的英文是Java Memory Model,简称JMM。在java中的对象、静态变量以及字符串常量池都是保存在堆内存中,其他的方法内局部变量和经过逃逸分析没有逃逸的对象都是保存在栈区,保存在堆区的数据都有内存可见性等问题,而JMM内存模型就是解决线程之间通信,也就是一个线程对共享内存变量修改何时对另一个线程可见。JMM内存模型定义了多线程与共享变量的抽象关系:线程中的所有共享变量保存在主存中,也就是堆内存中,每个线程都有自己的本地内存,本地内存包含对对共享变量的副本拷贝,且本地内存并不真实存在只是一种抽象模型。在实际的多线程操作中,线程对共享变量操作会先从主存中拷贝变量副本,当操作完成后会将变量回写刷入主存,所有线程只能通过主存进行通讯。

重排序

大家都知道Java类需要通过编译器编译为JVM可执行的class文件,JVM经过加载-验证-准备-解析-初始化再到运行后会调用计算机处理器操作,其中的编译阶段和处理器阶段为保证程序运行效率会有指令重排序的情况。但是重排序虽然能够提供效率,但是在某些特殊情况下会造成多线程安全问题。故JMM的编译器重排序规则会禁止指定类型编译器重排,处理器重排序规则会在编译器编译过程中插入内存屏障禁止指令重排。当然基于as-if-serial语义,对于有依赖关系的指令编译器和处理器不会对其进行重排序。

并发编程三要素

1、原子性

一次或多次操作中要么线程在执行过程中不被其他操作打断,要么全部不执行。

Java代码编写过程中很多地方其实都是非原子性的,比如:

@Test 
public void toTest(){ 
    //原子操作 
    int a = 1; 
    //非原子操作,第一步计算a的值,第二步再进行赋值 
    a = a+1; 
}

非原子性的操作会引起多线程安全问题。

2、可见性

多个线程操作共享变量时,一个线程修改了变量应立即被其他变量可见结果。

由于我们JMM内存模型将多个线程共享变量通过共享主内存进行通信,而各个线程自行维护一个本地内存副本。如果有本地内存修改变量未及时刷新共享主内存数据,那么其他线程拿到的变量数据就是错误的数据,也就会导致多线安全问题。

3、顺序性

程序执行的顺序应按照代码顺序执行。在JMM允许的重排序环境下,单线程的执行结果和没有重排序的情况下保持一致。

在Java程序中,倘若在本线程内,所有的操作都可视为有序性,在多线程环境下,一个线程观察另外一个线程,都视为无顺序可言。在本篇博客里我们描述了指令重排,重排序是虽然会提升运行效率,但是有些时候会打乱代码执行顺序、指令执行顺序,代码和指令不同的执行顺序在多线程情况下可能也会有不同的执行结果。

线程安全

上面我们介绍了JMM内存模型是多线程与共享变量的抽象关系,了解到了Java编译器和计算机处理器为提升效率会对代码顺序和指令进行重排,也了解到了多线程编程有很多的安全问题。那么,JMM内存模型如何来解决多线程问题的呢?

1、保证原子性

1.1 自带原子性

java里面的基本数据类型的读取和赋值默认是原子的,比如如:byte、short、int

1.2 原子类操作

JUC java.util.concurrent.atomic 包下提供很多的原子类操作,这些操作类能够保证运算原子性,比如:AtomicLong、AtomicInteger等等

1.3 synchronized

synchronized 同步类可保证进入方法和类的线程单一,从而保证执行流程串联化来保证执行结果原子。

1.4 Lock锁机制

Lock锁机制底层采用AQS实现,无论是公平锁、非公平锁以及读写锁其根本都是保证执行结果原子。

2、保证可见性

2.1 synchronized

synchronized 同步类可保证进入方法和类的线程单一,其本质是在进入同步方和或类时会从主存加载最新的变量数据,当线程执行完成时会将本地内存刷入主存,从而保证可见性。

2.2 Lock锁机制

Lock锁机制底层采用AQS实现,无论是公平锁、非公平锁以及读写锁都是能够获取到最新的共享内存数据,其原理与synchronized相似。

2.3 Volatile关键字

Volatile关键字的作用是当线程修改了本地内存变量会立即刷入主存,且其他线程变量的拷贝副本会立即失效,并且其他线程需要对这个变量进行读写必须从主存重新加载。

3、保证顺序性

3.1 synchronized

synchronized 同步块和方法保证执行线程串行化,我们可以认为单线程里面的操作都是有序的。

3.2 Lock锁机制

Lock锁机制同同步代码块一样,在进行操作时候只允许单线程进行,当然可以保证操作顺序性。

3.3 Volatile关键字

Volatile关键字底层使用内存屏障禁止指令重排,从而使得在多线程环境下操作是有序的。

3.4 happens-before原则

happens-before原则是Java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,也就是说发生操作B之前,操作A产生的影响能被操作B观察到。

备注

as-if-serial语义

不管怎么重排序,程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

happens-before原则

在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是一个线程之内,也可以是不同线程之间。两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行。happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。

happens-before规则如下:

程序顺序规则(Program Order Rule):一个线程中的每个操作,happens-before于该线程中的任意后续操作。

监视器锁规则(Monitor Lock Rule):对一个锁的解锁,happens-before于随后对这个锁的加锁。

volatile变量规则(Volatile Variable Rule):对一个volatile域的写,happens-before于任意后续对这个volatile域的读。

start()规则(Thread Start Rule):如果线程A执行线程B.start()(启动线程B),那么A线程的B.start()操作happens-before于线程B中的任意操作。

join()规则(Thread Join Rule):如果线程A执行线程B.join()并成功返回,那么线程B中的任意操作happens-before于线程A从B.join()操作成功返回。

程序中断规则(Thread Interruption Rule):对线程interrupt()的调用happens-before于被中断线程的interrupted()或者isInterrupted()。

finalizer规则(Finalizer Rule):一个对象构造函数的结束happens-before于该对象finalizer()的开始。

传递性规则(Transitivity):如果A happens-before B,且B happens-before C ,那么A happens-before C。

猜你喜欢

转载自blog.csdn.net/weixin_39970883/article/details/129494720