并发与并行同步与异步,线程安全的实现
Posted 若曦`
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发与并行同步与异步,线程安全的实现相关的知识,希望对你有一定的参考价值。
1. 并发与并行
并发其实是一个宽泛的概念,它代表计算机可以同时可以执行多个任务;
首先,我们知道cpu的最小调度单位是线程,所以说,一个cpu在一段时间内只能处理一个线程;
那么当我们在进行并发编程的时候,如果是多核电脑,也就是有多个cpu的情况下,就是真正的并发执行;我们一般也称之为并行执行;
如果是单个cpu的情况,cpu会使用时间片轮转的方式,让线程a执行一段时间后,切换到线程b去工作,让线程来回切换工作,这也被成为进程或线程的上下文切换;
2. 同步与异步
同步代表必须等到前一个任务执行完后,才能执行下一个任务,也就是线程间排队执行;所以一般情况,同步是不会有并发或并行的概念;
异步代表不同的任务之间不会相互等待,也就是说,你在执行任务a的时候,也可以执行任务b;
一个典型实现异步的方式,就是并发编程,开启多个线程,每个线程去执行不同的任务;
那么如果是多核cpu,每个线程都会被分配到独立的核心上去运行;当然如果是单核cpu,就会用时间轮转片的方式,让线程间来回切换工作,这种情况下也是并发执行的(因为cpu的执行速度非常快,让我们感觉就像是在同时执行一样);
3. 那么什么情况下适合并发编程呢?
一般会分为以下两种情况
CPU密集的情况
cpu密集的情况不太适合多线程执行,为何这么说,CPU密集型会消耗掉大量的CPU资源,例如需要大量的计算,视频渲染啊,仿真啊之类的。这个时候CPU就尽最大努力在运行,这个时候切换线程,反而浪费了切换的时间,效率不高。
就像你的大脑是CPU,你本来就在一本心思地写作业,多线程这时候就是要你写会作业,然后立刻敲一会代码,然后在P个图,然后在看个视频,然后再切换回作业。emmmm,过程中你还需要切换(收起来作业,拿出电脑,打开VS…)那你的作业怕是要写到挂科。。。这个时候不太适合使用多线程,你就该一门心思地写作业~
但是有一种方式,可以使cpu计算使采用多线程编程,那就是使用forkjoin框架。或者是stream并行流的方式;
ForkJoin与Stream并行流的使用
ForkJoin与Stream并行流的使用可以看我这篇博客 ForkJoin的使用及for循环、stream并行流三种方式的时间比较
其实stream并行流的方式,底层也是使用了forkjoin框架;
IO密集的情况
涉及到网络、磁盘IO的都是IO密集型,这个时候CPU利用率并不高,这个时候适合使用多线程。
首先我们要明白,IO密集是“不烧脑的工作”,但是一般IO操作都会带来一定时间的逻辑等待或网络等待;那么如果在单个cpu上去执行,就会时不时的等待,从而浪费cpu的资源;
那么这时如果采用并发的方式,就会在等待的时候切换线程去执行其他任务,从而规避掉等待的时间,虽然线程间来回切换也会有开销的时间,但一般来说都还是会更优,所以IO密集适合多线程来达到更高的一个效率;
4. 线程安全
线程安全,其实是指多个线程在并发执行的时候,对于数据的读写等操作不会出现错误,也就是和预期结果是相同的;
那么线程安全,本质上就转移到并发情况访问资源的安全;
对于JVM中的资源,我们一般区分为线程私有和线程共有;
JVM内存模型中 线程私有和线程共有
所以我们明白了,栈和方法区是线程私有的,堆是线程共享的;
栈、方法区、堆 中存储的数据
- 栈:局部变量中的基本数据类型,类的方法;
- 方法区:.Class、常量池、静态区、全局区(hotspot7及之前) ; .Class的模板信息(被加载的类信息) (hotspot8后)
- 堆:对象(hotspot7及之前);对象、.Class、常量池、静态区、全局区(hotspot8后)
对于线程私有的模块,是不会有线程不安全的问题的;
而对于线程共享的模块,才会存在线程不安全的问题;
在现在的jdk1.8常用的版本下,hotspot的版本同样也是8;
所以经过上面的分析,我们可以得出结论;
线程安全的数据
- 局部变量 (线程私有,存放在方法调用的栈帧中,随着栈帧出栈结束而消亡)
线程不安全的数据
- 全局变量 (线程共享)
- 堆内存上的对象
i++的非原子性
当i++在被翻译成机器代码时,其实是被翻译成三条不同的机器指令;
分别是
(1) 将内存中的数据num加载到cpu的寄存器中
(2) 然后将寄存器中的num数据给+1
(3) 最后再将num给放回内存中
当一个线程执行到第二步时,另一个线程就读取了内存中的数据,就会造成两次+1其实只真正执行了一次;
换句话说,i++并不是一个原子操作;原子操作是不能再继续拆分的操作,或是不能被其他线程打断的指令;
上述情况是不是和我们之前探讨的线程的工作内存,和主内存间的关系有点像呢;
Java线程的工作内存与硬件内存的关系
从书中我们可以得到答案,线程的工作内存/本地内存,其实就是指缓存和寄存器等内存;
5. 实现线程安全
为了实现线程安全,那么就要实现线程同步,也就是让线程有序的去执行;这一部分想必大家也无比的熟悉了;
那么线程安全问题,其本质是维护全局变量资源的一个安全性;
我们可以从两个角度出发,一种是对线程进行同步控制;一种是直接对资源进行控制,不允许同时多个线程对其进行操作;
对线程进行同步控制
- synchronized + wait() + notify() (其实synchronized本质还是锁的资源,只是线程获取资源获取不到时看起来就像线程被阻塞了)
- Reentrantlock + Condiction (重入锁与线程监视器)
详细的使用可以看我的这篇博客
synchronized、Lock接口、Condition接口、读写锁及ReentrantLock(重入锁) 特性及使用
对数据进行安全控制
- Atomic、 AtomicReference(原子类与原子引用)
- 线程安全集合类
详细的内容可以看我这两篇博客
原子类与自旋锁原理初探
集合的常用线程安全实现类使用及源码分析
6. foreach循环中为什么不能对集合进行add或remove操作 (补充知识点)
想必大家肯定遇到过这样的情况,在foreach循环中对集合进行操作,然后出现了以下这种情况的错误
这是什么原因呢?
首先我们要知道,使用foreach去遍历集合的时候,本质是使用的Iterator迭代器去遍历元素,所以问题就出在这个Iterator迭代器身上;
Iterator迭代器其实是建立了一个链式索引表,存储指向集合的指针;
当我们使用foreach的时候,就已经建立好了这个链式索引表,也就是说每个结点都存储好了对应的指针;
当我们遍历集合的时候,本质上是去遍历这个Iterator迭代器的链式索引表的指针;
所以当迭代器去遍历下一个节点的时候,实质是上是调用next()方法去访问下一个结点;
我们可以从编译后的源码去查看
再点击这个ArrayList实现的迭代器中去看
在源码中我们可以看出,在每次执行next()方法的时候,都会去检查一开始链式索引表的长度和现在的list的大小是否一致,如果不一致那么就会抛出并发修改异常;
以上是关于并发与并行同步与异步,线程安全的实现的主要内容,如果未能解决你的问题,请参考以下文章