Java中的多线程和线程安全问题

线程

线程是操作系统进行调度的最小单位。一个进程至少包含一个主线程,而一个线程可以启动多个子线程。线程之间共享进程的资源,但也有自己的局部变量。多线程程序和普通程序的区别:每个线程都是一个独立的执行流;多个线程之间是并发执行的。

在这里插入图片描述

多线程的实现方法

继承Thread类

class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }

    public MyThread() {
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println(this.getName() + ":" + i);
        }
    }
}

public class Text {
    public static void main(String[] args) {
        //实例化对象  创建线程
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread("线程t2");
        t1.start();
        t2.start();  //启动线程

        for (int i = 0; i < 5; i++) {
            System.out.println("hello main");
        }
    }
}

使用lambda表达式创建线程

public class Demo1 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (true) {
                System.out.println("t1线程");
            }
        });

        Thread t2 = new Thread(() -> {
            while (true) {
                System.out.println("t2线程");
            }
        });
        t1.start();
        t2.start();

        while (true) {
            System.out.println("hello main");
        }
    }
}

start()和run()的区别

start()是启动一个分支线程 是一个专门用来启动线程的方法 而run()是一个普通方法和main方法是同级别的 单纯调用run方法是不会启动多线程并发执行的.

public class Demo2 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName()+ ":我还存活");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println(Thread.currentThread().getName() + ":我即将死去");
        });

        System.out.println(Thread.currentThread().getName() + ": 状态" + thread.getState());

        //启动线程
        thread.start();
        boolean flg = thread.isAlive();  //是否存活
        System.out.println(Thread.currentThread().getName() + ": 状态:" + thread.getState());
    }
}

上面的代码涉及到的一些线程的方法
在这里插入图片描述
还有获取线程状态的方法是getState()

sleep()方法

该方法就是让线程休眠 括号里面写休眠的时间 单位是毫秒级别 下面通过代码来描述

public class Demo3 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("hello t1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5;i++) {
                System.out.println("hello t2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        
        t1.start();
        t2.start();
    }
}

通过上面的代码 就可以控制两个线程没执行一遍就休眠一秒钟 再继续执行下一遍。

join方法

多线程的join方法就是用来等待其他线程的方法 就是谁调用该方法谁就等待 join()这种是死等 ,当然也可以有时间的等 过了这个时间就不等了
就类似舔狗 有写舔狗 舔自己的女神 可能 会一直舔 舔到死 那种 有些就是有原则的舔 ,可能就在固定的时间内舔 ,过了这个时间段就坚决不舔。

代码实现如下:

public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("t2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        Thread t1 = new Thread(() -> {
            //t1等t2
            try {
                Thread.sleep(500);
                t2.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            for (int i = 0; i < 5; i++) {
                System.out.println("t1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        t1.start();
        t2.start();
        t1.join();
        System.out.println("main end");
    }
}

import java.time.Year;

public class Demo5 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("t1线程在执行");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        t1.start();
        t1.join(3000);
        System.out.println("main线程over");
    }
}

运行结果:
在这里插入图片描述
通过代码和运行截图可以看出 main线程在等了3秒之后就结束了 而t1线程还没执行完。
join方法的底层原理:
join方法的工作原理基于Java的内置锁机制。当你调用join方法时,当前线程会尝试获取目标线程对象的锁,并在目标线程执行完毕后释放锁。在这个过程中 ,当前调用的线程就会被阻塞等待,直到目标线程结束执行并释放锁,当前线程才能执行。

线程的状态

在Java官方的线程状态分类中 ,一共给出6种线程状态。
在任意一个时间点 ,一个线程就有且仅有一种状态

6种状态如下

NEW:创建好线程但是还没启动

RUNNABLE该状态是已经调用了start()方法 是可以工作的状态 但这种状态的线程有两种情况 :一种是正在执行 还要一种就是在等待CPU分配执行时间。

BLOCKED: 该状态就是线程被阻塞了,在等待别的线程的锁

WAITING:这种状态就是无限期等待 CPU不会给他分配执行时间 这种要等别的线程来唤醒。

TINED_WAITING: 这种就是有限期德等待,在一定时间之后就会被系统自动唤醒。

TERMINATED:工作完成了 线程已经执行结束了 。

线程状态的转换

在这里插入图片描述

线程的安全问题

