离上次写博客又隔了很久,心中有愧。在我不断使用Java的过程中,几乎都是拿来就用,就Java并发这块我还没有系统的梳理过,趁着国庆有空余时间,把它梳理一遍。以下部分内容参考相关书籍,以作学习之用,特此说明。
1.并行定律
随着科技的发展,集成电路上的晶体管数量也达到了物理极限,摩尔定律也随之不再那么有效,例如Amdahl定律和Gustafson定律代替它成为计算机性能发展的源动力。从这个演变也可以看出,计算机的性能发展也不得不从追求处理器频率到多核并行处理的发展过程。
1.1.定义
所谓阿姆达尔(Amdahl)定律,它定义了串行系统并行化后的加速比的计算公式和理论上限。
1.2.公式
就是其公式就是:
其中Sp就是加速比,T1是优化前系统耗时,Tp是优化之后系统耗时,p就是处理器个数。那么这个公式意义就是 加速比 = 优化前系统耗时 / 优化后系统耗时。
我们逐步看一下它的公式推导:
其中,p为处理器个数,F为串行比率,那么1-F就是并行的比例了。这个公式就是计算优化后的耗时公式,将这个公式代入加速比公式我们就可以得出CPU的处理器数量越多,那么加速比与系统的串行率就成反比:
我们不妨看个例子,假设现在有个系统是按如下方式串行运行的:
这个系统有三步,其中第一步和第三步都是100ms,第二步是200ms,整个串行的运行时间是400ms。那我们现在可能要对这个系统做个优化,已知这个系统是两个核心,那么如果Step2的操作内部由串行改为并行,那么理想情况可能是这样的:
我们看到Step2分解成并行的操作,那么代入公式得到最终它的加速比为1.2。我们不妨推算一下,假设处理器的个数为无穷大,那么Step2的操作耗时无限趋近于0,那么对于这个系统而言,它的加速比(300ms/200ms)最大也不过是1.5。也就是说,P越趋近于无穷大,那么Sp=1/F。
加速比越高,表明优化效果越好。根据Amdahl定律,使用多核的CPU对系统优化,优化的效果取决于CPU的数量和系统串行化程序的比重,如果仅仅提升Cpu数量,而不降低程序的串行比重,也是无法提高系统性能的。所以,我们要根本上去改变程序的串行行为,合理的并行与增加处理器数量,才能获得更大的性能提升。
1.3.Gustafson定律
Gustafson定律只是从不同的角度去阐述处理器个数、串行比例和加速比之间的关系。所以这里不再赘述。
2.Java内存模型
2.1.处理器、高速缓存、主存交互
提高计算机的性能并不是让计算机同时处理多个任务那样简单,处理器需要和内存交互,例如读取数据、存储运算结果,因为现代计算机的处理器能力太强,存储设备的读写速度与之相差太大,所以在存储设备和处理器之间加上高速缓存来作为处理器和内存之间的缓冲。这样的话CPU就不需要等待相对而言缓慢的内存读写了。
当高速缓存作为一种解决处理器与内存读写速度矛盾的手段时,带来了新的问题,那就是缓存一致性。处理器有对应的高速缓存,而它们又对应同一块主内存。当多个处理器的运算都涉及到同一个主内存时,该如何保证数据的一致性?所以为了解决一致性,又在处理器访问缓存时候遵循一些协议。
那么诸如Java虚拟机的内存模型之类就可以理解成,在特定的操作协议下,对特定的内存或者高速缓存进行读写的过程抽象。
除了高速缓存之外,处理器也会对输入代码乱序执行优化(Out-Of-Order Execution)优化,这种优化并不能保证处理器的执行顺序会和输入代码的顺序一致,但会保证最终输入的结果是一致的。与之相对应的,Java也存在着一套类似的机制,就是指令重排(Instruction Reorder)优化。
2.2.Java内存模型(JMM)
Java虚拟机定义了一套内存模型来屏蔽各种硬件和操作系统带来的内存访问差异,实现Java程序在各平台下达到一致的内存访问结果。
JMM主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储、取出的底层细节。这里的变量包括实例字段、静态字段和构成数组对象的元素,但不包括局部变量和方法参数,因为这些是线程私有的,不会被共享。
Java内存模型规定所有变量都存在主内存,每条线程都有自己的工作内存,线程所有对变量的操作都必须在工作内存中执行,线程的工作内存保存了被该线程使用到的变量主内存拷贝,不能直接读写主内存中的变量,线程之间变量值传递需要通过主内存完成。线程、主内存、工作内存交互如下:
2.3.内存间的交互操作
在主内存和工作内存之间的交互协议的具体细节,Java内存模型定义了8个操作来完成,虚拟机来保证这8个操作都是原子的。
操作 | 说明 | 描述 |
---|---|---|
lock | 锁定 | 作用于主内存的变量,将一个变量标识为一条线程独占的状态 |
unlock | 解锁 | 作用于主内存的变量,将一个标记为锁定状态的变量解锁,以便其它线程使用 |
read | 读取 | 作用于主内存的变量,将一个变量的值从主内存传输到线程的工作内存中,以便load操作使用 |
load | 载入 | 作用于工作内存的变量,将read操作读取过来的值放入工作内存的变量副本中 |
use | 使用 | 作用于工作内存的变量,将工作内存的值传递给执行引擎,虚拟机遇到一个需要使用变量的字节码指令就会这么做 |
assign | 赋值 | 作用于工作内存的变量,从执行引擎接受到的值赋给工作内存的变量,虚拟机遇到一个给变量赋值的字节码指令就会这么做 |
store | 存储 | 作用于工作内存的变量,将工作内存的变量的值传递给主内存中,以便write操作 |
write | 写入 | 作用于主内存的变量,它把store操作从工作内存中得到的变量赋值放入主内存的变量中 |
Java内存模型只要求两个操作必须按顺序执行,而没有保证是连续执行,也就是说两个指令之间是可以有其它指令的。Java内存模型还规定可在执行上述8种基本操作时必须满足以下的规则:
* 不允许read和load、store和write操作之一单独出现;
* 不允许一个线程丢弃它最近的assign操作,即assign操作之后必须将值同步给主内存;
* 不允许一个线程没发生过assign就把数据同步给主内存;
* 一个新的变量只能诞生在主内存,不允许工作内存直接使用一个未被(load和assign)的变量;
* 一个变量在同一时刻只允许同一条线程对其进行lock操作;
* 如果对一个变量执行lock,那么将清空这个变量在工作内存的此变量的值,在执行引擎使用这个变量之前,重新执行load和assign操作初始化工作内存的值;
* 如果一个变量事先没有被lock操作锁定,就允许对其或其它线程进行unlock操作;
* 对一个变量执行unlock操作之前,必须先同步回主内存;
2.4.原子性(Atomicity)、可见性(Visibility)、有序性(Ordering)
原子性(Atomicity):原子性是指一个操作是不可中断的,一旦一个操作开始,就不会被其它线程干扰。Java内存模型来直接保证原子性变量的操作包括read、load、assign、use、store和write,基本可以认为基本数据类型的读写是原子性的,但是double、long类型例外,这是它们的非原子性协定决定的。当然,Java内存模型还提供了lock和unlock来满足更大范围的原子性操作,这两个操作反映到字节码指令就是monitorenter和monitorexit隐式的操作,反映到代码上就是synchronized关键字。
可见性(Visibility):可见性是指一个线程修改了共享变量的值,其它线程能立即得知这个更改。Java内存模型是通过变量修改后将新值同步给主内存,在变量读取前从主内存刷新变量值依赖主内存作为传递媒介的方式来实现可见性的,无论这个变量是否被volatile修饰,但它们的区别是volatile变量的特殊规则能立即同步到主内存,以及每次使用前从主内存刷新,而普通变量不行。当然,除了volatile能实现可见性之外,synchronized和final同样可以。synchronized的可见性是通过“对一个变量执行unlock操作之前,必须把此变量同步回主内存中”这条规则获得的;而final的可见性是指,被final修饰的字段在构造器一旦初始化完成,并且构造器没把this的引用传递出去,那么在其它线程就能看见final字段的值。
有序性(Ordering):前面也提到,java会指令重排,代码顺序未必和指令执行顺序一致。Java提供了volatile和synchronized来保证线程之间操作的有序性。volatile关键字本身就禁止指令重排,而synchronized是由“一个变量在同一时刻只允许一条线程对其lock操作”这条规则获得。
2.5.Happen-Before原则
Java里的有序性除了靠volatile和synchronized两个关键字完成,其实还隐藏着先行发生(Happen-Before)原则,通过这个原则和之前的规则基本能解决并发环境下两个操作之间的冲突问题。
* 程序次序原则(Program Order Rule):一个线程内保证语义的串行;
* 管程锁定原则(Monitor Lock Rule):unlock操作必定在之后的同一个锁的lock操作之前;
* volatile规则(Volatile Variable Rule):volatile变量的写操作先行发生于后面这个变量的读操作;
* 线程启动规则(Thread Start Rule):线程的start()方法先于该线程其它的每一个动作;
* 线程终止规则(Thread Termination Rule):线程的所有操作都先于该线程的终结(Thread.join());
* 线程中断规则(Thread Interruption Rule):线程的interrupt()方法调用先行发生于被中断线程的代码检测到中断事件的发生;
* 对象终结规则(Finalizer Rule):一个对象的初始化完成先行与它的finalize()方法的开始;
* 传递性 (Transitivity):如果A操作先于B操作,B操作先于C操作,那么A必定先于C;
3.volatile
3.1.语义
Java内存模型基本是围绕原子性、有序性、可见性展开,而volatile关键字的语义,一是保证此变量对所有线程的可见性,二是禁止指令重排。可以看出,volatile不能保证原子性,这个需要通过加锁或者一些原子类来实现。
举个例子:
public class VolatileTest {
public static volatile int i = 0;
public static void increase() {
i++;
}
public static class IncreaseTask implements Runnable{
public void run() {
for (int y = 0; y < 10000; y++) {
increase();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(new IncreaseTask());
threads[i].start();
}
for (int i = 0; i < 10; i++) {
threads[i].join();
}
System.out.println(i);
}
}
在上面这段代码中,变量i用volatile修饰,循环10个线程,每个线程内部对i递增10000次,如果这段代码并发成功的话,预期的结果应该是100000。但是运行结果可见,每次的结果值都小于100000。
这个正是因为increase()方法内部对i递增的处理,也就是 i++ 这一段代码不是原子的,代码虽然只有一行,但是编译出来的字节码指令却有多个指令,而且每个指令本身未必就是原子的,因为这些指令还会转化成若干个本地机器码指令。不难分析出,每个线程取到i的值那一刻,volatile保证了这一刻取到的是正确的数据,但是继续往下执行的时候,这个值就可能已经被其它线程修改了,而此时的数据就变成过期的数据,同步到主内存中的数据就可能是一个较小的数据。
除了在操作递增时候加锁之外,使用AtomicInteger原子类代替int一样可以得到预期的结果。
3.2.volatile的可见性和指令重排
volatile修饰的变量,赋值后的指令会多出一个内存屏障,这个内存屏障会杜绝后面的指令排到前面去。这种内存屏障其实就是一个空操作,这个空操作指令是lock前缀,它的作用就是使得本CPU的Cache写入内存(write和store操作),该写入动作使得其它CPU或者别的内核无效化其Cache,所以通过这样的一个空操作,让volatile修饰的变量对其它CPU立即可见。也因此,这个空操作指令在同步到内存时,意味着所有的操作都已经执行完成,这样就形成了“指令排序无法越过屏障”的效果。
Java并发(一、概述)的更多相关文章
-
Java并发知识概述
1.Java内存模型的抽象结构 Java中,所有的实例.静态域和数组元素都存储在堆内存中,堆内存是线程共享的.局部变量,形参,异常处理参数不会在线程之间共享,所以不存在内存可见性问题,也就不受内存模型 ...
-
java并发:同步容器&;并发容器
第一节 同步容器.并发容器 1.简述同步容器与并发容器 在Java并发编程中,经常听到同步容器.并发容器之说,那什么是同步容器与并发容器呢?同步容器可以简单地理解为通过synchronized来实现同 ...
-
构建自己的Java并发模型框架
Java的多线程特性为构建高性能的应用提供了极大的方便,可是也带来了不少的麻烦.线程间同步.数据一致性等烦琐的问题须要细心的考虑,一不小心就会出现一些微妙的,难以调试的错误. 另外.应用逻辑和线程逻辑 ...
-
【Java 并发】详解 ThreadPoolExecutor
前言 线程池是并发中一项常用的优化方法,通过对线程复用,减少线程的创建,降低资源消耗,提高程序响应速度.在 Java 中我们一般通过 Exectuors 提供的工厂方法来创建线程池,但是线程池的最终实 ...
-
java并发程序——并发容器
概述 java cocurrent包提供了很多并发容器,在提供并发控制的前提下,通过优化,提升性能.本文主要讨论常见的并发容器的实现机制和绝妙之处,但并不会对所有实现细节面面俱到. 为什么JUC需要提 ...
-
Java并发编程系列-AbstractQueuedSynchronizer
原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/10566625.html 一.概述 AbstractQueuedSynchronizer简 ...
-
【Java并发编程六】线程池
一.概述 在执行并发任务时,我们可以把任务传递给一个线程池,来替代为每个并发执行的任务都启动一个新的线程,只要池里有空闲的线程,任务就会分配一个线程执行.在线程池的内部,任务被插入一个阻塞队列(Blo ...
-
Java 并发AQS
转载出处:http://www.cnblogs.com/waterystone/ 一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQu ...
-
构建Java并发模型框架
Java的多线程特性为构建高性能的应用提供了极大的方便,但是也带来了不少的麻烦.线程间同步.数据一致性等烦琐的问题需要细心的考虑,一不小心就会出现一些微妙的,难以调试的错误.另外,应用逻辑和线程逻辑纠 ...
随机推荐
-
.NET跨平台之旅:在Linux上将ASP.NET 5运行日志写入文件
在前一篇博文(增加文件日志功能遇到的挫折)中,我们遇到了这样一个问题:虽然有一些.NET日志组件(比如Serilog, NLog)已经开始支持.NET Core,但目前只支持控制台输出日志,不支持将日 ...
-
线性存储结构-Stack
Stack继承于Vector,是一个模拟堆栈结构的集合类.当然也属于顺序存储结构.这里注意Android在com.android.layoutlib.bridge.impl包中也有一个Stack的实现 ...
-
HTML+JS版本的俄罗斯方块
<!doctype html><html><head></head><body> <div id="box" st ...
-
CSS3 Media Queries 简介
原文链接:Introduction to CSS3 Media Queries 原文日期: 2014年2月21日 翻译日期: 2014年2月26日 翻译人员: 铁锚 简介 随着移动设备的日益普及,we ...
-
JS实现 阿拉伯数字金额转换为中文大写金额 可以处理负值
JS实现 阿拉伯数字金额转换为中文大写金额 可以处理负值 //************************* 把数字金额转换成中文大写数字的函数(可处理负值) ****************** ...
-
CH2401 送礼物(算竞进阶习题)
双向dfs 数据不是很大,但是如果直接暴搜的话2^45肯定过不了的.. 所以想到乱搞!!要让程序跑的更快,肯定要减下搜索树的规模,再加上这道题双搜的暗示比较明显(逃),所以就来乱搞+双搜求解 所以先从 ...
-
thinkpad e系列 装win7过程
电脑买回来时是win8系统,但是卡顿的厉害,装成win7,win8装win7流程还是比较复杂,后来又装成xp,现在又改成win7,记录一下装win7 的过程 我是用光盘安装的系统 第一步:进入boss ...
-
[UE4]传值与传引用
值传递是圆形图标 设置引用需要使用Set by ref函数 对象在蓝图中都是以引用传递 对象,不需要额外设置参数类型是传值还是传引用. 结构体在蓝图中默认是按值传递 也可以手动设置结构体参数为按引用类 ...
-
java读取properties文件时候要注意的地方
java读取properties文件时,一定要注意properties里面后面出现的空格! 比如:filepath = /home/cps/ 我找了半天,系统一直提示,没有这个路径,可是确实是存在的, ...
-
阿里Sophix热修复
阿里巴巴对Android热修复技术已经进行了长达多年的探索. 最开始,是手淘基于Xposed进行了改进,产生了针对Android Dalvik虚拟机运行时的Java Method Hook技术,Dex ...