Java并发编程实战 03 | Java线程状态

news2024/11/15 3:54:31

在本文中,我们将深入探讨 Java 线程的六种状态以及它们之间的转换过程。其实线程状态之间的转换就如同生物生命从诞生、成长到最终死亡的过程一样。也是一个完整的生命周期。

首先我们来看看操作系统中线程的生命周期是如何转换的。

操作系统中的线程状态转换

线程在操作系统中通常有五种状态。

在现代操作系统中,线程被视为轻量级进程,因此操作系统中的线程状态实际上与进程状态类似。

从实际意义上讲,除了 new 和 terminated 状态外,线程主要有以下三种状态:

  • 就绪 (Ready) :线程已准备好执行,但可能由于调度策略或其他因素未能获得 CPU ,处于就绪状态的线程只要获得CPU,它就会进入运行状态。
  • 正在运行(RUNNING):线程当前正在占用CPU,执行其任务。
  • 等待(WAITING):线程正在等待某些事件的发生或资源的获取(例如I/O操作)。

new 和 terminated 状态在线程的实际运行过程中并不频繁涉及,因此讨论这两个状态在实际应用中并不具有太大意义。

Java 线程的 6 种状态

在 Java 中,线程状态的定义与操作系统中的状态并不完全相同。Java 的线程状态提供了更细粒度的管理。

Java 线程状态通过 java.lang.Thread.State 枚举进行定义:

public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

这些状态之间的转换关系如下图所示:

接下来我们就对Java线程的六种状态进行深入分析。

1. NEW

线程对象被创建出来但是start() 方法还没有被调用,这个时候线程处于new状态。

public class ThreadStateDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {});
        System.out.println(thread.getState());
    }
}

//输出:
NEW

处于new状态的线程可以扭转到RUNNABLE状态。

2. RUNNABLE

处于new状态的线程,通过调用Thread实例的start()方法可以使线程进入RUNNABLE状态。

注意,Java线程中的RUNNABLE状态对应的是操作系统中线程定义的两种 状态:

  1. 就绪 (Ready)
  2. 正在运行(RUNNING)

也就是说在Java中,当一个线程正在运行时,如果CPU时间片用完了, CPU 被调度去执行其他任务,导致该线程暂时停止运行,它的状态仍保持为 RUNNABLE。因为该线程随时可能被重新调度回 CPU 上继续执行。

一个简单的线程示例:

public class ThreadExample {
    public static void main(String[] args) {
        // 创建一个线程对象,但尚未启动
        Thread myThread = new Thread(() -> {
            System.out.println("线程正在运行...");
        });

        // 打印线程状态,是 NEW
        System.out.println("线程状态: " + myThread.getState());

        // 启动线程
        myThread.start();

        // 打印线程状态,是 RUNNABLE(取决于线程调度)
        System.out.println("线程状态: " + myThread.getState());
    }
}

前面讲了处于NEW状态的线程,通过调用Thread实例的start()方法进入RUNNABLE状态。关于start()方法,其实有两个值得思考的问题:

  1. 是否可以在同一个线程对象上重复调用 start() 方法?
  2. 如果一个线程已经执行完毕,处于 TERMINATED 状态,是否可以再次调用 start() 方法?

要回答这两个问题,我们可以查看 start() 方法的源码。

public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
        }
    }
}

在 start() 方法中,可以看到一个名为 threadStatus 的变量。如果这个变量不等于 0,再次调用 start() 方法时,就会直接抛出 IllegalThreadStateException 异常。

接着,start() 方法调用了一个名为 start0() 的方法,该方法是一个本地方法(native method),由底层操作系统或虚拟机实现,因此我们无法从 Java 代码中看到它对 threadStatus 的具体处理方式。不过,这并不妨碍我们了解其行为。

我们可以通过在调用 start() 方法时打印出当前线程的状态,然后尝试多次调用 start() 方法,以观察并理解 IllegalThreadStateException 异常的触发条件和线程状态的变化。

public class ThreadStateDemo {

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {});
        System.out.println(thread.getState());

        //第一次调用
        thread.start(); 
        System.out.println(thread.getState());
        //第二次调用
        thread.start(); 
    }
}