那什么叫做线程安全呢
就是多线程环境下代码运行的结果是符合问你预期的,即在单线程环境应该的结果,则表示该线程是安全的 ,否则该线程就是不安全的。

线程不安全的原因

线程的调度是随机的 这是线程不安全的罪魁祸首
随机调度使一个程序在多线程环境下,执行顺序存在很多变数
多线程是一个神奇的东西

下面我们通过一个代码来看看什么是线程的不安全 也就是有bug 和预期效果不符。

public class Demo6 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " + count);
    }
}

运行结果:
在这里插入图片描述
在这里插入图片描述
结果不符合预期 预期结果是100000 但是却输出小于10万的数,且每次运行的结果都不一样 为什么呢?
1、count++这个操作,站在cpu指令的角度来说,其实是三个指令。
load :把内存中的数据,加载到寄存器中
add:把寄存器中的值 + 1
save :把寄存器中的值写回到内存中

2、两个线程并发执行的进行count++
因为多线程的执行是随机调度,抢占式执行的模式

相当于某个线程执行指令的过程中,当她执行到任何一个指令的时候都有可能被其他线程把他的cpu资源抢占走。

综上所述,实际并行执行的时候,两个线程执行指令的相对顺序就可能存在无数种可能。

在这里插入图片描述
除了上面的两种可能还有无数种可能。

出现线程不安全的原因:
还是那句话:1、线程在系统中是随机调度的

2、在上面那个代码中 ,多个线程同时修改同一个变量就会出现这种线程不安全的问题

3、线程针对变量的修改操作,不是“原子”的
就像上面count++这种代码 就不是原子操作 因为该操作涉及到三个指令。
但有些操作,虽然也是修改操作 ,但是只有一个指令,是原子的。
比如直接针对 int / double进行赋值操作(在cpu上就只有一个move操作)
相当于来说 ,就是某个代码操作对应到一个cpu指令就是原子的 如果是多个那就是原子的。

那如何解决 该问题呢
那必须得从原因入手

线程调度是随机的这个我们无法干预
我们可以通过一些操作 把上诉非原子操作,打包成一个原子操作
那就是给线程加锁 下面我们举个例子:
就比如上厕所 现在厕所里面就一个坑位 现在来了两个人A和B 现在A先进去上厕所了 结果 A还没上完 B就冲了进去 这显然是不科学的 。在现实生活中,一般我们上厕所都会锁上门 。A进去上厕所把门给锁上,这时B要是也想进去上厕所就得等待A上完厕所解锁出来 这时B才能进去接着上厕所。

:本质上也是操作系统提供的功能 通过api给到应用程序 ;在Java中JVM对于这样的操作系统又进行了封装。

synchronized对象锁(可重入锁)

在Java中 我们引入synchronized 关键字
synchronized()括号里面就是写锁的对象

锁对象的用途,有且仅有一个 ,就是用来区分 两个线程是否针对同一个对象加锁
如果是 那就会出现锁竞争 /互斥 就会引起阻塞等待
如果不是,就不会出现锁竞争 ,也就不会出现阻塞。

下面给你们看看加上锁之后的代码

public class Demo6 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (locker) {
                    count++;
                }
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (locker) {
                    count++;
                }
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " + count);
    }
}

运行的结果:
在这里插入图片描述
加锁之后 明显线程就没有bug了变安全了

加上锁之后 当t1进入count操作的时候 ,如果t2想进去执行 就会阻塞等待 因为 现在锁在t1的手里

还有 一种嵌套加锁 就是在第一个锁的基础上再加一个锁 就相当于 你要获取第二个锁得先执行完第一个锁 要想执行完第一个锁 ,得获取到第二个锁 ,这就相互矛盾了 就产生死锁看了

但是实际上 对于synchronized是不适用的 这个锁在上面这种情况下不会出现死锁 但是这种情况在C++和Python中就会出现死锁。

synchronized没有出现上诉情况是因为自己内部进行了特殊的处理(JVM)
每个锁对象里,会记录当前是哪个线程持有这个锁。
当针对这个对象加锁操作时,就会先判定一下,当前尝试加锁线程是否是持有锁的线程
如果不是就阻塞 否则就直接放行 不会阻塞。

