单核 cpu 下,线程实际是串行执行的。操作系统中有一个组件叫做任务调 度器,将 cpu 的时间片,分给不同的线程使用,只是由于 cpu 在线程间(时间片 很短)的切换非常快,人类感觉是同时运行的。 总结为一句话就是:微观串行,宏观并行,一般会将这种线程轮流使用 cpu 的做法称为并发,concurrent.
多核 cpu 下,每个核(core)都可以调度运行线程,这时候线程可以是并 行的。
例:
大家排队在一个咖啡机上接咖啡,交替执行,是并发;两台咖啡机上面接咖啡, 是并行。
从严格意义上来说,并行的多任务是真的同时执行,而对于并发来说,这个过程 只是交替的,一会执行 任务 A,一会执行任务 B,系统会不停地在两者之间切 换。
并发说的是在一个时间段内,多件事情在这个时间段内交替执行。
并行说的是多件事情在同一个时刻同事发生。
Java 是最先支持多线程的开发的语言之一,
Java 从一开始就支持了多线程 能力。由于现在的 CPU 已经多是多核处理器了,是可以同时执行多个线程的.
多线程技术使程序的响应速度更快 ,可以在进行其它工作的同时一直处于活动
状态,程序性能得到提升.
性能提升的本质 就是榨取硬件的剩余价值(硬件利用率).
多线程带来的问题是什么?
安全性(访问共享变量),性能(切换开销等)
问题
硬件的发展中,一直存在一个矛盾,CPU、内存、I/O 设备的速度差异。
速度排序:CPU > 内存 > I/O 设备
为了平衡这三者的速度差异,做了如下优化:
JMM
Java 内存模型(Java Memory Model,JMM)规范了 Java 虚拟机与计算 机内存是如何协同工作的。Java 虚拟机是一个完整的计算机的一个模型,因此这个模型自然也包含一个内存模型——又称为 Java 内存模型
Java 内存模型,用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现 让 Java 程序在各种平台下都能达到一致的并发效果,JMM 规范了 Java 虚拟机 与计算机内存是如何协同工作,规定了一个线程如何以及何时可以看到由其他线 程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。
计算机在高速的 CPU 和相对低速的存储设备之间使用高速缓存,作为内存 和处理器之间的缓冲。将运算需要使用到的数据复制到缓存中,让运算能快速运 行,当运算结束后再从缓存同步回内存之中
在多处理器的系统中(或者单处理器多核的系统),每个处理器内核都有自己 的高速缓存,它们有共享同一主内存(Main Memory)。
当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致。
JVM 主内存与工作内存
Java 内存模型的主要目标是定义程序中各个变量的访问规则,**即在虚拟机 中将变量(线程共享的变量)存储到内存和从内存中取出变量这样底层细节**。 **Java 内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工 作内存**,**线程对变量的所有操作都必须在工作内存中进行**,**而不能直接读写主内 存中的变量**。 这里的工作内存是 JMM 的一个**抽象概念**,也叫本地内存**,其存储了该线程以 读 / 写共享变量的副本**。
就像每个处理器内核拥有私有的高速缓存,JMM 中每个线程拥有私有的本地内存。
不同线程之间无法直接访问对方工作内存中的变量,线程间的通信一般有两种方 式进行,一是通过消息传递,二是共享内存。Java 线程间的通信采用的是共享 内存方式,线程、主内存和工作内存的交互关系如下图所示:
这里所讲的主内存、工作内存与 Java 内存区域中的 Java 堆、栈、方法区 等并不是同一个层次的内存划分,这两者基本上是没有关系的**,如果两者一定要 勉强对应起来,那从变量、主内存、工作内存的定义来看,主内存主要对应于 Java 堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域**。
一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。 对于如今的多核处理器,每颗 CPU 都有自己的缓存,而缓存仅仅对它所在 的处理器可见,CPU 缓存与内存的数据不容易保证一致。 为了避免处理器停顿下来等待向内存写入数据而产生的延迟,处理器使用写 缓冲区来临时保存向内存写入的数据。写缓冲区合并对同一内存地址的多次写, 并以批处理的方式刷新,也就是说写缓冲区不会即时将数据刷新到主内存中
缓存不能及时刷新导致了可见性问题。
举例:
假设线程 1 和线程 2 同时开始执行,那么第一次都会将 a=0 读到各自的 CPU 缓存里,线程 1 执行 a++之后 a=1,但是此时线程 2 是看不到线程 1 中 a 的值的,所以线程 2 里 a=0,执行 a++后 a=1。 线程 1 和线程 2 各自 CPU 缓存里的值都是 1,之后线程 1 和线程 2 都会将 自己缓存中的 a=1 写入内存,导致内存中 a=1,而不是我们期望的 2。
线程切换带来的原子性问题
原子的意思代表着——“不可分”; 一个或多个操作在 CPU 执行的过程中不被中断的特性,我们称为原子性. 原子性是拒绝多线程交叉操作的,不论是多核还是单核,具有原子性的量,同一 时刻只能有一个线程来对它进行操作. CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符。线程切换导致了原子性问题
Java 并发程序都是基于多线程的,自然也会涉及到任务切换,任务切换的 时机大多数是在时间片结束的时候。我们现在基本都使用高级语言编程,高级语 言里一条语句往往需要多条 CPU 指令完成。如 count++,至少需要三条 CPU 指令
指令 1:首先,需要把变量 count 从内存加载到工作内存;
指令 2:之后,在工作内存执行 +1 操作;
指令 3:最后,将结果写入内存;
还是以上面的 count++ 为例。两个线程 A 和 B 同时执行 count++, 即便 count 使用 volatile 修辞,我们预期的结果值是 2,但实际可能是 1。
有序性指的是程序按照代码的先后顺序执行。 编译器为了优化性能,有时候会改变程序中语句的先后顺序。
例子:
init(); 与 inited = true; 并没有数据的依赖**,在单线程看来,如果把两句的代 码调换好像也不会出现问题。**
但此时处于一个多线程的环境,而处理器真的把这两句代码重新排序,那问题就 出现了,若线程 1 先执行 inited = true; 此时,init() 并没有执行,线程 2 就 已经开始调用 work() 方法,此时很可能造成一些奔溃或其他问题的出现。 Java 内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不 会影响到单线程程序的执行,却会影响到多线程并发执行的正确性.
总结
缓存导致的可见性问题,线程切换带来的原子性问题,编译优化带来的有序性问题。其实缓存、线程、编译优化的目的和我们写并发程序的目的是相同的, 都是提高程序安全性和性能。但是技术在解决一个问题的同时,必然会带来另外 一个问题,所以在采用一项技术的同时,一定要清楚它带来的问题是什么,以及如何规避
一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后:
一个代码实例:
package concurrentProgrammer.volatiledemo;public class ThreadDemo implements Runnable{/*volatile 修饰的变量,在一个线程中被修改后,对其它线程立即可见禁止cpu对指令重排序*/private volatile boolean flag = false;//共享数据@Overridepublic void run() {try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}this.flag = true;//让一个线程修改共享变量值System.out.println(this.flag);}public boolean getFlag() {return flag;}public void setFlag(boolean flag) {this.flag = flag;}
}
测试:
package concurrentProgrammer.volatiledemo;public class TestVolatile {public static void main(String[] args) {ThreadDemo td = new ThreadDemo();Thread t = new Thread(td);//创建线程t.start();//main线程中也需要使用flag变量while(true){if(td.getFlag()){System.out.println("main---------------");break;}}}
}
结果:
不加volatile关键字结果:
程序一直在运行
上一篇:最新或2023(历届)漂亮简单安全手抄报图片大全_安全手抄报图片 安全主题手抄报有安全字的内容 消除安全隐患筑牢安全防线手抄报
下一篇:最新或2023(历届)安全知识手抄报内容 安全主题手抄报图片大全简单漂亮 安全手抄报和勤俭节约手抄报 普及安全知识确保生命安全手抄报