并发与并行、同步与异步,线程安全的实现

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的大小是否一致,如果不一致那么就会抛出并发修改异常;

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
线程是操作系统能够进行运算调度的最小单位,它是进程中的一个实体。线程可以被操作系统独立地进行调度和执行,并与同一进程中的其他线程共享进程的资源。 首先,串行线程是指多个线程按照顺序依次执行,一个线程的执行必须在前一个线程执行完毕之后才能开始。这意味着多个任务无法同时进行,执行效率相对较低。 其次,并行线程是指多个线程同时进行执行,每个线程独立执行自己的任务,互不干扰。这样可以大大提高程序的执行效率。 然后,同步线程是指多个线程之间存在有序的执行关系,每个线程需要等待其他线程执行完毕之后才能开始自己的任务。同步线程可以保证数据的一致性,但容易造成程序的阻塞。 最后,异步线程是指多个线程之间不存在执行的先后顺序,各个线程可以独立执行自己的任务。异步线程可以提高程序的响应速度和并发性,但需要特别注意线程安全问题。 总结起来,线程的执行方式可以分为串行、并行同步和异步四种情况。其中,串行线程按照顺序执行,效率较低;并行线程可以同时进行,提高执行效率;同步线程按照有序关系执行,保证数据一致性;异步线程可以并发执行,提高响应速度。根据具体的应用场景和需求,选择合适的线程模式可以提高程序的执行效率和稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值