场景二: ;两个线程 两把锁
现在有线程t1 和t2 以及锁A和锁B 现在这两个线程都需要获取这两把锁 ‘拿到锁A后 不释放锁A ,继续去获取锁B 就相当于 先让两个线程分别拿到一把锁,然后去尝试获取对方的锁。

举个例子: 疫情期间,现在广东的健康吗崩了 程序猿赶紧来到公司准备修bug 被保安拦住了
保安: 请出示健康吗 才能上楼
程序猿:我得上楼修复bug才能出示健康码

就这样 如果两个人互不相让 就会出现僵住的局面。
类似的 还有 钥匙锁在车里 ,而车钥匙锁屋子里了 这样也是僵住了

下面我们通过代码来实现一下 该情况:

public class Demo8 {
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                synchronized (locker2) {
                    System.out.println("t1获取到两把锁");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (locker2) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                synchronized (locker1) {
                    System.out.println("t2获取到两把锁");
                }
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

运行结果:
在这里插入图片描述
你会发现现在运行起来什么都没有
t1尝试针对locker2加锁 就会阻塞等待 等待t2释放locker2 ,而t2尝试针对locker1加锁 也会阻塞等待等待t1 释放locker1.

这就相当于两个互相暗恋的人 你喜欢我 我也喜欢你 谁都在等对方 但是没人主动 说出来 终究是会错过。

针对这个问题 我们可以不用使用嵌套锁 ,但是也可以规定获取锁的顺序 比如说 t1和t2
线程规定好 先对locker1 加锁 再对locker2加锁 。这样就不会出现死锁的情况了。

产生死锁的四个必要条件

1.互斥条件
每个资源不能同时被两个或更多个线程使用。
2、不可剥夺性
一旦一个线程获得了 资源,除非该线程自己释放 ,否则其他线程不难强行剥夺这些资源。
3、请求和保持条件
一个线程因请求资源而阻塞时,必须保持自己的资源不放。如果一个线程在请求资源之前就释放了已获得的资源,那么就不会发生死锁现象。
4循环等待条件
如果存在一个资源等待链 ,即P1正在等待P2释放的资源 ,P2正在等待P3释放的资源 ,以此类推,最后Pn又在等待P1释放的资源。

以上四个条件必须同时满足,才能产生死锁现象 在实际开发中我们应该合理设计代码 避免死锁的发生。

在上面这四种产生死锁的条件中 前面两个是线程的基本特征 ,我们无法干预 ,最好解决死锁的方法就是破除条件3或者条件4 条件3 要破解 就需要 不要写锁嵌套 ,那如果非要写成锁嵌套怎么办 ,那就是破解第四个条件 当代码中有多个线程获取多把锁的情况 ,我们就需要统一规定好加锁的顺序 ,这样就能有效的避开死锁的现象。

内存可见性 引起的线程安全问题

下面我们通过一个代码来展示这个线程安全问题:

import java.util.Scanner;

public class Demo1 {
    private  static int count = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (count == 0) {
                ;
            }
            System.out.println("t1 执行结束");
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数 :");
            count = scanner.nextInt();
        });

        t1.start();
        t2.start();
    }
}

运行结果:
在这里插入图片描述
你会发现虽然修改了count的值 但是程序并没有结束 这就和我们预期的出现了差错 出现bug了 那是为什么呢?

在那个while循环里面 会执行 load和cmp这两个指令
**load 😗*从内存读取数据到寄存器
cmp:(比较,同时产生跳转) 条件成立,就继续执行程序 条件不成立,就会跳到另一个地址来执行

当前的循环旋转速度很快 短时间内会有大量的load 和 cmp 反复执行的效果 load执行消耗的时间 会比 cmp 多很多
这样JVM 就会因为load执行速度慢 而每次load的结果都是一样的 JVM 就会干脆 把上面的load操作给优化了 只\有第一次执行load才是真的在进行load 后续再执行到相对应的代码,就不再真正的load了,而是直接读取已经load过的寄存器的值了
当我们在while循环体里面加入一些IO操作 程序运行就要正确了
这又是为什么呢
因为如果循环体里面有IO操作 就会使循环体的旋转速度大幅度降低 ,因为IO操作比load操作要慢得多 所以JVM也就不会再去优化load操作 ,而IO操作是不会被优化的.
内存可见性问题说到底是由于编译器优化引起的,优化掉load操作之后 ,使得t2线程的修改没有被t1线程感知到.
JVM在什么时候优化 什么时候不优化 这也是不确定的
那我们该怎么解决该内存可见性问题呢?

