java 并发

并发概览
并发编程背景概述[建议改成“并发编程背景概述”或者“CPU和内存发展概述”。]
在早期,计算机采用的都是冯诺依曼架构,即CPU-内存架构。运行在计算机上的程序也是顺序执行的,当一个程序执行完,才能执行另一个。但很快发现顺序执行效率非常低下,当一个程序被阻塞时,整个计算机都被阻塞。随着计算机技术的发展,于是出现了多道程序设计,允许一个计算机可以同时运行多个进程,当一个进程被阻塞时可以让出CPU允许其他进程执行。但是随着CPU和内存的发展,CPU和内存之间的速度差越来越大,CPU等待内存I/O成为了主要瓶颈。[s]于是在CPU和内存之间增加了高速缓存,高速缓存是和CPU绑定的。但随着单个CPU的性能已经到达了物理极限,于是出现了多CPU架构,每个CPU都有自己的缓存。在多CPU架构中,每个CPU上都可以运行一个进程。相对来看CPU上进行进程切换是比较耗时的,因为不同的进程对计算机资源的需求(内存,缓存,寄存器)差异很大,导致每次切换进程需要更换大量的计算机资源。于是出现了线程,线程属于进程。在进程内部可以划分出多个线程,当一个线程被阻塞时,可以切换到其他线程。[描述突兀。可以和进程进行对比描述。]因为线程属于进程,而且比进程更加轻量级,线程之间切换更加高效。
高速缓存
缓存:这里的缓存指的是物理硬件缓存,并不是软件组件缓存。缓存会把经常读取或将要读取的数据存储起来,当CPU读取这些数据时可以从缓存里进行读取,当CPU写数据时,也不会直接对内存进行写入,而是将数据写入缓存,再从缓存写入到内存。CPU对缓存访问要比对内存访问快的多。
进程概念
进程:一个正在运行的程序就是一个进程[请解释的再具体一些。],例如运行一个hello word就是一个进程,同时运行两个hello word,就是两个进程。
线程概念
线程:CPU调度的最小单位。当一个进程被阻塞时,可以不切换进程,而是让该进程的线程进行切换。
并发概念
并发分为进程并发和线程并发。进程并发指的是多个进程都在运行,线程并发指的是多个线程都在运行。并发是有一个阈值的,当并发数小于阈值时并发可以提高CPU执行效率,当并发数超过阈值时,更多的并发并不能提高CPU效率,反而会浪费计算机资源。
并发问题
进程并发不会导致数据错误(进程之间基本不会共享数据),但线程并发如果不对共享资源进行访问控制可能会引发数据错误。假设这样一种场景, foo=0,线程A和线程B要对foo执行foo++,线程A先执行,但线程A还没有执行完毕,线程B也开始执行,因此线程B读到的值也是foo=0,线程A执行完毕foo=1, 线程B执行完毕foo=1.这样就产生了脏数据,但期望值是foo=2.
Java内存模型
JMM
java是运行在java虚拟机(JVM)上的,但本质上还是运行在操作系统和硬件上的。Java早期的一个宣传口号就是:”Write Once, Run Anywhere”。那如何实现呢?通过JVM对操作系统和硬件进行封装,对计算机资源做了进一步的抽象和统一。在JVM中,就包含了对内存的抽象,也就是java内存模型JMM。JMM的作用就是屏蔽掉操作系统和硬件所导致的内存访问的差异性。JMM定义了主存(Main Memory)和工作内存(Working Memory),每条线程都有自己的工作内存。主存保存了各个线程的共享变量,工作内存存储了对应线程所使用的共享变量的副本和本地变量。线程访问共享变量时,是不能直接访问主存的,线程只能访问工作内存中的数据。工作内存可以访问主存中的数据,工作内存可以类比为计算机中的缓存(cache)。
图2-1java内存模型

原子操作
原子在古希腊中指的是不可分割的最小物质,强调的是不可分割。原子操作指的是不可分割的操作,其他操作无法读取到原子操作的中间状态。
读取数据
CPU访问数据需要通过一系列的CPU指令。在JMM中访问数据类似,也必须通过一系列的操作。线程访问数据时需要通过以下8中操作,lock, unlock,read,load,use,assign,store,write每一种操作都是原子操作。例如线程A读取数据,需要执行read,load两个原子操作,但是在这两个原子操作中间还可以发生其他原子操作。因此线程A读取数据不是原子的。同样进行写数据,需要进行store和write操作,因此写操作也不是原子操作。
指令重排序
指令流水
为了提高CPU执行效率,将指令执行拆分为了流水线结构。指令的执行分为以下几个步骤:指令读取,指令编译,读取操作数,指令执行,写结果。[可以具体说明一下。]当一条指令执行完指令读取,然后执行指令编译,另一条指令就可以执行编译了。CPU将时间量化,将读取一个指令字的最短时间规定为CPU周期。假设CPU执行指令读取,指令编译,读取操作数,指令执行,写结果几个操作分别需要一个CPU周期,那么执行完这条执行需要占用5个CPU周期。采用流水线结构之后,CPU可以一次读入5条指令执行,相当于只占用了一个CPU周期。
多级指令流水
CPU为了进一步提高指令执行效率,采用了多级指令流水,即CPU内有多条指令流水线,每条指令流水线都可以执行多条指令。
多级指令流水3-1

