并发编程相关知识
Posted jis121
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发编程相关知识相关的知识,希望对你有一定的参考价值。
1、并发编程领域的关键问题
线程之间的通信
线程间的同步
1.1 线程之间的通信
线程之间的通信机制有两种,共享内存和消息传递。
在共享内存的并发模型里,线程之间通过写-读内存中的公共状态来隐式进行通信,典型的共享内存通信方式就是通过共享对象进行通信。
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信,在java中典型的消息传递方式就是wait()和notify()
1.2 线程间的同步
同步是指程序用于控制不同线程之间操作发生相对顺序的机制
2、计算机内存模型
处理器要与内存交互才可以完成一项执行任务(如读取运算数据、存储运算结果等)
早期计算机中cpu和内存的速度是差不多的,但在现代计算机中,cpu的指令速度远超内存的存取速度
2.1 引入高速缓存区
所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲
将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了
2.2 带来的问题
引入高速缓存区,很好地解决了处理器与内存的速度矛盾,但是也为计算机系统带来更高的复杂度,缓存一致性问题(Cache Coherence)。
在多处理器系统中,每个处理器都有自己的高速缓存,仅对当前处理器可见,而它们又共享同一主内存(MainMemory),
当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致。
3、java 内存模型
Java的并发采用的是共享内存模型
JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式,JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。
JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程需要用到的变量的副本。
3.1 JVM对Java内存模型的实现
在JVM内部,Java内存模型把内存分成了两部分:线程栈区和堆区
JVM中运行的每个线程都拥有自己的线程栈
所有原始类型的局部变量都直接保存在线程栈当中,对于它们的值各个线程之间都是独立的。
(boolean,byte,short,char,int,long,float,double)
堆区包含了Java应用创建的所有对象信息,不管对象是哪个线程创建的,其中的对象包括原始类型的封装类(如Byte、Integer、Long等等)
一个局部变量如果是原始类型,那么它会被完全存储到栈区。 一个局部变量也有可能是一个对象的引用,这种情况下,这个本地引用会被存储到栈中,但是对象本身仍然存储在堆区。
3.2 Java内存模型带来的问题
3.2.1 可见性问题
CPU中运行的线程从主存中拷贝共享对象obj到它的CPU缓存,把对象obj的count变量改为2。
但这个变更对于其他CPU中的线程不可见,因为这个更改还没有flush到主存中,要解决共享对象可见性这个问题,我们可以使用java volatile关键字或者是加锁
线程A和线程B共享一个对象obj。假设线程A从主存读取Obj.count变量到自己的CPU缓存,同时,线程B也读取了Obj.count变量到它的CPU缓存,
并且这两个线程都对Obj.count做了加1操作。此时,Obj.count加1操作被执行了两次,不过都在不同的CPU缓存中。
不管是线程A还是线程B先flush计算结果到主存,最终主存中的Obj.count只会增加1次变成2,尽管一共有两次加1操作。 要解决上面的问题我们可以使用java synchronized代码块。
3.3 Java内存模型中的重排序
重排序是为了提高执行效率
3.3.1 数据依赖性
如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。只要重排序两个操作的执行顺序,程序的执行结果就会被改变。
3.3.2 as-if-serial
不管如何重排序,都必须保证代码在单线程下的运行正确,连单线程下都无法正确,更不用讨论多线程并发的情况
3.4 解决在并发下的问题
3.4.1 内存屏障
禁止重排序
3.4.2 临界区
临界区内的代码可以重排序
3.4.3 Happens-Before
两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!
happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前
as-if-serial语义保证单线程内程序的执行结果不被改变,
happens-before关系保证正确同步的多线程程序的执行结果不被改变。
3.5 实现原理
内存语义:可以简单理解为 volatile,synchronize,atomic,lock 之类的在 JVM 中的内存方面实现
3.5.1 volatile的两个特性
(1)可见性
对volatile修饰的变量的修改,会及时刷到主内存中,并让其他线程已读取的此变量失效,使其他线程可以看到此线程的执行结果
(2)原子性
对于任意volatile变量的读/写具有原子性,但是类似于volatile++这种操作,不具备原子性
volatile写的内存语义如下:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
volatile读的内存语义如下:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
3.5.2 volatile的实现原理
有volatile变量修饰的共享变量进行写操作的时候会使用CPU提供的Lock前缀指令:
将当前处理器缓存行的数据写回到系统内存
这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
3.5.3 锁
当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。。
当线程获取锁时,JMM会重新获取该线程对应的本地内存中的共享变量。
3.5.4 synchronized的实现原理
使用monitorenter和monitorexit指令实现的:
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处
每个monitorenter必须有对应的monitorexit与之配对
任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态
3.5.5 锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。
(1)偏向锁:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。无竞争时不需要进行CAS操作来加锁和解锁。
(2)轻量级锁:无竞争时通过CAS操作来加锁和解锁。(CAS(自旋锁)——是一种锁的机制,不是状态)
(2)重量级锁:真正的加锁操作
以上是关于并发编程相关知识的主要内容,如果未能解决你的问题,请参考以下文章
全栈编程系列SpringBoot整合Shiro(含KickoutSessionControlFilter并发在线人数控制以及不生效问题配置启动异常No SecurityManager...)(代码片段