volatile

我们会引入volatile关键字
这个关键字的作用就是告诉编译器不要触发上述优化 volatile关键字是专门针对内存可见性的场景来解决问题的.

关于线程安全问题还有一些内容 我们下篇内容讲解 本篇内容就到此结束了 谢谢大家的浏览 !!!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++模板进阶操作 —— 非类型模板参数、模板的特化以及模板的分离编译

非类型模板参数 模板参数可分为类型形参和非类型形参。类型形参&#xff1a; 出现在模板参数列表中&#xff0c;跟在class或typename关键字之后的参数类型名称。非类型形参&#xff1a; 用一个常量作为类&#xff08;函数&#xff09;模板的一个参数&#xff0c;在类&#xff…

【 书生·浦语大模型实战营】学习笔记(一):全链路开源体系介绍

&#x1f389;AI学习星球推荐&#xff1a; GoAI的学习社区 知识星球是一个致力于提供《机器学习 | 深度学习 | CV | NLP | 大模型 | 多模态 | AIGC 》各个最新AI方向综述、论文等成体系的学习资料&#xff0c;配有全面而有深度的专栏内容&#xff0c;包括不限于 前沿论文解读、…

汇编语言——用INT 21H 的A号功能,输入一个字符串存放在内存,倒序输出

用INT 21H 的A号功能&#xff0c;输入一个字符串“Hello, world!”&#xff0c;存放在内存&#xff0c;然 后倒序输出。 在DOS中断中&#xff0c;INT 21H是一个常用的系统功能调用中断&#xff0c;它提供了多种功能&#xff0c;其中A号功能用于字符串的输入。 在使用这个功能时…

OSCP靶场--Internal

OSCP靶场–Internal 考点(CVE-2009-3103) 1.nmap扫描 ## ┌──(root㉿kali)-[~/Desktop] └─# nmap 192.168.216.40 -sV -sC -Pn --min-rate 2500 -p- Starting Nmap 7.92 ( https://nmap.org ) at 2024-03-31 07:00 EDT Nmap scan report for 192.168.216.40 Host is up…

pymysql进行数据库各项基础操作

目录 1、mysql数据库简介2、基于mysql数据库的准备工作3、通过pymysql进行表的创建4、通过pymysql进行数据插入5、通过pymysql进行数据修改6、通过pymysql进行数据查询7、通过pymysql进行数据删除 本文内容是通过pymysql来进行时数据库的各项基础操作。 1、mysql数据库简介 …

西南交大swjtu算法实验4.2|分治

1. 实验目的 编写一个分治算法来搜索 m*n 矩阵 matrix 中的一个目标值 target&#xff0c;该矩阵 具有以下特性:每行的元素从左到右升序排列。每列的元素从上到下升序排列。 通过该实例熟悉分治算法的分析求解过程&#xff0c;时间复杂度分析方法&#xff0c;以及如何设计 分治…

基于深度学习的图书管理推荐系统(python版)

