多线程--深入探究多线程的重点,难点以及常考点线程安全问题

˃͈꒵˂͈꒱ write in front ꒰˃͈꒵˂͈꒱
ʕ̯•͡˔•̯᷅ʔ大家好,我是xiaoxie.希望你看完之后,有不足之处请多多谅解,让我们一起共同进步૮₍❀ᴗ͈ . ᴗ͈ აxiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客
本文由xiaoxieʕ̯•͡˔•̯᷅ʔ 原创 CSDN 如需转载还请通知˶⍤⃝˶​
个人主页:xiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客

系列专栏:xiaoxie的JAVAEE学习系列专栏——CSDN博客●'ᴗ'σσணღ
我的目标:"团团等我💪( ◡̀_◡́ ҂)" 

( ⸝⸝⸝›ᴥ‹⸝⸝⸝ )欢迎各位→点赞👍 + 收藏⭐️ + 留言📝​+关注(互三必回)!

目录

一.线程安全问题

1.为什么会有线程安全问题

2.一个经典的线程安全的例子

1.Java代码

2.输出结果

 3.说明

4.原因说明

5.画图说明

6.解决方法

二.锁

1.什么是锁

2.如何加锁

1.synchronized关键字

3.为什么锁可以解决线程安全问题

三.内存可见性问题

1.什么是内存可见性

2.举一个例子

 3.造成内存可见性问题主要原因

4.如何解决内存可见性问题

5.具体用法

6.一道关于volatile关键字的面试问题

一.线程安全问题

1.为什么会有线程安全问题

线程安全问题在多线程编程中出现的根本原因是由于并发执行所带来的不确定性以及现代计算机系统在执行多线程任务时的内在机制。以下是线程安全问题产生的主要原因:

  1. 抢占式执行

    • 在多线程环境下,操作系统采用抢占式调度策略,这意味着线程可以在任何时候被停止执行或恢复执行,而不保证按照特定顺序完成。因此,线程间的执行顺序具有不确定性,可能导致数据竞争。
  2. 共享状态

    • 当多个线程访问并修改同一块共享数据时,如果没有适当的同步控制,就可能出现数据不一致或者竞态条件。例如,两个线程同时读取一个变量然后更新它,最终结果可能并不是每个线程单独操作所期望的结果。
  3. 非原子操作

    • 许多操作在硬件层面并不是原子的,即它们可以被中断并在稍后继续执行。如果一个非原子操作在执行过程中被另一个线程打断,可能导致数据损坏。
  4. 内存可见性

    • CPU和编译器为了性能优化,可能缓存数据到本地寄存器或缓存行中,而不是立即写回到主内存。这会导致不同线程看到的数据可能是过期的,即线程间对共享变量的修改彼此不可见。
  5. 指令重排序

    • 编译器或处理器为了优化性能,可能会重新安排指令执行的顺序,只要不影响单线程环境下的程序逻辑。但在多线程环境下,这种重排序可能导致依赖于特定执行顺序的代码出错。
  6. 死锁与资源争抢

    • 当多个线程相互等待对方释放资源时,可能会陷入永久阻塞的状态,即死锁。另外,如果资源分配不当,可能会导致某些线程长期得不到所需的资源而无法执行,形成饥饿现象。

综上所述,线程安全问题主要是由于并发执行中的数据访问冲突、操作的原子性和内存模型的复杂性等因素引起的。而出现这些问题,我们的代码就会有BUG(即不满足我们的业务要求就是BUG)

2.一个经典的线程安全的例子

1.Java代码

public class Demo {
    public static int count;
    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();
        //保证线程1和线程2执行完毕
        t1.join();
        t2.join();
        System.out.println("count = " + count);
    }
}

2.输出结果

1.第一次

2.第二次

​ 

 3.说明

我们可以看到上面的代码,就是很简单的,先定义好一个静态的count变量,在线程1和线程2中各循环50000次,每次循环都加加,根据静态变量的特性,我们最后输出的count变量应该为100000,并且每次输出都一样才对,可是最后结果却每次的输出结果都不同,这就代表我们的代码出现了BUG.