//输出:
NEW
RUNNABLE
Exception in thread "main" java.lang.IllegalThreadStateException
    at java.lang.Thread.start(Thread.java:708)
    at thread.basic.ThreadStateDemo.main(ThreadStateDemo.java:11)

可以看到,第一次调用 start() 方法是没有问题的,但在第二次调用时会报错。错误信息显示在 java.lang.Thread.start(Thread.java:708) 处,这个错误的原因是线程的状态检查失败。

这是因为当线程的 start() 方法第一次被调用时,线程被正确启动并进入 RUNNABLE 状态。第二次再调用start()时,由于线程的状态不再是初始NEW状态(0),直接抛出异常。

最后总结一下:

  1. 如果尝试在同一个线程上重复调用 start() 方法,会抛出 IllegalThreadStateException 异常,也就是说同一个线程对象只能启动一次。
  2. 如果线程已经完成(处于 TERMINATED 状态),再次调用 start() 方法也是不允许的,会同样抛出 IllegalThreadStateException 异常。线程一旦结束就不能再被重新启动。

处于RUNNABLE状态的线程根据不同的条件可以扭转到BLOCKED,WAITING,TIMED_WAITING,TERMINATED等状态。

3. BLOCKED

当线程处于 BLOCKED 状态时,表示它正在等待获取一个锁,以便进入同步区域。

我们可以用一个生活中的例子来说明 BLOCKED 状态:假设你去银行办理业务,当你走到某个窗口时,发现已经有一个人在你前面,此时你必须等到前面的人办完业务并离开窗口后,才能开始办理你的业务。

在这个例子中,你就是线程 B,前面的人是线程 A。当 A 正在占用窗口(即锁),而 B 需要等待 A 完成并释放窗口资源,这期间线程 B 就处于 BLOCKED 状态。

针对这个例子,写了一个简单的代码,如下:

public class BlockCase {

    private synchronized void businessProcessing() {
        try {
            System.out.println("Thread[" + Thread.currentThread().getName() + "] performs business processing");
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        BlockCase blockCase = new BlockCase();
        Thread A = new Thread(blockCase::businessProcessing, "A");
        Thread B = new Thread(blockCase::businessProcessing, "B");

        A.start();
        B.start();
        System.out.println("Thread[" + A.getName() + "] state:" + A.getState());
        System.out.println("Thread[" + B.getName() + "] state:" + B.getState());
    }
}

在这个例子中,我们使用 Thread.sleep() 方法来模拟业务处理所需的时间。

你可能会觉得线程 A 会首先调用同步方法,而在同步方法内调用 Thread.sleep() 方法,使其进入 TIMED_WAITING 状态;与此同时,线程 B 则在等待线程 A 释放锁,因此它的状态会是 BLOCKED。然而,实际情况并不总是如此!这是因为:

  1. 除了线程 A 和线程 B,程序中还有一个主线程在运行。
  2. 当我们调用 start() 方法启动线程时,线程从调用 start() 到真正开始执行 run() 方法之间存在一定的时间差。在这个时间差内,CPU 的调度竞争结果会导致不同的输出。

下面是一种可能的输出:

//输出:
Thread[A] performs business processing
Thread[A] state:RUNNABLE
Thread[B] state:BLOCKED
Thread[B] performs business processing

这种场景下,线程 A 正在执行 businessProcessing 方法,线程 B 正在等待获取 businessProcessing 方法的锁,因此它处于 BLOCKED 状态。

如果你希望线程 A 打印出 TIMED_WAITING 状态,而线程 B 打印出 BLOCKED 状态,可以稍微修改主线程的逻辑。在调用 A.start() 后,让主线程“休息一会儿”,使用 Thread.sleep() 方法让线程 A 有时间去获取锁。

注意的是,主线程的休眠时间应该足够长,确保线程 A 正在执行并进入 TIMED_WAITING 状态,但又不应太长,以免线程 A 完成任务并释放锁。这样,在线程 A 执行期间,线程 B 仍然会尝试获取锁并进入 BLOCKED 状态。

这样一来,我们可以有效地控制两个线程的状态,使得线程 A 进入 TIMED_WAITING 状态,而线程 B 则处于 BLOCKED 状态。

public static void main(String[] args) throws InterruptedException {
        BlockCase blockCase = new BlockCase();
        Thread A = new Thread(blockCase::businessProcessing, "A");
        Thread B = new Thread(blockCase::businessProcessing, "B");

        // Fixed output TIMED_WAITING state and BLOCKED state

        A.start();
        Thread.sleep(1000); //Sleep time should be less than business processing time
        B.start();
        System.out.println("Thread[" + A.getName() + "] state:" + A.getState());
        Sy  stem.out.println("Thread[" + B.getName() + "] state:" + B.getState());

}

//输出:
Thread[A] performs business processing
Thread[A] state:TIMED_WAITING
Thread[B] state:BLOCKED
Thread[B] performs business processing 

在这个例子中,两个线程的状态会按照以下步骤转换:

线程 A 的状态转换过程:

  1. NEW: 线程 A 被创建,但还未启动。
  2. RUNNABLE: 调用 A.start() 后,线程 A 进入可运行状态,等待被 CPU 调度。
  3. TIMED_WAITING: 线程 A 获取到锁后,调用 Thread.sleep() 方法进入计时等待状态。
  4. RUNNABLE: 等待时间结束,线程 A 重新进入可运行状态。
  5. TERMINATED: 线程 A 执行完任务,进入终止状态。

线程 B 的状态转换过程:

  1. NEW: 线程 B 被创建,但还未启动。
  2. RUNNABLE: 调用 B.start() 后,线程 B 进入可运行状态,等待被 CPU 调度。
  3. BLOCKED: 线程 B 尝试获取锁失败,因为线程 A 已经持有锁,所以 B 进入阻塞状态。
  4. RUNNABLE: 线程 A 释放锁后,线程 B 获取到锁,进入可运行状态。
  5. TIMED_WAITING: 线程 B 进入临时等待状态(例如,通过 Thread.sleep())。
  6. RUNNABLE: 等待时间结束,线程 B 重新进入可运行状态。
  7. TERMINATED: 线程 B 执行完任务,进入终止状态。

处于BLOCKED状态的线程获取到锁后可以扭转到RUNNABLE状态。

4. WAITING

线程进入WAITING状态的方式有三种:

  1. Object.wait():将当前线程置于等待状态,直到另一个线程调用同一对象的 notify() 或 notifyAll() 方法来唤醒它。
  2. Thread.join():使当前线程等待指定的线程执行完毕后再继续运行。底层实现是调用 Object.wait() 方法。
  3. LockSupport.park():使当前线程进入等待状态,直到被显式地唤醒。它的控制权完全取决于是否获得了唤醒权限。

让我们继续用之前的银行办理业务的例子来解释 WAITING 状态。

假设你在银行办理业务时,终于轮到你到柜台办理了。但是,不幸的是,柜台的电脑突然坏了。为了完成业务,你必须等待维修人员修好电脑后才能继续办理。

在这个场景中,假设你是线程 A,维修人员是线程 B。尽管你已经在柜台前等待(即获得了锁),但是你还要释放锁,此时线程A的状态是WAITING,然后线程B获得锁,进入RUNNABLE状态。

如果线程 B 不主动唤醒线程 A(通过调用 notify() 或 notifyAll() 方法),线程 A 将会一直处于等待状态,无法继续执行。

以下是一个简单的代码示例,演示了如何使用 Object.wait() 和 notify() 方法来实现这种行为:

public class WaitingCase {