基于深度学习的图书管理推荐系统 1、效果图 1/1 [] - 0s 270ms/step [13 11 4 19 16 18 8 6 9 0] [0.1780757 0.17474999 0.17390694 0.17207369 0.17157653 0.168248440.1668652 0.16665359 0.16656876 0.16519257] keras_recommended_book_ids深度学习推荐列表 [9137…

ES6 学习(一)-- 基础知识

文章目录 1. 初识 ES62. let 声明变量3. const 声明常量4. 解构赋值 1. 初识 ES6 ECMAScript6.0(以下简称ES6)是JavaScript语言的下一代标准&#xff0c;已经在2015年6月正式发布了。它的目标&#xff0c;是使得」JavaScript语言可以用来编写复杂的大型应用程序&#xff0c;成为…

C之易错注意点转义字符,sizeof,scanf,printf

目录 前言 一&#xff1a;转义字符 1.转义字符顾名思义就是转换原来意思的字符 2.常见的转义字符 1.特殊\b 2. 特殊\ddd和\xdd 3.转义字符常错点----计算字符串长度 注意 &#xff1a; 如果出现\890,\921这些的不是属于\ddd类型的&#xff0c;&#xff0c;不是一个字符…

车载电子电器架构 —— 局部网络管理汇总

车载电子电器架构 —— 局部网络管理汇总 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 屏蔽力是信息过载时代一个人的特殊竞争力,任何消耗你的人和事,多看一眼都是你的不对。非必要不费力证明…

在 Linux(红帽系列) 中使用 yum 工具安装 Nginx 及 Nginx 的常用命令与 Nginx 服务的启动和停止

官方文档&#xff1a;https://nginx.org/en/linux_packages.html 在红帽系列的 Linux 发行版中&#xff0c;使用 yum 工具帮助我们管理和下载安装 rpm 软件包&#xff0c;并帮助我们自动解决 rpm 软件包之间的依赖关系。 关于 yum 可以参考&#xff1a;https://www.yuque.com/u…

ROS2 学习(一)ROS2 简介与基本使用

参考引用 动手学 ROS2 1. ROS2 介绍与安装 1.1 ROS2 的历史 ROS&#xff08;Robot Operating System&#xff0c;机器人操作系统&#xff09;&#xff0c;但 ROS 本身并不是一个操作系统&#xff0c;而是可以安装在现在已有的操作系统上&#xff08;Linux、Windows、Mac&…

无需mac系统申请ios证书的傻瓜式教程

在hbuilderx云打包&#xff0c;无论是开发测试还是打生产包&#xff0c;都需要p12格式的私钥证书、证书密码和证书profile文件。这三样东西都是必须的&#xff0c;点击hbuilderx的官网链接&#xff0c;它创建证书的第一步&#xff0c;就需要使用mac系统的钥匙串访问去生成一个c…

设置asp.net core WebApi函数请求参数可空的两种方式

以下面定义的asp.net core WebApi函数为例&#xff0c;客户端发送申请时&#xff0c;默认三个参数均为必填项&#xff0c;不填会报错&#xff0c;如下图所示&#xff1a; [HttpGet] public string GetSpecifyValue(string param1,string param2,string param3) {return $"…

【Qt】窗口

目录 一、概述二、菜单栏&#xff08;QMenuBar&#xff09;三、工具栏&#xff08;QToolBar&#xff09;四、状态栏&#xff08;QStatusBar&#xff09;五、浮动窗口六、对话框 一、概述 Qt窗口是通过QMainWindow类来实现的。 QMainWindow是一个为用户提供主窗口程序的类&…

用1/10的成本为节点运营者启用零认证下载

在Sui网络上运行的验证节点和完整节点需要具有最高水平的可靠性和运行时间&#xff0c;以便提供高吞吐量及区块链的可扩展性。可靠地运行有状态应用的关键部分&#xff0c;确保可以相对轻松地进行硬件故障转移。如果磁盘故障或其他类型的故障影响到运行验证节点的机器&#xff…

最新2024年增强现实(AR)营销指南(完整版)

AR营销是新的最好的东西&#xff0c;就像元宇宙和VR营销一样。利用AR技术开展营销活动可以带来广泛的利润优势。更不用说&#xff0c;客户也喜欢AR营销&#xff01; 如果企业使用AR&#xff0c;71%的买家会更多地购物。40%的购物者准备在他们可以在AR定制的产品上花更多的钱。…

记录实现水平垂直居中的5种方法

记录块级元素实现水平垂直居中的方法&#xff0c;效果如图。 <div class"parent"><div class"child">居中元素</div> </div><style> .parent {position: relative;width: 600px;height: 300px;background-color: #679389; …

每日一练 找无重复字符的最长子串

我们来看下这个题目&#xff0c;我们要统计的是不重复的子串&#xff0c;我们可以使用“滑动窗口法”&#xff0c;其实我们很容易就能想到思路。 我们的左窗代表我们目前遍历的开始&#xff0c;即我们遍历的子串的开头&#xff0c;右窗从左窗开始进行遍历&#xff0c;每次遍历…

【Redis持久化】RDB、ROB介绍和使用

RDB、ROB介绍和使用 引言ROB介绍配置指令介绍使用指令&#xff1a;dump文件修复指令快照禁用 AOF工作流程&#xff1a;文件重写&#xff1a;三种写回策略&#xff1a; 混合使用 引言 持久化的目的&#xff0c;其实就是在Redis重启或者中途崩溃的时候能够依靠自身恢复数据&…