Java高并发

背景

  • 提升计算机的资源利用率
  • 提高计算机的处理速度
  • 等等吧

线程通信

通信就是线程之间交换信息,共享变量等。
这种通信机制有两种

通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:
共享内存和消息传递。  
a) 在共享内存的并发模型里,线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信。(重点)  
b) 在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过发送消息来显式进行通信。

线程之间如何同步
  同步是指程序中用于控制不同线程间操作发生相对顺序的机制。

在共享内存的并发模型里,同步是显示进行的。因为程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。
在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的

1、共享内存

就是共享内存中的公共对象来进行通信

2、消息传递

wait/notify 实现线程间的通信
join
ThreadLocal

线程同步

显示的指定某个方法或代码需要互斥执行,线程同步的方法有:

synchronized
volatile
ReentrantLock
ThreadLocal
使用阻塞队列
atomic 原子操作

Java的并发模型采用的是共享内存模型

java线程之间的通信有Java内存模型JMM控制的
JMM决定一个线程对共享变量的写入何时对另一个线程可见
共享变量存储在主存中
每个线程有一个私有的本地内存供该线程操作
 
线程间通信的话就是通过 更新、读取 共享的变量来完成的

JVM 对Java内存模型的实现

  • 程序计数器:线程私有
  • Java虚拟机栈:线程私有
描述Java方法执行的内存模型
每个方法执行的时候都会同时创建一个栈帧用于存储局部变量表、操作栈等信息。
  • 本地方法栈

执行Native方法

  • Java堆

存放对象的实例
线程共享
GC管理的主要区域

  • 方法区

线程共享
存放已被虚拟机加载的类信息、常量、静态变量等

堆中的对象可以被多个线程共享。
两个线程同时调用同一个对象的同一个方法,每个线程就会拷贝一份到自己的线程栈中

CPU执行多线程

cpu的组成:

  • 运算单元
  • 控制单元

控制单元是有许多寄存器组成
控制器从cache中取出一条指令,并指出下一条指令在cache中的位置

  • cache

cpu执行的速度很快,但是从内存中读取数据的速度跟不上cpu的执行速度,就产生了cache,
cpu读取cache中的数据,cache从内存中读取数据

cpu 执行的步骤

控制单元从程序获取指令
cache 从内存中获取数据
运算单元执行指令和操作cache数据

Java内存模型和硬件是有区别的,当程序执行的时候就会面临一些问题

可见性

当多个线程同时操作同一个共享对象时,如果没有合理的使用volatile和synchronization关键字,一个线程对共享对象的更新有可能导致其它线程不可见。

竞争现象

如果多个线程共享一个对象,如果它们同时修改这个共享对象,这就产生了竞争现象

CPU缓存一致性

MESI(缓存一致性)

https://www.cnblogs.com/yanlong300/p/8986041.html

指令重排

在执行程序时,为了提高性能,编译器和处理器会对指令重排序

1、编译器优化重排序
编译器在不改变单线程程序的语义的前提下,可以重新安排语句的执行顺序
2、指令级并行的重排序
如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
3、内存系统重排序
处理器使用缓存和读写缓冲区,使得加载和存储操作看上去可能是在乱序执行

内存屏障(Memory Barrier )

每个cpu都有自己的缓冲区,写缓冲区有很多好处但它只对自己所在的处理器所见。这个特性导致处理器对内存的读/写操作的执行顺序不一定与内存实际发生的读/写顺序一致。

通过内存屏障可以禁止特定类型处理器的重排序,从而让程序按我们预想的流程去执行。内存屏障,又称内存栅栏,是一个CPU指令

  • a)确保一些特定操作执行的顺序;
  • b)影响一些数据的可见性(可能是某些指令执行后的结果)。

为了保证内存的可见性,java编译器在生成指令序列的适当位置插入一个内存屏障来禁止特定类型的处理器重排序,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。

内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到各个CPU的缓存(lazy型刷新, CPU监听数据总线将缓存数据标记为invalid,用到时再重内存中载入),这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。

volatile 就是使用内存屏障

这意味着如果你对一个volatile字段进行写操作,你必须知道:

  • 1、一旦你完成写入,任何访问这个字段的线程将会得到最新的值。
  • 2、在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。

性能影响

内存屏障作为另一个CPU级的指令,没有锁那样大的开销。内核并没有在多个线程间干涉和调度。但凡事都是有代价的。内存屏障的确是有开销的——编译器/cpu不能重排序指令,导致不可以尽可能地高效利用CPU,另外刷新缓存亦会有开销。所以不要以为用volatile代替锁操作就一点事都没。

happens-before

从jdk5开始,java使用新的JSR-133内存模型,基于happens-before的概念来阐述操作之间的内存可见性。

在JMM中,如果一个操作的执行结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系,这个的两个操作既可以在同一个线程,也可以在不同的两个线程中。

与程序员密切相关的happens-before规则如下:

程序顺序规则:一个线程中的每个操作,happens-before于该线程中任意的后续操作。
监视器锁规则:对一个锁的解锁操作,happens-before于随后对这个锁的加锁操作。
volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。
传递性规则:如果 A happens-before B,且 B happens-before C,那么A happens-before C。

注意:两个操作之间具有happens-before关系,并不意味前一个操作必须要在后一个操作之前执行!仅仅要求前一个操作的执行结果,对于后一个操作是可见的,且前一个操作按顺序排在后一个操作之前。

as-if-serial

不管如何重排序,都必须保证代码在单线程下的运行正确,连单线程下都无法正确,更不用讨论多线程并发的情况,所以就提出了一个as-if-serial的概念。
as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。(强调一下,这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。)但是,如果操作之间不存在数据依赖关系,这些操作依然可能被编译器和处理器重排序。

volatile和synchronized的区别

volatile 就是使用内存屏障

首先需要理解线程安全的两个方面:执行控制和内存可见。
执行控制的目的是控制代码执行(顺序)及是否可以并发执行。

内存可见控制的是线程执行结果在内存中对其它线程的可见性。根据Java内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存。

synchronized关键字解决的是执行控制的问题,它会阻止其它线程获取当前对象的监控锁,这样就使得当前对象中被synchronized关键字保护的代码块无法被其它线程访问,也就无法并发执行。更重要的是,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中,从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作,都happens-before于随后获得这个锁的线程的操作。

volatile关键字解决的是内存可见性的问题,会使得所有对volatile变量的读写都会直接刷到主存,即保证了变量的可见性。这样就能满足一些对变量可见性有要求而对读取顺序没有要求的需求。

使用volatile关键字仅能实现对原始变量(如boolen、 short 、int 、long等)操作的原子性,但需要特别注意, volatile不能保证复合操作的原子性,即使只是i++,实际上也是由多个原子操作组成:read i; inc; write i,假如多个线程同时执行i++,volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况。

  • 区别

volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取
synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

volatile仅能使用在变量级别;
synchronized则可以使用在变量、方法、和类级别的

volatile仅能实现变量的修改可见性,不能保证原子性;
synchronized则可以保证变量的修改可见性和原子性

volatile不会造成线程的阻塞;
synchronized可能会造成线程的阻塞。

volatile标记的变量不会被编译器优化;
synchronized标记的变量可以被编译器优化

https://blog.csdn.net/suifeng3051/article/details/52611310
https://blog.csdn.net/suifeng3051/article/details/52611233
https://www.cnblogs.com/yuanfy008/p/9252555.html
https://www.jianshu.com/p/8a58d8335270
https://www.jianshu.com/p/421c1f1425f5

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值