4.原因说明

上述代码我们如果是在单线程的环境下,由于不存在并发访问共享资源的情况,当然是没问题的,而多线程,我们都知道,多线程在操作系统中,是抢占式调度,线程间的执行顺序具有不确定性,这便是我们每次执行代码输出结果都不同的主要原因,并且可能导致两个线程在未进行任何同步控制的情况下同时访问和修改 count 变量,进而产生竞态条件使得最终输出的 count 值低于预期的100000.

为了更通俗一点说明,博主通过画图的方式来帮助理解

5.画图说明

操作系统执行一个线程的过程,主要的就是CPU指令,我们就通过这底层的CPU指令来分析多线程问题

按照上图的CPU指令执行顺序,可以分为以下几步

1.t1从内存中的count,读取到寄存器中

 2.t2从内存中的count,读取到寄存器中

 3.t1寄存器中的count进行加加操作

  4.t2寄存器中的count进行加加操作

5.把t1寄存器的值写入到内存中

6.把t2寄存器的值写入到内存中

通过上述过程我们可以发现,正是因为,线程是随机调度的原因导致我们的CPU指令执行的顺序也是随机,从而导致了原本应该加1的操作被重复计数,最终结果小于预期的100000存在线程安全问题.

注意:这只是在循环过程中CPU执行顺序的其中一种.所以我们每次启动代码得到的count值都不相同.

6.解决方法

在上文中我们也提到了为什么会发生线程安全问题

1.线程在操作系统执行时是随机调度的,抢占式执行的

2.多个线程同时访问一个共享数据

3.线程对数据的修改是非原子操作的

4.内存可见性问题

5.指令重排序

我们如何解决线程安全问题呢,当然就是从这些原因入手,首先第一个原因,虽然它是造成线程安全问题的主要原因,但这是操作系统这个层级的问题,我们也是"有心却无力",其次第二个原因是代码问题,我们当然是可以修改代码让多个线程不要同时访问一个共享数据,但大多时候,业务代码都是比较复杂的,你修改了一下代码,属于牵一发而动全身,反而得不偿失.第四,第五的原因,这里还不涉及到(下文在提及),而第三个原因,是解决线程安全问题的最朴实的方法,使用锁来将非原子的操作,封装成一个原子操作即使用锁.

二.锁

1.什么是锁

在多线程环境中,当多个线程同时尝试访问和修改同一份数据时,如果没有妥善的协调机制,将会引发竞态条件(Race Condition)、数据不一致等问题。锁就是用来解决这类问题的一种工具。在最简单的形式下,锁是一种二元状态标志,表示资源是否可用。当一个线程获得了锁,它可以访问受保护的资源;其他尝试获取同一把锁的线程则会被阻塞(挂起),直到该锁被释放为止。这样,锁就确保了在任何给定时间内,只有一个线程能够访问临界区(Critical Section)内的资源。

总的来说:锁主要的方式就是:1.加锁 2.解锁,它的主要特性就是有"互斥性"即,一个线程加锁了之后,直到该锁被持有线程释放(解锁)其他线程不可以尝试加锁了,另一个或者是多个就会,阻塞等待.正是有这个特性,使得锁可以用来解决线程安全问题.

2.如何加锁

在Java中我们主要是使用关键字"synchronized"来进行加锁

有以下几种加锁的例子

1.在非原子操作中加锁(内置锁)

public class Demo {
    public static int count;
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();//创建一个锁对象,无论是什么类型都可以为锁对象,
        //这里的锁对象只是做一个标识的作用
        Thread t1 = new Thread(()->{
            synchronized (locker) {
                for (int i = 0; i < 50000; i++) {
                    count++;
                }
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (locker) {
                for (int i = 0; i < 50000; i++) {
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        //保证线程1和线程2执行完毕
        t1.join();
        t2.join();
        System.out.println("count = " + count);
    }
}

注意:这里的Object locker = new Object(); 创建一个锁对象,无论是什么类型都可以为锁对象,
这里的锁对象只是做一个标识的作用.最重要的是多个线程是否对同一个对象加锁这才是最重要的,如果是不同对象,那么锁就没有作用了.

2.使用synchronized修饰的方法

class Count {
    public static int count;
    Object locker = new Object();
    public static synchronized void add(){//使用synochronized修饰的方法
         count++;
    }

    public static int getCount() {
        return count;
    }
}
public class Demo10 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                Count.add();
            }
        });
        Thread t2 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                Count.add();
            }
        });
        t1.start();
        t2.start();
        //保证线程1和线程2执行完毕
        t1.join();
        t2.join();
        System.out.println("count = " + Count.getCount());
    }
}

