@@ -114,5 +114,94 @@ ABA问题的场景如下:
114114
115115守护线程是一类特殊的线程,它的生命周期与非守护线程不同。当程序中只剩下守护线程时,JVM会判断程序已经不再需要继续执行,因此会强制终止所有守护线程,然后退出程序。
116116
117+ 三、JMM
118+ JMM(Java Memory Model)是Java中定义的一种抽象的内存模型,用于规定多线程程序在访问共享变量时的行为。JMM定义了线程如何与主内存和工作内存进行交互,以及如何保证多线程程序的可见性、有序性和原子性。
119+
120+ 以下是JMM的主要特点和规则:
121+
122+ 1.主内存(Main Memory):主内存是所有线程共享的内存区域,包含了所有的共享变量。
123+
124+ 2.工作内存(Working Memory):每个线程都有自己的工作内存,工作内存存储了线程使用到的变量的副本。线程对共享变量的操作都先在工作内存中进行,然后根据一定的规则将结果同步到主内存或其他线程的工作内存中。
125+
126+ 3.内存间的交互操作:线程之间的变量值传递必须通过主内存来完成。一个线程对共享变量的写操作,必须先将值写入自己的工作内存,然后再将修改同步到主内存中。另一个线程对共享变量的读操作,必须先从主内存中读取最新的值到自己的工作内存,然后再进行读取。
127+
128+ 4.原子性(Atomicity):JMM保证对基本数据类型(如int、long)的读写操作具有原子性,即一个线程的读写操作不会被其他线程干扰。但对于复合操作,如i++这样的自增操作,并不具有原子性。
129+
130+ 5.可见性(Visibility):JMM保证一个线程对共享变量的修改对其他线程是可见的。当一个线程对共享变量进行修改后,必须将修改后的值同步到主内存中,然后其他线程才能读取到最新的值。
131+
132+ 6.有序性(Ordering):JMM保证程序的执行顺序符合代码的顺序。但在多线程环境下,由于指令重排序等优化,可能导致线程执行的顺序与代码编写的顺序不一致。JMM通过禁止某些类型的重排序来保证有序性。
133+
134+ JMM通过使用各种同步机制(如volatile、synchronized、Lock等)和内存屏障(Memory Barrier)等手段来实现上述特点和规则,从而保证多线程程序的正确性和可靠性。开发者在编写多线程程序时,需要遵循JMM的规则,合理使用同步机制和内存屏障,以确保线程安全性和正确的内存访问
135+
136+ 四、happen-before
137+ JMM(Java Memory Model)和happen-before 是相关的概念,用于描述多线程程序中操作之间的顺序关系。
138+ JMM定义了操作之间的happen-before关系,它是一种偏序关系,用于描述在多线程环境中,一个操作的执行结果对于其他操作的可见性和有序性。
139+ 以下是JMM中的happen-before规则:
140+ 程序顺序规则(Program Order Rule):在单个线程中,按照程序的顺序,前面的操作happen-before后面的操作。
141+ 锁定规则(Lock Rule):一个解锁操作happen-before对同一个锁的后续加锁操作。
142+ volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作happen-before后续对该变量的读操作。
143+ 传递性规则(Transitive Rule):如果操作A happen-before操作B,操作B happen-before操作C,那么操作A happen-before操作C。
144+ 线程启动规则(Thread Start Rule):线程的启动操作happen-before线程中的任何操作。
145+ 线程终止规则(Thread Termination Rule):线程的所有操作happen-before其他线程检测到该线程的终止。
146+ 中断规则(Interruption Rule):一个线程中的中断发生happen-before其他线程检测到该线程的中断事件。
147+ 这些规则定义了操作之间的顺序关系,通过happen-before关系,JMM保证了多线程程序的可见性和有序性。根据happen-before关系,对一个操作的执行结果对于其他操作的可见性是有保证的,因为后续的操作会看到先前操作的结果。同时,happen-before关系也保证了操作的执行顺序符合代码的顺序,避免了指令重排序等问题。
148+ 开发者可以利用happen-before关系来编写正确的多线程程序,通过合理地使用同步机制(如锁、volatile变量、synchronized等)来确保操作的happen-before关系符合预期,从而保证线程安全性和正确的内存访问。
149+
150+ 五、Store Buffer与
151+ Store Buffer(写缓冲)是现代处理器中的一种优化技术,用于延迟写入操作并提高内存访问的效率。Store Buffer允许处理器将写操作暂时缓存到一个专门的缓冲区,而不是立即将数据写入主内存。
152+ Store Buffer能够延迟写入的原因有以下几点:
153+ 提高处理器的吞吐量:将写操作暂时缓存到Store Buffer中可以减少对内存的访问次数,从而提高处理器的吞吐量。处理器可以继续执行后续的指令,而不需要等待写操作完成。
154+ 减少内存访问延迟:内存访问通常是一项较为耗时的操作,通过将写操作缓存到Store Buffer中,可以减少对主内存的直接访问,从而减少内存访问延迟,提高程序的执行效率。
155+ 优化写操作的顺序:Store Buffer可以对写操作进行重排序,以优化写操作的执行顺序。重排序可以提高处理器的性能,例如通过合并多个写操作到同一个内存区域,减少内存总线的竞争。
156+ Store Buffer解决了多线程程序中的内存可见性问题。当一个线程执行写操作时,修改的数据首先会被存储到Store Buffer中,而不是立即写入主内存。其他线程在读取该共享变量时,可能会从自己的工作内存中读取旧的值,而不是最新的值。然而,Store Buffer会定期将缓存的数据刷新到主内存中,从而确保对共享变量的修改对其他线程是可见的。
157+ 因此,Store Buffer在多线程环境中起到了延迟写入和优化内存访问的作用。它通过提高处理器的吞吐量、减少内存访问延迟和优化写操作顺序,提高了程序的性能。同时,它通过刷新缓存数据到主内存,解决了多线程程序中的内存可见性问题,确保线程之间对共享变量修改的可见性。
158+
159+
160+ - 修改的数据首先会被存储到Store Buffer中,而不是立即写入主内存 不会导致其他线程脏读吗
161+ Store Buffer 的延迟写入特性可能会导致其他线程发生脏读的情况。
162+ 当一个线程执行写操作时,修改的数据首先会被存储到 Store Buffer 中,而不是立即写入主内存。如果其他线程在此时读取共享变量,它们可能会从自己的工作内存中读取旧的值,而不是最新的值。这种情况下就会发生脏读,即读取到了尚未写入主内存的数据。
163+ 为了解决脏读的问题,现代处理器采用了一些机制来确保内存的一致性和可见性。其中一个常用的机制是** 内存屏障** (Memory Barrier)或者称为内存栅栏(Memory Fence)。内存屏障是一种同步操作,它可以保证在内存屏障之前的所有写操作在屏障之后的读操作之前完成。
164+ 内存屏障可以用来解决 Store Buffer 导致的脏读问题。当一个线程执行 Store Buffer 刷新到主内存的操作时,在刷新之前插入一个内存屏障,确保在该内存屏障之前的所有写操作在屏障之后的读操作之前完成。这样,其他线程在读取共享变量时,会从主内存中获取最新的值,避免了脏读的问题。
165+ 需要注意的是,具体的内存屏障机制和实现可能因处理器架构和编程语言的不同而有所差异。在编写多线程程序时,开发人员应该使用合适的同步机制(如锁、volatile变量、synchronized等)来确保内存的一致性和可见性,以避免脏读和其他内存访问问题。
166+
167+
168+ 六、DCL(Double-Checked Locking)
169+ DCL(Double-Checked Locking)是一种用于实现延迟初始化的线程安全单例模式的技术。它的目标是在保持高性能的同时,确保只有一个实例被创建。
170+
171+ DCL的基本思想是在锁的粒度上进行优化。它使用两个检查来减少对共享资源的同步访问。首先,通过检查实例是否已经被创建,避免了大部分的同步开销。其次,在需要创建实例时,使用同步块来确保只有一个线程创建实例。
172+
173+ 以下是DCL的基本实现模式:
174+ ```
175+ public class Singleton {
176+ private static Singleton instance;
177+
178+ private Singleton() {
179+ // 私有构造函数
180+ }
181+
182+ public static Singleton getInstance() {
183+ if (instance == null) {
184+ synchronized (Singleton.class) {
185+ if (instance == null) {
186+ // 此处代码有问题
187+ instance = new Singleton();
188+ }
189+ }
190+ }
191+ return instance;
192+ }
193+ }
194+ ```
195+ 在第一次调用getInstance()方法时,会进行第一次检查,如果实例已经存在,则直接返回实例,避免了不必要的同步开销。如果实例不存在,则进入同步块,在同步块中再次检查实例是否已经被创建,以防止多个线程同时通过了第一次检查。如果实例仍然为null,才会创建实例。
196+ 需要注意的是,DCL需要保证instance变量是volatile类型,以确保其在多线程环境下的可见性。此外,DCL在某些特定的平台或编译器上可能存在问题,例如指令重排序导致的安全性问题。为了解决这些问题,可以使用其他的延迟初始化技术,如静态内部类实现单例模式或者使用线程安全的饿汉式单例等。
197+
198+ 上述的 instance = new Singleton(); 代码有问题:其底层会分为三个操作:
199+ 1 . 分配⼀块内存。
200+ 2 . 在内存上初始化成员变量。
201+ 3 . 把instance引⽤指向内存。
202+ 在这三个操作中,操作2和操作3可能重排序,即先把instance指向内存,再初始化成员变量,因为⼆者并没有先后的依赖关系。此时,另外⼀个线程可能拿到⼀个未完全初始化的对象。这时,直接访问⾥⾯的成员变量,就可能出错。这就是典型的“构造⽅法溢出”问题。
203+
204+ 解决办法也很简单,就是为instance变量加上volatile修饰。
205+ volatile的三重功效:64位写⼊的原⼦性、内存可⻅性和禁⽌重排序。
117206
118207[ 面试官:Java线程与底层操作系统线程是一 一对应的吗? 中篇[ 一] _ mob60475706bec5的技术博客_51CTO博客] ( https://blog.51cto.com/u_15127698/2842977 )
0 commit comments