跳到主要内容

04-Java 多线程知识汇总(二)内存可见问题,线程死锁,volatile

内存不可见问题?如何解决?

image-20240716215500084

图所示的是一个双核 CPU的系统架构,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辑运算。每个核都有自己的一级缓存,在有些架构里面还有一个所有CPU都共享的二级缓存。那么Java内存模型里面的工作内存,就对应这里的L1或者L2缓存或者CPU的寄存器。

当一个线程操作共享变量时,它首先从主内存复制共享变量到自己的工作内存,然后对工作内存里的变量进行处理,处理完后将变量值更新到主内存。

那么假如线程A和线程B同时处理一个共享变量,会出现什么情况?我们使用图2-5所示CPU架构,假设线程A和线程B使用不同CPU执行,并且当前两级 Cache 都为空,那么这时候由于 Cache 的存在,将会导致内存不可见问题,具体看下面的分析。

  • 线程A首先获取共享变量X的值,由于两级 Cache 都没有命中,所以加载主内存中X的值,假如为0。然后把X=0的值缓存到两级缓存,线程 A 修改 X 的值为 1,然后将其写入两级 Cache,并且刷新到主内存。线程A操作完毕后,线程A所在的CPU的两级Cache 内和主内存里面的X的值都是1。
  • 线程B获取X的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回 X= 1;到这里一切都是正常的,因为这时候主内存中也是X=1。然后线程B修改X的值为2,并将其存放到线程2所在的一级 Cache 和共享二级 Cache 中,最后更新主内存中 X 的值为2;到这里一切都是好的。
  • 线程A 这次又需要修改 X 的值,获取时一级缓存命中,并且 X=1,到这里问题就出现了,明明线程B 已经把 X 的值修改为了 2,为何线程A获取的还是1呢?这就是共享变量的内存不可见问题,也就是线程 B 写入的值对线程 A 不可见。**那么如何解决共享变量内存不可见问题?**使用 Java 中的 volatile 关键字就可以解决这个问题,

一般在什么时候才使用 volatile关键字呢?

  • 写入变量值不依赖变量的当前值时。因为如果依赖当前值,将是获取—计算—写入三步操作,这三步操作不是原子性的,而volatile 不保证原子性。
  • 读写变量值时没有加锁。因为加锁本身已经保证了内存可见性,这时候不需要把变量声明为 volatile 的。

死锁的四个条件

死锁的产生必须具备以下四个条件。

  • 互斥条件:指线程对已经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
  • 请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
  • 不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。
  • 环路等待条件:指在发生死锁时,必然存在一个线程—资源的环形链,即线程集合{T0, T1, T2, …, Tn}中的T0正在等待一个T1占用的资源,T1 正在等待 T2 占用的资源,……Tn 正在等待已被T0占用的资源。

要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,但是学过操作系统的读者应该都知道,目前只有请求并持有和环路等待条件是可以被破坏的。

守护线程与用户线程

Java 中的线程分为两类,分别为 daemon 线程(守护线程)和 user 线程(用户线程)。在 JVM 启动时会调用 main 函数,main 函数所在的线程就是一个用户线程,其实在 JVM 内部同时还启动了好多守护线程,比如垃圾回收线程。那么守护线程和用户线程有什么区别呢?区别之一是当最后一个非守护线程结束时,JVM 会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响 JVM 的退出。言外之意,只要有一个用户线程还没结束,正常情况下 JVM 就不会退出。

💡本文声明

转载请注明出处,谢谢合作!转载本文请声明原文章链接如下:

原文链接: https://zhoujun134.github.io/docs/java/04-mutil-thread-02

作者: Z 不殊

Z 不殊 致力于分享有价值的信息和知识。我们尊重并保护知识产权。本文仅代表作者观点,不代表任何立场。 如果本文有所侵权,请联系作者删除或修改!

Loading Comments...