当然还有其他的方法使用synchronized来加锁这里就不过的赘述了

1.synchronized关键字

这里再解释一下synchronized关键字是Java语言中用于处理多线程同步的关键字

1.synchronize在使用于代码块时后面跟的()括号里面的参数为锁对象

2.synchronized(){}进入 { 时就是为()里的锁对象上锁,出入}才代表着解锁

3.为什么锁可以解决线程安全问题

就像上图表示的一样,只有当 t2 进行 lock操作时,锁已经被 t1 占有了,用于锁具有互斥性此时  t2 就只能阻塞等待,直到t1解锁后,t2才能执行 load add save CPU指令操作.这样就相当于count++这个操作是串行化执行的.这里需要注意的是博主说的这里是串行化执行的,仅仅是count++这个操作,两个线程还是并发执行的.

这里只是对锁的初步介绍,后续博主会更新更多关于锁的问题,大家感兴趣可以关注一下.

三.内存可见性问题

1.什么是内存可见性

内存可见性在多线程编程中是一个至关重要的概念,它涉及到当一个线程修改了共享变量的值后,其他线程能否及时看到这个修改后的值。在一个多核或多处理器系统中,每个线程可能有自己的工作内存,而主内存是所有线程共享的。

  • 问题背景: 当线程A在自己的工作内存中修改了共享变量的值,这个更改可能不会立刻同步回主内存,同时线程B也无法感知到线程A所做的更改,除非线程B也有某种机制来刷新或重新获取主内存中该变量的最新值。这就是所谓的内存不可见性问题。

  • 后果: 内存不可见性可能导致程序的行为变得不可预测,特别是在依赖于共享变量状态进行决策的并发代码中。如果不采取措施保证内存可见性,程序可能因为不同线程看到的变量值不同而产生各种错误,例如数据不一致、程序逻辑混乱等。

2.举一个例子

public class Demo11 {
    public static int count;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (count == 0) {
                // do nothing
            }
            System.out.println("线程t1发现count值已改变,不再为0");
        });

        Thread t2 = new Thread(() -> {
            System.out.println("请输入count的值:");
            Scanner scan = new Scanner(System.in);
            count = scan.nextInt(); // 修改count的值
            scan.nextLine(); // 清除换行符
        });

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

        // 为了让t1有机会看到t2对count的修改,可以让主线程等待t2结束
        t2.join();
        System.out.println("主线程已确认t2线程结束,现在t1应能看到count的新值");
    }
}

 预期的效果:用户在输入一个非0的整数t1就会退出循环并输出("线程t1发现count值已改变,不再为0") 我们可以看一下结果

我们可以发现t1并没有退出循环,这是为什么呢? 在多线程环境下,当一个线程修改了共享变量的值,其他线程并不一定能立即看到这个修改。这就是内存可见性问题

 3.造成内存可见性问题主要原因

造成内存可见性问题,最主要的原因就是,jvm对操作进行了优化,使得t2修改了count值,但t1,无法察觉到(没有读取到),就造成内存可见性问题这里同样是从CPU指令这一层级进行分析

在由于循环体中是空的所以中主要执行以下两步操作

1.load : 将内存的count读取到CPU寄存器中 

2.cmp : 比较,条件成立就继续顺序执行,条件不成立就跳转到另一个地址中(这里不过多解释)

由于指令的执行其实是很快的,所以,短时间内执行大量的重复的load 和 cmp .

