并发编程3大基本特性
一、并发编程的基本特性有3点(这3点需人为来保障)
1.有序性:保证程序中的指令按照代码顺序执行,不会发生重排序,除非在不影响程序执行结果的情况下。
2.可见性:确保当一个线程修改了共享变量的值,其他线程能够立即看到这个变化。
3.原子性:保证复合操作(如自增、读写等)在执行过程中不会被中断,即这些操作要么完全执行,要么完全不执行。
这意味着想做到多线程并发安全,就要实现这3个基本特性。像 java 的 volatile、synchronized、Lock 等技术的目的就是在实现这3个基本特性。
二、思考
有序性(算力层优化导致的无序)
1、跟有序性相对的是无序性,那为什么并发程序是无序性的?
有序性通常指的是程序指令的执行顺序。为了提高性能,jvm在单线程安全前提下 可能会对指令进行重排序,这在一定程度上引入了无序性。然而,这种无序性可能就会对多线程程序的正确性产生影响
2、为什么jvm要对指令进行重排序?
jvm进行指令重排序,可以减少执行指令的数量,或者将相关操作放在一起执行。这样可以减少不必要的数据读取和处理时间,从而提高程序的整体运行速度。
3、jvm的指令重排序,如何能够减少执行指令的数量?
a. 编译器优化:编译器在编译过程中会进行一定程度的重排,例如将读取同一数据的操作放置在一起执行,以减少数据读取的时间开销和提高效率。这种优化是在保证不改变单线程内程序语义的前提下进行的。
b. CPU重排序:CPU在执行时也可能进行重排序,采用乱序执行的技术来提高整体的执行效率。这是为了充分利用CPU的资源,比如在等待某些慢速操作(如内存访问)完成时,可以执行其他不依赖于这些操作的指令。
c. 数据依赖性考虑:在进行重排序时,编译器和CPU都会遵守数据依赖性原则。如果两个操作之间存在数据依赖关系,即一个操作的结果是另一个操作的输入,则这两个操作不能被重排序,因为这会改变程序的执行结果。
4、jvm如何禁用重排序(volatile)
volatile关键字在Java中用于声明一个变量是易变的,它确保了对该变量的读写操作会直接与主内存进行交互,而不是通过线程的本地缓存。这样,对volatile变量的修改对所有线程都是可见的。
在多线程环境下,volatile关键字可以防止指令重排序,具体来说,它执行以下两个重要的功能来防止重排序:
a.内存屏障:当读或写一个volatile变量时,它会插入一个内存屏障,确保在该操作之前的读和写操作都已经完成,而后续的读和写操作都不会被提前。这就防止了编译器或处理器对这些操作进行重排序。
b.禁止优化:volatile关键字告诉编译器,不要对这个变量进行某些优化,特别是那些会改变变量读写顺序的优化。这样,即使没有内存屏障,读取volatile变量也会返回最新的值,而不是缓存中的值。
可见性(存储层优化导致的不可见)
1、对于共享变量,为什么会存在线程间不可见问题?
在java内存模型中,共享变量存在于主内存,每个线程有独享的工作内存(为了提高处理速度,每个处理器都有自己的缓存,如闪存等)。当一个线程在一个处理器上修改了一个变量的值,这个值可能会暂时只存在于该处理器的缓存中,而其他处理器的缓存中仍然保留着旧值。这就导致了不同线程可能看到同一个变量的不同值。
2、保证了内存可见性,意味着禁用了缓存?
是的。内存不可见是因为处理器缓存才出现数据不一致问题。而禁用缓存是由内存屏障来实现的。
3、内存屏障怎么保障可见性?
内存屏障包含读屏障和写屏障
a.Load屏障:在每个读操作之前加上Load屏障,可以强制处理器从主内存中读取最新的数据,而不是从缓存中读取可能已经过期的数据。
b.Store屏障:在每次赋值操作之后加上Store屏障,可以强制将数据刷新到主内存,确保其他线程能够看到这个最新的值。
4、volatile 和 内存屏障的关系?
volatile关键字与内存屏障紧密相关,它利用内存屏障来禁止指令重排序,确保共享变量的可见性。
首先,当一个变量被volatile修饰时,编译器会插入内存屏障,这些内存屏障可以防止编译器或处理器对代码进行重排序,从而改变程序的执行顺序。内存屏障确保了在读取或写入volatile变量时,该操作之前的所有读/写操作都已完成,而且之后的所有读/写操作都不会提前执行。这种机制保证了volatile变量的修改对所有线程都是可见的。
其次,内存屏障还确保了对volatile变量的修改会立即写入主内存,而不是仅仅停留在处理器的缓存中。这样,其他线程在访问这个变量时,会从主内存中获取最新的值,而不是从可能已经过期的缓存中读取。
原子性(复合操作导致的不原子)
1、为什么不具备原子性?即复合操作(如自增、读写等)在执行过程中为什么会被中断?
非原子性:复合操作通常由多个步骤组成,例如读取变量的值、修改变量的值等。在多线程环境下,如果多个线程同时执行复合操作,这些步骤可能会被其他线程的操作打断,导致数据的不一致性。
2、为什么synchronized能保证操作的原子性
synchronized提供了锁定机制:当一个线程进入一个使用synchronized关键字修饰的方法或代码块时,它会获取一个锁。这个锁可以是一个对象锁,也可以是类锁,取决于synchronized的作用范围。一旦线程获取了锁,其他试图进入相同代码块或方法的线程将会被阻塞,直到持有锁的线程释放该锁。
三、并发编程其他重要的高级特性和概念
- 一致性:一致性是指在并发环境中,所有线程对共享数据的访问最终应该达到一个一致的状态。例如,在一个银行转账的例子中,无论发生多少次转账操作,系统的总金额应该保持不变,除非有外部因素干预。
- 隔离性:隔离性是指并发执行的多个线程或进程互相之间不干扰,即一个线程的操作不会影响另一个线程的结果。这通常通过使用锁或其他同步机制来实现。
- 活性:活性指的是系统能够保证长时间内至少有一个进程或线程能够继续执行下去,而不会在没有遇到错误的情况下停滞不前。
- 死锁:死锁是指两个或多个线程在等待对方释放资源时互相等待,导致都无法进行下去的情况。避免死锁是并发编程中的一个重要考虑点。
- 资源竞争:当多个线程或进程需要访问同一资源时,可能会出现资源竞争的问题。这通常需要通过同步机制来解决。