    private synchronized void businessProcessing() {
        try {
            System.out.println("Thread[" + Thread.currentThread().getName() + "] expects to process business, but the computer is broken");
            // Release the monitor(lock)
            wait();
            // business processing
            System.out.println("Thread[" + Thread.currentThread().getName() + "] continues to process business");
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    private synchronized void repairComputer() {
        System.out.println("Thread[" + Thread.currentThread().getName() + "] comes to repair the computer");
        try {
            // Simulated Repair
            Thread.sleep(1000L);
            System.out.println("Thread[" + Thread.currentThread().getName() + "] has completed the repair.");
            notify();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        WaitingCase blockedCase = new WaitingCase();
        Thread A = new Thread(blockedCase::businessProcessing, "A");
        Thread B = new Thread(blockedCase::repairComputer, "B");

        A.start();
        Thread.sleep(500); //Used to ensure that thread A grabs the lock first. Sleep time should be less than repair time
        B.start();
        System.out.println("Thread[" + A.getName() + "] state:" + A.getState());
        System.out.println("Thread[" + B.getName() + "] state:" + B.getState());
    }
}

//输出:
Thread[A] expects to process business, but the computer is broken
Thread[B] comes to repair the computer
Thread[A] state:WAITING
Thread[B] state:TIMED_WAITING
Thread[B] has completed the repair.
Thread[A] continues to process business

关于 wait() 方法,有几个关键点需要特别强调:

  1. 持有锁:在调用 wait() 方法之前,线程必须先获得对象的监视器(锁)。换句话说,调用 wait() 的线程必须在同步代码块或同步方法中运行,即持有对象的锁。

  2. 释放锁:当线程调用 wait() 方法时,它会释放当前持有的锁,并进入等待状态。线程将保持在等待状态,直到其他线程调用 notify() 或 notifyAll() 方法来唤醒它。

  3. notify() 方法:调用 notify() 方法只能唤醒一个正在等待该锁的线程。如果有多个线程在等待同一个对象的锁,notify() 方法只会唤醒其中一个线程,这个线程并不是固定的,具体哪个线程被唤醒取决于线程调度的具体实现。

  4. notifyAll() 方法:调用 notifyAll() 方法会唤醒所有正在等待该锁的线程。这些被唤醒的线程会竞争重新获得锁,但并不保证它们会立即得到 CPU 时间片,具体的调度顺序取决于操作系统的线程调度策略。

我们再来看看Thread.join()方法。

join() 方法用于使调用线程暂停执行,直到被调用的线程执行完毕。调用 join() 的线程将进入 WAITING 状态,直到目标线程完成执行。这个方法常用于主线程中,确保在继续执行之前等待其他线程完成。

我们来回顾一下之前的 BlockCase 示例,其中 A.start() 和 B.start() 都是在主线程中直接调用的。这就像是让多个线程竞争窗口的使用权。如果参与竞争的线程越来越多,窗口就会变得非常拥挤。

为了改善这个问题,银行引入了一个新的办法:给每个办理业务的客户一个编号,按编号叫号。只有被叫到的客户才能到窗口办理业务,其余的客户则可以在休息区等待。

现在,我们可以在之前的 BlockCase 示例中扩展这个想法,假设我们有三个线程来模拟这个场景。每个线程代表一个客户,我们将使用 join() 方法来确保主线程等待所有客户(线程)完成业务后才继续执行。

public class JoinCase {

    private synchronized void businessProcessing() {
        try {
            System.out.println("Thread[" + Thread.currentThread().getName() + "] performs business processing");
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        JoinCase blockedCase = new JoinCase();
        Thread A = new Thread(blockedCase::businessProcessing, "A");
        Thread B = new Thread(blockedCase::businessProcessing, "B");
        Thread C = new Thread(blockedCase::businessProcessing, "C");

        System.out.println("Please ask thread A to go to the window to handle the business.");
        A.start();
        A.join();
        System.out.println("Please ask thread B to go to the window to handle the business.");
        B.start();
        B.join();
        System.out.println("Please ask thread C to go to the window to handle the business.");
        C.start();
    }
}

//输出:
Please ask thread A to go to the window to handle the business.
Thread[A] performs business processing
Please ask thread B to go to the window to handle the business.
Thread[B] performs business processing
Please ask thread C to go to the window to handle the business.
Thread[C] performs business processing

您可以尝试多次执行该程序,并且总是会得到相同的结果。

处于WAITING状态的线程被其他线程唤醒可以扭转到RUNNABLE状态。

5.TIMED_WAITING

超时等待状态 (TIMED_WAITING) 是线程在指定时间内等待的状态,时间到后线程会自动唤醒。以下方法可以使线程进入超时等待状态:

  • Thread.sleep(long millis):使当前线程休眠指定的时间,并不会释放锁。这种方法使线程进入超时等待状态,但在此期间线程持有锁。
  • Object.wait(long timeout):使线程等待指定的时间,即使没有其他线程通过 notify() 或 notifyAll() 唤醒它,也会在超时时自动唤醒。
  • Thread.join(long millis):使当前线程等待指定线程最多 millis 毫秒,如果 millis 为 0,则一直等待,直到目标线程结束。
  • LockSupport.parkNanos(long nanos):禁止当前线程在指定时间内进行线程调度,除非获得调用权限。
  • LockSupport.parkUntil(long deadline):与 parkNanos() 类似,但使用绝对时间戳作为参数。

处于TIMED_WAITING状态的线程被其他线程唤醒或等待的时间到了以后被扭转到RUNNABLE状态。

6. TERMINATED


当线程已完成执行时,处于TERMINATED状态。

已经终止的线程无法再扭转到其它状态

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2111078.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Web安全之GroovyShell讲解:错误与正确示范,安全问题与解决方案

1. 引言 Groovy 是一门基于 Java 虚拟机(JVM)的动态语言,而 GroovyShell 是 Groovy 提供的一个灵活强大的脚本执行工具。通过 GroovyShell,开发者可以在运行时动态执行 Groovy 脚本,它的灵活性非常适合那些需要动态编…

本地使用Docker部署Nexus Maven私有仓库工具结合内网穿透实现远程管理

文章目录 前言1. Docker安装Nexus2. 本地访问Nexus3. Linux安装Cpolar4. 配置Nexus界面公网地址5. 远程访问 Nexus界面6. 固定Nexus公网地址7. 固定地址访问Nexus 前言 本文主要介绍在Linux中使用Docker来一键部署Nexus Maven私有仓库工具并结合Cpolar内网穿透实现远程访问Nex…

【Python】应用:pyproj地理计算库应用

😏★,:.☆( ̄▽ ̄)/$:.★ 😏 这篇文章主要介绍pyproj地理计算库应用。 无专精则不能成,无涉猎则不能通。——梁启超 欢迎来到我的博客,一起学习,共同进步。 喜欢的朋友可以关注一下,下…

中国传媒业人工智能应用发展图谱2024

易观分析:传媒产业是指以传播各类信息、知识为核心,通过多种媒介形式进行内容生产、发布和分发的综合性产业。技术的进步和应用对于传媒产业发展变革起到了核心驱动力的作用,2022年生成式AI进入应用爆发期,不仅带动了人工智能产业…

APP中断测试知多少

中断测试有助于移动测试人员识别与系统或用户行为相关的潜在风险,以便在发生流量干扰时能够识别出可能导致意想不到或不期望结果的问题。 让我们深入探讨中断测试的细节及其手动或程序化实现方式。本文还介绍了用于自动化测试过程、加快测试执行速度并向用户提供高…

士兰微 SC32F5432 通过配置寄存器方式 将管脚配成开漏输出模式和TTL输入模式

目录 前言: 士兰微电子介绍 士兰微 SC32F5432介绍 士兰微 SC32F5432 通过配置寄存器方式 将管脚配成开漏输出模式和TTL输入模式 开漏输出模式 TTL输入模式 前言: 下面是对我在工作时公司所使用的一款国产芯片(士兰微 SC32F5432&#x…

【Qt】事件的处理

事件的处理 事件的处理,让一段代码和某个事件关联起来,当事件触发的时候,就能指定这段代码。在之前学习的信号槽是通过 connect 来完成上述关联的,对于事件来说,需要让当前的类,重写某个事件处理函数。这里…

【服务对接】✈️SpringBoot 项目整合华为云 obs 对象存储服务

目录 👋前言 👀一、环境准备 🌱二、整合实现 1.依赖引入 2.准备 AK 和 SK ​ 3.配置类 4.obs 工具类封装 💞️三、测试使用 🍻四、 obs 客户端 📫五、章末 👋前言 小伙伴们大家好&…

2024国赛数学建模C题完整论文:农作物的种植策略

农作物种植策略优化的数学建模研究(完整论文,持续更新,大家持续关注,更新见文末名片 ) 摘要 在本文中,建立了基于整数规划、动态规划、马尔科夫决策过程、不确定性建模、多目标优化、相关性分析、蒙特卡洛…

Packet Tracer - 单区域OSPFv2的配置方法以及思路

Packet Tracer - 单区域OSPFv2的配置思路 1、思路前夕查看 做这个的时候大家了解一下通配符,不然不理解这个东西为什么子网掩码为什么会取反 这里给大家简单演示一下 2、使用进程 ID 10 在所有路由器上激活 OSPF。 在 Headquarters 网络中的路由器上使用 network…

FxFactory 8 for Mac 视觉特效插件包安装

Mac分享吧 文章目录 介绍页面效果一、下载软件二、开始安装1、Install安装2、显示软件页面,表示安装成功3、补丁安装 三、注意事项1、若已安装过其他版本,需要使用软件自带的卸载功能进行软件卸载,再安装此版本 安装完成!&#x…

pod install 报错处理

由于墙的原因,pod install 、 pod update经常报错 有效的解决方案(推荐): 以SnapKit为例 找不报错的同事要以下两个文件(指定的版本) 1. /Users/xxx/Library/Caches/CocoaPods/Pods/Release/SnapKit 2. /Users/xxx/Library/Cac…

后缀表达式转中缀表达式

假定有后缀表达式1 2 3 4 * 5 – ,请将它转化为前缀表达式。 利用表达式树: 1.从左到右扫面后缀表达式,一次一个符号读入表达式。2.如果符号是操作数,那么就建立一个单节点树并将它推入栈中。如果符号是操作符,那么…

针对不同区域的摄像头,完成不同的算法配置的智慧快消开源了

智慧快消视频监控平台是一款功能强大且简单易用的实时算法视频监控系统。它的愿景是最底层打通各大芯片厂商相互间的壁垒,省去繁琐重复的适配流程,实现芯片、算法、应用的全流程组合,从而大大减少企业级应用约95%的开发成本。 基于多年的深度…

JAVAEE初阶第六节——网络编程套接字

系列文章目录 JAVAEE初阶第六节——网络编程套接字 文章目录 系列文章目录JAVAEE初阶第六节——网络编程套接字 一. 网络编程基础1. 为什么需要网络编程2. 什么是网络编程3.网络编程中的基本概念 3.1 发送端和接收端 3.2 请求和响应 3.3 客户端和服务端 4. 常见的客户端服务…

求二叉树的深度——(力扣c语言)

题目如下: 给定一个二叉树 root ,返回其最大深度。 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。 示例 1: 输入:root [3,9,20,null,null,15,7] 输出:3示例 2: 输入&#xff1a…

[网络编程]TCP和UDP的比较 及 通过java用UDP实现网络编程

文章目录 一. 网络编程套接字TCP和UDP的特点有连接 vs 无连接可靠传输 vs 不可靠传输面向字节流 vs 面向数据报全双工 vs 半双工 二. java用UDP实现网络编程代码实现:运行代码:启动多个客户端别人能否使用?实现翻译功能 一. 网络编程套接字 网络编程套接字, 就是指操作系统提…

linux 部署Ollama本地大模型

简介 llama 是一个大模型的管理框架,其作用类似于 Docker:如果将每一个标准化的大模型视为“镜像”,那么 Ollama 就能够通过一行命令快速拉取并运行这些大模型。然而,Ollama 本身是基于命令行的服务,所以为了方便我们…

足底筋膜炎怎么治

足底筋膜炎是一种常见的足部疾病,其主要症状及治疗方法如下: 一、症状 1、‌疼痛‌:足底筋膜炎最典型的症状是足跟或足底靠近足跟处的疼痛。这种疼痛在晨起或长时间休息后初次站立时尤为明显,但行走一段时间后可能会逐渐缓解。 …

超级兔子and这三款数据恢复软件,我的数据守护神!!

在数字化的时代,数据丢失已经成为了一个令人头疼的问题。无论是误删重要文件,还是硬盘出现故障,数据的丢失都可能带来不可估量的损失;幸运的是,有了超级兔子这这三款数据恢复软件这样的工具,让数据找回变得…