接着上篇文章,了解了一些线程的基本知识,知道了如何去使用线程,那么在多线程的环境下,我们会遇到什么问题呢?经常能遇到的JMM是啥?多线程的三大特性是啥,跟我们平时的使用有什么关系?
为什么多线程会出现各种各样的问题?
想要知道为什么单线程情况下开发那么简单,而多线程的情况下却需要考虑那么多的问题,其实主要还是需要了解线程是如何被调度、执行,以及线程执行与内存的关系。
线程执行
首先我们要知道,我们一直理解的这个线程,其实并不是一个真正实际意义上的线程,真正的线程是在我们CPU内部的执行线程,我们平时创建的线程,其实是虚拟线程,甚至说我把它理解为任务,只不过这个任务在我们jvm中,是以一个栈的形式存在,和cpu中的执行线程类似,所以称为线程,虚拟线程中的所有工作,最终只能交给CPU中的实际线程进行执行,我们调用了Thread类的start方法之后,对于我们来说,其实已经认为这个线程是运行状态了,但是对于真正CPU来说,其实它根本不会知道下一个执行的是哪个线程,而是由操作系统去将我们程序中的虚拟线程调度给CPU,让CPU执行。
单核CPU执行
在早期单核时代,抛开超线程去理解,在单核CPU的情况下,其实真正CPU只有一个线程在执行所有的任务,那么其实系统上所有的虚拟线程都是被单线程执行,这种情况下,操作系统将进程内的任务一个一个调度给CPU,CPU一个一个执行,对于一个进程内,所有任务就是按顺序执行,我们只需要在一个任务内将所需要做的工作编排好,不需要有其他的考虑,这样只有一个不好的地方就是,任务处理速度慢,当一个任务运行的时候,其他的任务都是一个排队的状态,甚至简单点说,其他程序运行的时候,当前程序是处于一个暂停的状态。
造成的问题gon
运行正常并且好控制,但是因为慢,无法承受很大的工作量
多核CPU执行
访问量着时代的发展,单核CPU已经无法支撑大量的工作,多核CPU的出现,大大提升了电脑/服务器的性能,如果单核CPU的意思是说一个线程去执行所有的任务,那么多核CPU就是增加人手去减轻单个CPU的压力,并且提升整体系统的处理水平,原本10个虚拟线程任务,一个CPU线程去执行,现在有2个,4个,8个甚至更多的CPU线程去执行,那肯定是杆杠的,但是人手多了就会出现一个问题,原本一个人的时候,我们知道会执行完一个任务再进行下一个任务,但是人多了,出现的情况是操作系统将我们的一大堆虚拟线程任务分给CPU之后,根本没办法知道,哪个任务会先执行,哪个后执行,还是说会同时执行,这已经不是我们能控制的了,那再出现一种情况是说,任务之间可能还会有关联,甚至说操作的都是同一块资源。那这种情况下,虽然大大提升了系统的性能,但是也给我们造成了困扰,多个线程同时执行,我们无法预知任务执行顺序、时机的情况下,如何才能保证我们的资源被正确使用呢?
造成的问题
运行快,性能高,完成的任务量大,但是不好控制线程之间执行顺序
JMM
什么是JMM
之前我有写过几篇关于JVM的文章,那JMM是什么呢,它跟JVM有什么关系吗?其实,JVM是内存结构,而JMM是内存模型,JVM说的是整个java虚拟机内部的资源如何分配,而JMM是说JVM运行起来之后,内部线程与内存之间的关系。在搜索引擎里搜索JMM,可以看到很多图文解释,其实最终的目的是告诉我们,在线程运行之前,所有的资源其实都是存放到主内存之上,当一个线程运行起来的时候,占用的是独享的工作内存,如果需要操作主内存上的资源,线程是会在运行起来的时候,先去将主内存的资源copy到工作内存中,在工作内存进行操作,操作完成之后,才会刷新到主内存里。那这个会造成什么问题呢?如果是在多线程运行的情况下,一个主内存的资源被多个线程拿到自己的工作内存中操作,那么这个资源的变化在线程互相之间根本是不可见的,如果某些执行慢,某些快,那慢的是不是会一直用一个早已经被修改过,错误的资源一直处理,而且更恐怖的是处理完之后还会将一个错误的结果刷新到主内存中,那结果不就是错误的吗?
造成的问题
线程内部处理速度快,但是处理的资源互相不可见,无法保证处理的是最新的资源
重排序
什么是重拍序呢,其实就是为了提高最终cpu处理的速度,在编译或者执行层面,略微对代码的执行顺序进行调整。重排序分为两种情况,一种是编译器级别的重排序,一种是处理器级别的重排序。
重排序遵守的规则
Happens-before
简单来说,happens-before原则就是JMM规定,在以下几种情况下,不能进行重排序,否则会对程序结果产生影响
-
单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
-
锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
-
volatile的happen-before原则:对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。
-
happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
-
线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
-
线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
-
线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。
-
对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。
as-if-serial
as-if-serial规则的意思是说,无论怎么进行重排序,必须保证(单线程)当前线程下执行的结果要和原顺序执行的结果保持一致
各种级别的重排序
编译器级别的重排序
其实因为我们一个任务在最终被执行的时候,cpu是会先将部分需要操作的数据从内存拿到缓存,再拿到寄存器中,然后再通过cpu的计算单元去通过操作寄存器中的数据来增加处理速度。那这种情况下,如果能保证后续需要操作的数据一直在寄存器中,那么可以省去再去缓存,内存中拿数据的时间,计算会很快。但是如果按我们自己写的程序去执行,可能会出现一种情况就是相邻的两行操作指令根本没什么关系。会导致CPU需要一直反复的去缓存,内存读数据。效率肯定不如先执行一些已经存在可以操作的数据。所以编译器其实在编译阶段,会给我们代码做了优化,尽可能的让cpu先去执行比较快的操作。
处理器指令级别的重排序
其实我们指令级别的重排序,是为了让我们的指令按照流水线的模式去运行,解释一下这个流水线模式,其实就是一个指令级别的并行模式,原本单个执行单元的操作的顺序可能是,读取指令、解码指令、运行指令、写回结果。这种情况会是相当于一个执行单元完成四件事情,完成这操作之前,下个操作就需要等待。比如读取指令的时候,剩下单个指令都需要等着被执行。剩下的指令在操作过程中,下一个任务就需要等着。但是流水线模式改变了这种方式,提升了指令操作的效率。它将四个操作指令创建了四个指令单元(举例),每个指令单元只做一件事,并且可以同时执行,执行,A单元只用来读,B单元只用来解码,C单元只用来运行,D单元只用来写回。这样每个指令单元只做相同的操作就可以,并且任务1在处理到B这个执行的时候,任务2已经进入到了A执行单元。大大提升了执行效率。指令级别的重排序就是为了让相同的指令任务尽量排在一起,提升执行效率。
处理器内存级别的重排序
内存级别的重排序可能并不是真的重排序了,是因为CPU操作之后写回内存的时候,并不是实时的,因为中间有一层缓存层,它是通过缓存flush到内存中,这就会有可能导致cpu处理之后的值无法实时更新到内存中,刷到主内存中的顺序可能跟实际操作的顺序不一样,看起来是乱序的
造成的问题
单线程下,重排序最终结果是一直的,并且提升了运行的效率,但是多线程下,虽然重排序了之后单线程的结果不变,却导致多线程运行期间,需要互相访问资源的时候,拿到的资源并不一定是完整的状态,而是一个一半的状态,甚至就是一个错误的状态。
如何解决这些问题?
多线程情况下,资源互相不可见,那就可以使用volatile保证可见性,指令会重排序,那就可以用volatile让阻止重排序或者final直接将变量焊死,不让其内部指令分开执行,线程乱序执行导致最终结果不能保证原子性,那我们就可以使用sychronized关键字,让我们需要保持原子性的任务按顺序执行,保证有序性和原子性,具体他们是如何保证的,下边文章我会继续梳理。
总结
这篇文章简单介绍了多线程运行的情况下,哪些因素会导致最终多线程运行的结果出问题,比如多CPU执行线程,JMM内存模型,重排序等引出多线程开发的三个特性,可见性,有序性,原子性,了解多线程之间会出现什么问题,才能让我们知道如何去解决这些问题。