乱序执行
采用多级指令流水虽然可以高效利地用CPU,但是也会导致一些问题。程序是顺序执行的,一条指令执行完毕,才可以执行另一条,但由于指令分布在多条流水线上,后面的指令有可能先执行完毕。为了保证程序的正确性,编译器会禁止对某些指令进行乱序执行,比如逻辑结构,if,switch等等,因此在单线程的情况下乱序执行能够保证正确,并提高效率。
但是在多线程的环境下,指令乱序执行仍然会导致错误。因为在多线程情况下,一个线程的执行需要依‘观察’另一个线程,考虑以下情况:
变量x=0,变量f=0,线程A中当f=0时,一直进行循环,f!=0是会print x,线程B中对x进行赋值,x=42,对f进行赋值,f=1。
线程A:
while (f==0);
// 插入内存屏障
print x;
线程B:
x = 42;
// 插入内存屏障
f = 1;
对于这段程序,期望值是x=42,但有可能输出的不是42。线程B进行乱序执行,f=1先执行,然后线程A执行,检测f!=0,print x=0;同样线程A可能进行乱序执行,print x先执行,然后再执行f的检查操作。
内存屏障
内存屏障(memory barrier):该指令之前的操作执行完毕之后,才可以执行该指令之后的操作。在单个CPU的情况内存屏障没有什么意义,但是在多CPU的情况下,就可以避免指令乱序执行导致的错误。上例中,可以在注释处插入内存屏障来保证程序的正确性。内存屏障属于CPU指令级别的操作,在高级程序语言中是无法直接使用,但高级语言会通过一些特性,进行隐式的调用,在java中就是volatile关键字。
Volatile
Volatile语义
Volatile关键字使用的非常少,很多开放人员都无法正确的使用它。线程访问volatile修饰的变量时会遵循以下规则:
1进行多操作时,会把主存中的数据更新到工作内存中,然后从工作内存中读取数据。
2进行写操作时,会把数据写入到工作内存中,然后从工作内存中再同步到主存中。
因此被volatile修饰的变量每次都可以读取到最新的数据。但是并不能保证在多线程的情况下的正确性。考虑以下情况,foo=0, 线程A读取foo=0, 线程B读取foo=0,线程A执行foo++写入工作内存,并刷新主存foo=1,线程B执行foo++写入工作内存foo=1,并刷新主存foo=1,这样仍然会产生脏数据。volatile本质上是实现了happens-before约束。
happens-before
happens-before:如果操作A先发生,然后操作B发生,操作B是会被操作A影响到的。这看起来和自然界的公理一样。为什么说volatile实现了happens-before呢?虽然读操作和写操作是原子操作,但是JMM中有一个类似于缓存的概念—工作内存。工作内存并不会把数据实时的刷新到主存中,也不会从主存中实时的获取数据。通过volatile的工作内存和主存之间的数据同步策略,可以实现happens-before。
volatile应用场景
volatile适用于这样的多线程场景,至多只有一个线程可以对volatile变量进行写操作,其他线程只有读操作。比如有一个多线程应用,在应用启动的时候需要做大量的初始化和检查操作,当这些操作执行完毕之后才可以提供服务,可以称之为开关服务。
开关服务伪代码
volatile switch = false;
public void run(){
while(!switch){
//do something
}
}
public void start(){
switch = true;
}
[缺少资源引用]结束语
计算机技术在不断地发展,从经典的内存–CPU架构到内存–多级缓存–CPU架构,从单核到多核,从单指令执行到多级指令流水。每一次技术的升级都会极大的提升计算机性能,但对软件设计也 要求更加苛刻。java是一门流行的高级程序语言,通过虚拟机的形式屏蔽掉了计算机硬件和操作系统的差异,然后提出了统一的主存–工作内存–线程模型。在java中会碰到拥有奇怪特性的volatile关键字。通过了解硬件特性和JMM,volatile关键字并没有那么诡异

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值