1.由于load要从内存读取数据到寄存器中,这个操作比 cmp 操作要慢很多.

2.并且因为在t2修改前,count值其实是一样的.

3.基于上述原因这个时候 jvm就直接将load这个操作,优化成直接读取之前保存在寄存器中的值(这里只是描述了一下具体的优化是JVM要遵循JMM和编译器的优化规则),使代码的效率提高,这种做法固然是好的,但是在多线程的情况下,你直接读取寄存器的值,就读取不到count被t2修改后的值,导致发生线程安全问题,代码出现了BUG.

4.为什么在循环体里内不做任何事呢,就比如打印一句话,是因为,打印是需要进行I/O操作的,比load还要浪费时间,这个时候 jvm就不一定优化load过程了,虽然还是会产生内存可见性问题,但这是小概率问题了,不易于观察.

4.如何解决内存可见性问题

为了确保内存可见性,Java提供了以下几种机制:

  1. volatile关键字:声明一个变量为volatile可以禁止JVM和CPU对这个变量的读写操作进行重排序,并且要求每次读取该变量都从主内存获取,每次写入都同步到主内存,确保了多线程间的可见性。

  2. synchronized关键字:通过synchronized同步块或方法,不仅保证了在同一时刻只有一个线程访问临界区,而且还隐含地包含了内存可见性,即在同步块或方法结束时,会将修改过的共享变量刷回主内存,同时在进入同步块或方法前会从主内存重新加载变量的值。

  3. final关键字:对于final字段,JMM保证了在构造函数完成后,final字段的值对所有线程都是可见的。

  4. java.util.concurrent.atomic原子类:提供了一系列原子操作,这些操作保证了线程间的原子性和内存可见性。

由于 使用synchronized还会涉及到加锁,以及解锁的时间消耗,这里就不过多的介绍,这里最主要介绍的是volatile关键字.禁止JVM和CPU对这个变量的读写操作进行重排序,并且要求每次读取该变量都从主内存获取,这样就可以避免内存可见性问题了,

5.具体用法

public class Demo {
    public static volatile int count;//count被volatile修饰
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (count == 0) {
                // do nothing
            }
            System.out.println("线程t1发现count值已改变,不再为0");
        });

        Thread t2 = new Thread(() -> {
            System.out.println("请输入count的值:");
            Scanner scan = new Scanner(System.in);
            count = scan.nextInt(); // 修改count的值
            scan.nextLine(); // 清除换行符
        });

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

        // 为了让t1有机会看到t2对count的修改,可以让主线程等待t2结束
        t2.join();
        System.out.println("主线程已确认t2线程结束,现在t1应能看到count的新值");
    }
}

结果如下:

这个时候就可以避免内存可见性问题.

6.一道关于volatile关键字的面试问题

问题:volatile的作用,能否保证线程安全问题

volatile关键字在Java中主要作用于变量,其主要目的和作用包括:

  1. 可见性:当一个线程修改了标记为volatile的变量时,其他线程可以立即看到这个变量的最新值,而不是从各自的工作内存(缓存)中读取旧值。这是因为volatile变量的读写操作都会与主内存进行同步,每次读取都会从主内存获取,每次写入都会立即刷新到主内存。

  2. 禁止指令重排序:Java内存模型确保了对volatile变量的操作不会与其他普通变量的读写操作发生重排序,即在多线程环境下,对volatile变量的读写具有一定的顺序约束。

然而,volatile关键字不能完全保证线程安全。它不能防止多个线程同时读写同一变量时产生的竞态条件(race conditions),特别是对于需要多个连续操作组成的原子操作(如递增操作count++),volatile关键字无法保证其原子性。

举例来说,如果你有两个线程同时对一个volatile int count进行递增操作,尽管count的更新对所有线程是可见的,但由于递增操作不是原子的,所以仍然可能发生线程安全问题。

在实际应用中,要实现线程安全,对于需要多个线程读写共享数据的场景,单纯使用volatile往往是不够的,还需要结合synchronizedjava.util.concurrent包中的原子类(如AtomicInteger)或者其他同步机制来确保原子性和线程安全性。

