觉得不错请按下图操作,掘友们,哈哈哈!!!
目录及前言
一:Java并发编程基础
主要讲解Java的并发编程的基础知识,包含原⼦性、可⻅性、有序性,以及内存模型JMM,所以如果想在Java上有所成果,并发的基础知识一定要结实把握哦 ,本系列会一步一步深化。
1.1 并发编程基本概念
1.1.1原⼦性
⼀个操作或者多个操作,要么全部执⾏并且执⾏的进程不会被任何因素打断,要么就都不执⾏。
原⼦性是拒绝多线程操作的,不论是多核还是单核,具有原⼦性的量,同⼀时间只能有⼀个线程来对它进⾏操作。 简⽽⾔之,在整个操作进程中不会被线程调度器中断的操作,都可认为是原⼦性。例如 a=1是原⼦性操作,可是 a++和a +=1就不是原⼦性操作。Java中的原⼦性操作包含:
- 基本类型的读取和赋值操作,且赋值有必要是值赋给变量,变量之间的彼此赋值不是原⼦性操作;
- 一切引⽤reference的赋值操作;
- java.concurrent.Atomic.* 包中一切类的⼀切操作。
1.1.2 可⻅性
指当多个线程访问同⼀个变量时,⼀个线程修正了这个变量的值,其他线程能够⽴即看得到修正的值。
在多线程环境下,⼀个线程对同享变量的操作对其他线程是不可⻅的。Java提供了volatile来保证可⻅性,当⼀个变 量被volatile润饰后,表明着线程本地内存⽆效,当⼀个线程修正同享变量后他会⽴即被更新到主内存中,其他线 程读取同享变量时,会直接从主内存中读取。当然,synchronize和Lock都能够保证可⻅性。synchronized和Lock 能保证同⼀时间只要⼀个线程获取锁然后执⾏同步代码,并且在释放锁之前会将对变量的修正刷新到主存当中。因 此能够保证可⻅性。
1.1.3 有序性
即程序执⾏的次第按照代码的先后次第执⾏。
Java内存模型中的有序性能够总结为:如果在本线程内观察,一切操作都是有序的;如果在⼀个线程中观察另⼀个 线程,一切操作都是⽆序的。前半句是指“线程内表现为串⾏语义”,后半句是指“指令重排序”现象和“⼯作内存主主 内存同步推迟”现象。
在Java内存模型中,为了功率是允许编译器和处理器对指令进⾏重排序,当然重排序不会影响单线程的运⾏结 果,可是对多线程会有影响。Java提供volatile来保证⼀定的有序性。最著名的例⼦就是单例形式⾥⾯的DCL(两层 检查锁)。别的,能够经过synchronized和Lock来保证有序性,synchronized和Lock保证每个时间是有⼀个线程 执⾏同步代码,相当于是让线程次第执⾏同步代码,⾃然就保证了有序性。
为了让⼤家更好了解可⻅性和有序性,这个就不得不了解“内存模型”、“重排序”和“内存屏障”,因为这三个概 念和他们联系⾮常亲近。
二:内存模型
JMM决议⼀个线程对同享变量的写⼊何时对另⼀个线程可⻅,JMM界说了线程和主内存之间的笼统联系:同享变量 存储在主内存(Main Memory)中,每个线程都有⼀个私有的本地内存(Local Memory),本地内存保存了被该线 程使⽤到的主内存的副本拷⻉,线程对变量的一切操作都有必要在⼯作内存中进⾏,⽽不能直接读写主内存中的变 量。
关于一般的同享变量来讲,线程A将其修正为某个值发⽣在线程A的本地内存中,此刻还未同步到主内存中去;⽽线 程B现已缓存了该变量的旧值,所以就导致了同享变量值的不⼀致。解决这种同享变量在多线程模型中的不可⻅性 问题,能够使⽤volatile、synchronized、final等,此刻A、B的通讯进程如下:
- ⾸先,线程A把本地内存A中更新过的同享变量刷新到主内存中去;
- 然后,线程B到主内存中去读取线程A之前已更新过的同享变量。
JMM经过操控主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可⻅性保证,需求留意的 是,JMM是个笼统的内存模型,所以所谓的本地内存,主内存都是笼统概念,并不⼀定就实在的对应cpu缓 存和物理内存。
总结⼀句话,内存模型JMM操控多线程对同享变量的可⻅性!!!
三:重排序
重排序是指编译器和处理器为了优化程序性能⽽对指令序列进⾏排序的⼀种⼿段。
重排序需求恪守⼀定规矩:
- 重排序操作不会对存在数据依靠联系的操作进⾏重排序。⽐如:a=1;b=a; 这个指令序列,因为第⼆个操作依 赖于第⼀个操作,所以在编译时和处理器运⾏时这两个操作不会被重排序。
- 重排序是为了优化性能,可是不管怎样重排序,单线程下程序的执⾏成果不能被改动。 ⽐如: a=1;b=2;c=a+b这三个操作,第⼀步(a=1)和第⼆步(b=2)因为不存在数据依靠联系, 所以或许会发⽣重排 序,可是c=a+b这个操作是不会被重排序的,因为需求保证终究的成果⼀定是c=a+b=3。
重排序在单线程下⼀定能保证成果的正确性,可是在多线程环境下,或许发⽣重排序,影响成果,请看下⾯的示例 代码:
class ReorderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1; //1
flag = true; //2
}
public void reader() {
if (flag) { //3
int i = a * a; //4
System.out.println(i);
}
}
}
flag变量是个符号,⽤来标识变量a是否已被写⼊。这⾥假设有两个线程A和B,A⾸先执⾏writer()⽅法,随后B线程 接着执⾏reader()⽅法。线程B在执⾏操作4时,输出是多少呢?
答案是:或许是0,也或许是1。
因为操作1和操作2没有数据依靠联系,编译器和处理器能够对这两个操作重排序;同样,操作3和操作4没有数据依 赖联系,编译器和处理器也能够对这两个操作重排序。让我们先来看看,当操作1和操作2重排序时,或许会产⽣什 么效果?请看下⾯的程序执⾏时序图:
如上图所示,操作1和操作2做了重排序。程序执⾏时,线程A⾸先写符号变量flag,随后线程B读这个变量。因为条 件判别为真,线程B将读取变量a。此刻,变量a还底子没有被线程A写⼊,在这⾥多线程程序的语义被重排序损坏 了!最终输出i的成果是0。
温馨提示:这⾥其实了解起来有点绕,⽐如线程A先执⾏了writer(),然后线程B执⾏reader(),关于线程A, 怎样会有这个重排序呢?其实这个重排序,是对线程B⽽⾔的,不是线程A哈!
有了线程B这第⼀视⻆,我们再了解⼀下,虽然线程A将writer()执⾏了,执⾏次第是a=1,flag=true,可是对 于线程B来说,因为重排序,线程B是依据重排序后的成果去执⾏的,所以才会呈现上述异常情况,这么给⼤ 家解说,是不是就清晰很多呢?
下⾯再让我们看看,当操作3和操作4重排序时会产⽣什么效果(凭借这个重排序,能够趁便阐明操控依靠性)。下 ⾯是操作3和操作4重排序后,程序的执⾏时序图:
在程序中,操作3和操作4存在操控依靠联系。当代码中存在操控依靠性时,会影响指令序列执⾏的并⾏度。为此, 编译器和处理器会采⽤猜想(Speculation)执⾏来克服操控相关性对并⾏度的影响。以处理器的猜想执⾏为例, 执⾏线程B的处理器能够提早读取并计算a*a,此刻成果为0,然后把计算成果暂时保存到⼀个名为重排序缓冲 (reorder buffer ROB)的硬件缓存中。当接下来操作3的条件判别为真时,就把该计算成果写⼊变量i中。
从图中我们能够看出,猜想执⾏实质上对操作3和4做了重排序。重排序在这⾥损坏了多线程程序的语义!因为 temp的值为0,所以最终输出i的成果是0。
那怎么避免重排序对多线程的影响呢,答案是“内存屏障”!
四:内存屏障
为了保证内存可⻅性,能够经过volatile、final等润饰变量,java编译器在⽣成指令序列的恰当方位会插⼊内存屏障 指令来禁⽌特定类型的处理器重排序。内存屏障主要有3个功用:
- 它保证指令重排序时不会把其后⾯的指令排到内存屏障之前的方位,也不会把前⾯的指令排到内存屏障的后 ⾯;即在执⾏到内存屏障这句指令时,在它前⾯的操作现已全部完成;
- 它会强制将对缓存的修正操作⽴即写⼊主存;
- 如果是写操作,它会导致其他CPU中对应的缓存⾏⽆效。
假设我对上述示例的falg变量经过volatile润饰:
class ReorderExample {
int a = 0;
boolean volatile flag = false;
public void writer() {
a = 1; //1
flag = true; //2
}
public void reader() {
if (flag) { //3
int i = a * a; //4
System.out.println(i);
}
}
这个时候,volatile禁⽌指令重排序也有⼀些规矩,因为篇幅原因,改规矩将会鄙人⼀章讲解,依据happens before规矩,这个进程建⽴的happens before 联系能够分为两类:
- 依据程序次第规矩,1 happens before 2; 3 happens before 4。
- 依据volatile规矩,2 happens before 3。
- 依据happens before 的传递性规矩,1 happens before 4
happens before规矩,其实就是重排序规矩建⽴的代码前后依靠联系。
温馨提示:这⾥⼤家或许会有疑问,1、3的规矩我了解,可是关于2,为什么“2 happens before 3”,还记得 前⾯讲的“内存模型”么?因为你对变量flag指定了volatile,所以当线程A执⾏完后,变量flag=true会直接刷到 内存中,然后B⻢上可⻅,所以说2⼀定是在3前⾯,不或许因为重排序,导致3在2前⾯执⾏。(然后还要提 示⼀下,这⾥执⾏时有个前提条件,就是线程A执⾏完,才能执⾏线程B⾥⾯的逻辑,因为线程A不执⾏完, flag⼀直是false,线程B底子就进不到主流程,所以你也能够直接了解为线程A执⾏完后,再执⾏线程B,才 有这么个先后联系。)
上述happens before联系的图形化表现形式如下:
在上图中,每⼀个箭头链接的两个节点,代表了⼀个happens before 联系。⿊⾊箭头表明程序次第规矩;橙⾊箭 头表明volatile规矩;蓝⾊箭头表明组合这些规矩后提供的happens before保证。
这⾥A线程写⼀个volatile变量后,B线程读同⼀个volatile变量。A线程在写volatile变量之前一切可⻅的同享变量, 在B线程读同⼀个volatile变量后,将⽴即变得对B线程可⻅。
五:总要有总结
今天讲解了Java并发编程的3个特性,然后基于⾥⾯的两个特性“可⻅性”和“有序性”引出⼏个重要的概念,分别为“内 存模型JMM”、“重排序”和“内存屏障”,这个对后续了解volatile、synchronized、final,以及避免使⽤的各种坑, 真的是⾮常⾮常重要,所以如果想在Java上有所成果,并发的基础知识一定要结实把握哦 ,本系列会一步一步深化!!!
这篇⽂章是我对Java并发编程的⼊⻔⽂章,后⾯会持续分别写volatile、synchronized、final,相关内容现已看完,后续直接收拾输出即可。就最终还有干货实战部分。
因为博主目只在发文目前没有对应公众号,有些内容借鉴一个老哥 ,他公众号 公众号:楼仔,能够关注下,我们下期再会!
本文正在参与「金石计划」