转载请注明出处:https://blog.csdn.net/yulong0809/article/details/79728009
前言
Java的并发时一般都会synchronized、volatile和Lock来实现同步,或者使用Java提供的一些辅助类,例如atomic和concurrent包下AtomicXXXX,ConcurrentXXX等,但是我们是否想过为什么会有这些关键字和类呢?这些关键字和类解决了什么问题?又是如何实现的呢?可能有的人会说不就是因为多线程操作吗;解决的就是并发时数据错误的问题;是通过唤醒机制实现的;但是真的就这么简单吗?显然不是的,这些涉及了大量的知识点,接下来就来梳理一下并将它们串下来。
首先我们知道多线程并发操作的目前是让程序可以更快速更高效,但并不是启动更多的线程就会执行的更快,其实为了程序可以更高效和快速,编译器和处理器也会做一些底层的操作,让我们程序在有效的时间可以更高效的执行,例如重排序,但是不管是重排序还是多线程操作都存在一些其他的问题,例如多线程之间内存可见性的问题、重排序后导致程序执行结果的错误,那么为了解决这些出现的问题,则有了这些并发的关键字和工具,但是具体是因为什么,又是如何解决的,还需要我们注意些什么,我们慢慢来分析,下面的内容是环环相扣的,希望可以按顺序看。
上下文切换
刚才上面提到了在有效的时间内可以更高效执行,其实就是上下文切换。多线程执行的时候CPU会给每个线程分配CPU时间片,时间片就是分配给各个线程的执行时间,因为时间片非常的短,所以CPU可以通过不停的切换线程,让我们错觉的认为线程是同时执行的。每次在切换之前都需要保持当前的线程状态,以便下次切换回来的时候快速的读取状态,每一次切换就叫做一次上下文切换。那么如果过多的切换上下文自然也就会影响到程序的执行效率。既然线程执行会受到时间片的限制,所以为让了让线程在有效的时间里可以执行更多的操作,编译器和处理器就会给我们的代码进行重排序,因为我们的代码在执行的时候会转变成汇编语言,所以重排序也分成编译器重排序和指令重排序,但是不管是哪种重排序都是为了更充分的利用CPU分配的时间片。
减少上下文切换的方法有如下方法:
- 无锁并发编程:多线程竞争锁的时候会引发上下文的切换,可以使用一些其他的办法来避免使用锁。
- CAS算法:又称为自旋锁,Java的Atomic包中就使用的CAS算法来更新数据。
- 尽量减少线程的数量:线程的创建和切换都是需要的时间的,线程多了来回切换的次数自然也就就更多,所以减少线程的数量也就能减少上下文的切换了。
重排序
上面说为了可以让线程更充分的利用CPU分配的时间片,就有了重排序。为什么重排序就可以让程序更效率呢?那是因为CPU执行的速度太快,而内存操作的时间没有CPU执行的速度快,举个例子,假如有一个内存操作相当的耗时,如果不进行重排序的话,可能直到时间片结束都没有处理完这一步操作,但是如果虚拟机碰到这样的操作时先暂时绕过这一步操作,先执行下一步操作这样就可以更好的利用CPU分配的时间片了,而站在硬件层面来说为了可以快速的执行,也会按照一定的规则将一系列的指令进行重排序执行。重排序的原理非常复杂,这也是为了更好的理解举得一个例子而已。再看一下如下两行代码,在真正执行的时候不一定操作1就执行在操作2的前面。既然编译器和处理器会对代码进行重排序,那么就一定会因为重排序的缘故而导致代码逻辑错误,最终的执行结果错误,所以这时就需要有一些规则来约束因为重排序而导致的错误。
byte[] bytes = new byte[1024];//操作1
int a = 0;//操作2