以上就是关于线程安全的初步介绍,感谢你的阅读

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

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

相关文章

SpringBoot登录校验(四)过滤器Filter

JWT令牌生成后&#xff0c;客户端发的请求头中会带有JWT令牌&#xff0c;服务端需要校验每个请求的令牌&#xff0c;如果在每个controller方法中添加校验模块&#xff0c;则十分复杂且冗余&#xff0c;所以引入统一拦截模块&#xff0c;将请求拦截下来并做校验&#xff0c;这块…

100道面试必会算法-18-岛屿问题(数量、周长、面积)

100道面试必会算法-18-岛屿问题&#xff08;数量、周长、面积&#xff09; 题目描述 给你一个由 1&#xff08;陆地&#xff09;和 0&#xff08;水&#xff09;组成的的二维网格&#xff0c;请你计算网格中岛屿的数量。 岛屿总是被水包围&#xff0c;并且每座岛屿只能由水平…

银行数字化转型导师坚鹏:银行数字化转型给支行带来的8大价值

银行数字化转型给支行带来的8大价值 银行数字化转型对不仅对总行、分行产生了深远影响&#xff0c;给总行、分行带来了新质生产力&#xff0c;对银行支行&#xff08;包括网点&#xff09;也会产生重要价值&#xff0c;银行数字化转型导师坚鹏从以下8个方面进行详细分析&#…

Linux多进程通信(4)——消息队列从入门到实战!

Linux多进程通信总结——进程间通信看这一篇足够啦&#xff01; 1.基本介绍 1&#xff09;消息队列的本质其实是一个内核提供的链表&#xff0c;内核基于这个链表&#xff0c;实现了一个数据结构&#xff0c;向消息队列中写数据&#xff0c;实际上是向这个数据结构中插入一个…

keil创建工程 芯源半导体CW32F003E4P7

提前下载keil 安装步骤 1、下载CW32F003固件库 芯源半导体官网下载固件库 下载好后右键解压 CW32F003_StandardPeripheralLib_V1.5\IdeSupport\MDK 进入MDK文件夹 双击WHXY.CW32F003_DFP.1.0.4.pack安装固件库 点击next然后finish安装结束 keil创建工程 点击new uVision P…

【软件工程】详细设计(一)

1. 引言 1.1 编写目的 该文档的目的是描述《学生成绩管理系统》项目的详细设计&#xff0c;其主要内容包括&#xff1a; 系统功能简介 系统详细设计简述 各个模块的实现逻辑 最小模块组件的伪代码 本文档的预期的读者是&#xff1a; 开发人员 项目管理人员 测试人员 …

插入排序---算法

1、算法概念 插入排序&#xff1a;它的工作原理是通过构建有序排序&#xff0c;对于未排序数据&#xff0c;在已排序序列中从后向前扫描&#xff0c;找到相应位置插入。 2、算法步骤 将第一待排序序列第一个元素看作一个有序序列&#xff0c;把第二个元素到最后一个元素当成是…

Exchanger 怎么用J.U.C

Exchanger简介 Exchanger通常用来解决以下类似场景的问题&#xff0c;如下&#xff1a;两个线程间需要交换数据的问题&#xff0c;在多线程编程中&#xff0c;经常会有这样的场景&#xff1a;两个线程各自持有一些数据&#xff0c;并且需要在某个点上交换这些数据&#xff0c;…

【项目实战】【Docker】【Git】【Linux】部署V2rayA项目

今天着手了一个全新领域的项目&#xff0c;从完全没有头绪到成功运行&#xff0c;记录一下具体的部署流程 github项目链接V2rayA 一开始拿到以后完全没有抓手&#xff0c;去阅读了一下他的帮助文档 写着能用docker运行&#xff0c;就去下载了一个Docker配置了一下 拉取代码到…

输入url到页面显示过程的优化

