目录
🎈线程安全
概念:一般来说,如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
🎈线程不安全的五大原因
先说一下是那五大原因吧:1.抢占式执行 ;2.多个线程修改同一个资源;3. 操作不是原子性;4.内存可见性;5.指令重排序
1. 抢占式执行
抢占式执行是导致线程不安全的罪魁祸首,当多个线程并发执行的时候,那个线程先执行,那个线程后执行,这是程序员决定不了的,是由操作系统内核来决定的,或者说是系统的调度器决定的。
2. 多个线程修改同一个资源>
由于线程是抢占式执行的,这就导致了多个线程可能会同时访问同一个系统资源,如果是只是读取资源当然不会有什么影响,但是如果有多个线程同时写资源,这样就会发生错误,从而导致了线程不安去的情况。
3. 操作不是原子性
比如像 i++ 这样的操作就不是原子性的,在系统调用执行时,这个操作被分为了三步,首先
是 cpu 从内存中读取到 i 的值,读到 cpu 里面的寄存器(寄存器是存在于 cpu 中,空间比内存小,访问速度比内存快)里,然后
在寄存器里执行了类似于 ADD 这样的指令然后就完成了 +1 的操作,操作结果依旧放在寄存器里,最后
cpu 里的寄存器把值写回到内存中,这样才完成了一个 i++ 的操作。这样的三步就相当于读取,修改,写入的过程。
由于线程是抢占式执行的,这就导致了,一个线程在自增执行了一半的时候,另一个线程也来读取,然后自增。就比如现在假如有两个线程分别在两个 cpu 中同时从执行 i++ 的操作,那么它们将同时读到 i 的值(此时读取到的值相同),然后分别执行加 1,再写回到内存中,此时就会发现 i 的值就只是加了一次。因为操作是非原子性,导致了出现了这样的错误,从而导致发生线程不安全
4. 内存可见性
两个线程同时操作一个内存,比如,一个读一个写,写操作的线程进行了修改的时候,读线程读取到的结果可能是修改前的也可能是修改后,结果是不确定的,也造成线程不安去。至于为什么会产生这样的结果?
原因:假如出现了一种情况,循环自增 while(true){i++},那么当一个线程执行的时候,理论上 cpu 会在内存上循环的执行
读取,修改,写入
…读取,修改,写入
…这样的操作,然后像这种连续且大量的读写内存的操作是非常消耗资源开销的,于是 JMM 就会对指令进行优化。那么是如何优化呢?我们知道数据的修改是在 cpu 的寄存器里面实现的,并且寄存器的的访问速度又比内存快,于是所有的 ADD 操作就会一直在寄存器里面实现,于是整个过程就变成了读取,修改..修改..修改,写入
这样的过程。
这种优化对于单线程来说,是很好的,但是如果是多线程,这样的有优化就适得其反了。就比如一个线程读取后,已经把值自增了 100 次了,另一个线程可能还读取的是它的初始值 1。而我们预期要读到的值应该是它自增过程中的一个中间值,这就发生了严重的错误,导致了线程不安全。
总结:内存的可见性本质就是编译器优化导致了线程修改后的结果没有及时地写回到内存中,从而也进一步导致了其他线程读取到地值不是最新地数据(读地是一个旧的数据)。
5. 指令重排序
指令重排序也是和编译器优化直接相关,为了让程序跑地更快,调整了执行地顺序。从而导致了不必要地错误,同样地在单线程下这样地优化是好的,但多线程下这样的优化就会出现问题。
关于 cpu 和 内存 交互的问题
文章上面一直用 cpu 和内存直接交互举的例子,但实际情况是 cpu 并没有和 内存 直接交互,它们之间还有 L1 Cache (一级缓存), L2 Cache (二级缓存),它们之间具体有多少级缓存,不同的计算机是不一样的。由于 cpu 空间很小,读取速度快;内存的空间大,读取速度慢,于是中间这些缓存就起到了一个存储交互的作用,cpu 一般是能去操作缓存就去操作缓存,除非缓存没有数据了才会去操作内存,总之,cpu 会尽量减少内存操作的次数,从而提高效率。
提示:在java内存模型 JMM 中就统一的把CPU的寄存器和缓存统称为“工作内存"把真实的内存,称为“主内存"
🎈如何解决线程不安全问题
对于如何解决线程不安全问题,就得从造成线程不安全的原因去分析解决,就是上面总结的五点原因去分析。线程的抢占式执行是是由于操作系统调配的,所以这个是无法去改变。多个线程修改同一个资源呢,也是无法去解决的。但操作是原子性的我们就可以通过加锁
的方式来保证线程安全
加锁:对一个方法或者一个代码块使用
synchronized
(关键字)来修饰,保证这个操作是原子性的,当这个代码块加锁后,其他的线程想要来执行就得等待它先释放锁。又称监视器锁
synchronized
也同样禁止了内存可见性和指令重排序对编译器的优化。同样的,一旦使用加锁这个操作,代码的效率就变低了,就很难和高性能沾边了。
在 java 多线程编程中还有 volatile
关键字 ,它起到的效果是辅助保证线程安全,用来修饰的共享变量,可以保证内存可见性
(就相当于其他线程直接读取了 CPU 里寄存器的一直被实时修改后的值),部分保证顺序性