线程:
一: 多线程的三大特性
-
原子性:一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。 同步操作即保证多线程的原子性
一个很经典的例子就是银行账户转账问题:
比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。这2个操作必须要具备原子性才能保证不出现一些意外的问题。
我们操作数据也是如此,比如 i = i+1;其中就包括,读取i的值,计算i,写入i。这行代码在Java中是不具备原子性的,则多线程运行肯定会出问题,所以也需要我们使用同步和lock这些东西来确保这个特性了。
原子性其实就是保证数据一致、线程安全的一部分 -
可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程2没看到这就是可见性问题。 使用volatile修饰全局变量达到可见性。
使用Volatile关键字将解决线程之间可见性, 强制线程每次读取该值的时候都去“主内存”中取值
-
有序性:程序执行的顺序按照代码的先后顺序执行
一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
如下:
int a = 10; //语句1 int r = 2; //语句2
a = a + 3; //语句3
r = a*a; //语句4
则因为重排序,他还可能执行顺序为 2-1-3-4,1-3-2-4
但绝不可能 2-1-4-3,因为这打破了依赖关系。
显然重排序对单线程运行是不会有任何问题,而多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了。
二:Java内存模型(JMM)
- 概述:Java内存模型(Java Memory Model,JMM) JMM主要是为了规定了线程和内存之间的一些关系。根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。
1. 为什么要有内存模型?
- 内存模型并不是真正的存储区域,JMM并不像 JVM内存结构(java运行时内存区域)一样是真实存在的,他只是一个抽象的概念。JMM就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能得到一致效果的机制及规范。目的是解决由于多线程通过共享内存进行通信时,存在的原子性、可见性(缓存一致性)以及有序性问题。
1.1 硬件内存结构
-
CPU: 一般一个CPU会有多个核,又或者一个CPU在多个线程之间告诉切换执行,所以如果使用java开了一个多线程的任务,CPU在这些线程之间快速切换执行,我们就可以认为这些线程士兵发进行的
-
CPU Register:CPU寄存器。CPU寄存器是CPU内部集成的,所以CPU其存储的数据会比告诉缓存区的数据快
-
CPU Cache Memory:CPU高速缓存,相对于硬盘读取速度来说内存读取的效率非常高,但是与 CPU 还是相差数量级,所以在 CPU 和主存间引入了多级缓存,目的是为了做一下缓冲。
-
Main Memory:主内存缓存的数据要比前面两者缓存的东西要多,但读取速度要慢
1.2 缓存一致性问题
-
使用高速缓存解决了CPU和主存速率不批配的问题,但这就引出了一个问题:缓存一致性
-
概述: 当程序在运行过程中,会将运算需要的数据从主存复制一份到cup的高速缓存当中,那么cpu进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束后,再将高速缓存中的数据刷新到主存当中。
例如:主内存中存在 i = 0;
对其进行 i = i + 1操作
当线程执行这个语句时,会先从缓存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行命令对i进行加1操作,然后将数据写入高速缓存。最后将高速缓存中的最新的值刷新到主存当中。
这个代码在单线程中运行是没有问题的,但在多线程中运行就有可能会有问题了。假如我们现在开两个线程A,B,让它们都执行i = i + 1,由于每个线程都有自己的高速缓存,在线程执行之初会将i = 0存入自己的告诉缓存中,假使A先执行完,此时A内的i = 1,然后将i = 1 刷入主内存,此时主内存中的i = 1,但此时B线程并不知道A线程已经将i值修改,此时B线程中的i值让为0,在执行完计算赋值逻辑后B线程的 i = 1,此时B线程将i的值刷入主内存中,我们发现在执行完A,B两个线程后主内存中的i的值为1,而不是2,这就是缓存一致性问题,而造成这种问题的原因为不同线程对于同一变量进行修改,这种被多个线程访问的变量为共享变量。
-
解决缓存一致性的方法:
- 通过对对共享变量进行操作的逻辑加锁,即使其成为同步代码块
- 通过缓存一致性协议
-
在早期的CPU当中,是通过在总线上加锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加锁的话,也就是说阻塞了其他CPU对其他部件访问,从而使得只有一个CPU能使用这个变量的内存。比如上面例子中,如果一个线程在执行i=i+1,如果在执行这段代码的过程中,在总线上发出了锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致问题。
但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。
所以就出现了缓存一致性协议。该协议保证了每个缓存中使用的共享变量的副本是一致的。它的核心思想是:当CPU向内存写入数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存行是无效的,那么它就会从内存重新读取。java中的volatile就是该协议的实现,
1.3 处理器优化和指令重排
-
为了提升性能在 CPU 和主内存之间增加了高速缓存,为了更进一步的CPU的执行效率,出现了处理器优化
- 为了使处理器内部的运算单元能够最大化被充分利用,处理器会对输入代码进行乱序执行处理,这就是处理器优化。
-
指令重排:现代编程语言的编译器也会做类似的优化,比如像 Java 的即时编译器(JIT)会做指令重排序。
重排序的分类:
- 编译器优化的重排序。编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序。由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
2. Java 内存模型
-
Java 内存模型是一种规范,定义了很多东西:
-
所有的变量都存储在主内存(Main Memory)中。
-
每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。
-
线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。
-
不同的线程之间无法直接访问对方本地内存中的变量。
-
本地内存向竹内存中书信数据的国臣是由JVM控制的,一般在执行完某以完整逻辑后会将此逻辑中的修改后的共享变量的值刷新至主内存之中
-
线程与线程之间无法直接通讯(即线程A无法访问线程B的值),线程与线程之间的通讯是通过主内存链接起来的(线程A先将值存入主内存,然后线程B在从竹内存读取,完成线程与线程之间的通讯)。
-
为了更好的控制主内存和本地内存的交互,JMM定义的了八种操作来实现:
lock:锁定。作用于主内存的变量,把一个变量标识为一条线程独占状态。
unlock:解锁。作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read:读取。作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
load:载入。作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use:使用。作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
assign:赋值。作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store:存储。作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
write:写入。作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
三: Volatile
-
简介: volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
-
锁的互斥和可见性:
锁提供了两种主要特性:互斥(mutual exclusion)和 可见性(visibility)
-
互斥即依次只允许一个线程持有某个特定的锁,一次只有一个线程能够使用该共享数据
-
可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的。也即当一条线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的。如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
-
对变量的写操作不依赖于当前值。
-
该变量没有包含在具有其他变量的不变式中。
-
这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。事实上就是保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
-
-
Java的内存模型JMM以及共享变量的可见性
JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
对于普通的共享变量来讲,线程A将其修改为某个值发生在线程A的本地内存中,此时还未同步到主内存中去;而线程B已经缓存了该变量的旧值,所以就导致了共享变量值的不一致。解决这种共享变量在多线程模型中的不可见性问题,较粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了,比较合理的方式其实就是volatile。
注意:JMM是个抽象的内存模型,所以所谓的本地内存,主内存都是抽象概念,并不一定就真实的对应cpu缓存和物理内存
-
volatile变量的特性
-
保证可见性,不保证原子性:
- 当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新至主内存中
- 这个写操作会导致其他线程中的volatile变量缓存无效
-
禁止指令重排序
-
当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
-
在进行指令优化时,不能将对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
总结:即执行到volatile变量时,其前面的所有语句都执行完,后面的所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见
-
-
-
volatile原理
-
概述: volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。
-
volatile实现可见性通过内存屏障和禁止重排序优化来实现可见性。
-
对共享变量进行写操作后,加入一条store屏障指令,强制将共享变量的值刷新到主内存;
-
对共享变量进行读操作前,加入一条load屏障指令,强制从主内存中将最新值刷新到工作内存;
-
-
内容屏障提供的三个功能:
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 它会强制将对缓存的修改操作立即写入主存
- 如果是写操作,它会导致其他CPU中对应的缓存行无效。
-