浏览器架构 线程&#xff1a;操作系统能够进行运算调度的最小单位。 进程&#xff1a;操作系统最核心的就是进程&#xff0c;他是操作系统进行资源分配和调度的基本单位。 一个进程就是一个程序的运行实例。启动一个程序的时候&#xff0c;操作系统会为该程序创建一块内存&a…

基于java+SpringBoot+Vue的学生心理咨询评估系统设计与实现

基于javaSpringBootVue的学生心理咨询评估系统设计与实现 开发语言: Java 数据库: MySQL技术: Spring Boot MyBatis工具: IDEA/Eclipse、Navicat、Maven 系统展示 后台展示 用户管理模块&#xff1a;管理员可以查看、添加、编辑和删除用户信息。 试题管理模块&#xff1a…

光伏智慧管理技术创新,提高能源利用率!

光伏电站的建设规模正在不断扩大&#xff0c;运维与管理成为了一个重要的问题。随着科技的迅速发展&#xff0c;智慧光伏将成为光伏发电系统的发展趋势。智慧光伏主要是通过传感器、通信设备和数据处理技术&#xff0c;实现对光伏电站的检测、控制和优化管理&#xff0c;从而提…

Head First Design Patterns -代理模式

什么是代理模式 代理模式为另一个对象提供替身或者占位符&#xff0c;以便控制客户对对象的访问&#xff0c;管理访问的方式有很多种。例如远程代理、虚拟代理、保护代理等。 远程代理&#xff1a;管理客户和远程对象之间的交互。 虚拟代理&#xff1a;控制访问实例化开销大的对…

利用Lora调整和部署 LLM

使用 NVIDIA TensorRT-LLM 调整和部署 LoRA LLM 大型语言模型 (LLM) 能够从大量文本中学习并为各种任务和领域生成流畅且连贯的文本&#xff0c;从而彻底改变了自然语言处理 (NLP)。 然而&#xff0c;定制LLM是一项具有挑战性的任务&#xff0c;通常需要完整的培训过程&#xf…

论文阅读:Walk These Ways: 通过行为多样性调整机器人控制以实现泛化

Walk These Ways: 通过行为多样性调整机器人控制以实现泛化 摘要&#xff1a; 通过学习得到的运动策略可以迅速适应与训练期间经历的类似环境&#xff0c;但在面对分布外测试环境失败时缺乏快速调整的机制。这就需要一个缓慢且迭代的奖励和环境重新设计周期来在新任务上达成良…

企业家见识、智慧与胸怀:超越知识、聪明与财富的核心价值​

一、引言 在商界的风云变幻中&#xff0c;企业家们不仅需要拥有丰富的知识和聪明才智&#xff0c;更需要具备远见卓识、深刻智慧和博大胸怀。正如某知名企业家所言&#xff1a;“企业家见识比知识重要&#xff0c;智慧比聪明重要&#xff0c;胸怀比财富重要。”&#xff0c;这…

OSCP靶场--Snookums

OSCP靶场–Snookums 考点(RFI信息收集数据库发现凭据bas64解码su切换用户/etc/passwd覆盖提权) 1.nmap扫描 ##┌──(root㉿kali)-[~/Desktop] └─# nmap 192.168.216.58 -sV -sC -Pn --min-rate 2500 -p- Starting Nmap 7.92 ( https://nmap.org ) at 2024-03-30 03:39 E…

误删C盘文件导致wps不可用如何解决(window 11)

一开始是为了清理C盘&#xff0c;然后第二天就发现wps不能用了&#xff0c;刚开始的时候Word&#xff0c;Excel&#xff0c;PowerPoint&#xff0c;OneNote都是空白的&#xff0c;连图标都没有了。 点击电脑固定栏左下角的开始 点击设置 点击安装的应用 找到你下载的后点击修改…

连入门都不算的Kylin相关概念畅谈!

本文图片来自于尚硅谷。 即席查询&#xff1f;即时查询&#xff1f; 作者学习过程中已经连续看到过两次即席查询了&#xff0c;不禁冒出个想法&#xff1a;是不是真的有“即席查询”的概念&#xff1f;我还以为是即时查询&#xff0c;打错了呢…… 即席查询概念 确实存在“即…