多线程并发为什么不安全?

看到个好文章,分享一下(转载)

链接:多线程并发为什么不安全 - undifinedException - 博客园 (cnblogs.com)

目录

一、线程安全定义

二、并发安全问题

2.1、 竞态条件

2.2、 重排序、有序性、可见性

2.3、内存模型

三、总结


​​​​​

一、线程安全定义

​ 定义:

​ 多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。

该定义由Brian Goetz在《Java Concurrency In Practice》(Java并发编程实战)中定义;被百度百科、《深入理解Java虚拟机2》引用;

二、并发安全问题

​ 大概很多人都知道一点为什么在多线程并发时会不安全,多线程同时操作对象的属性或者状态时,会因为线程之间的信息不同步,A线程读取到的状态已经过时,而A线程并不知道。所以并发安全的本质问题在于线程之间的信息不同步!

​ 分析并发不安全的现象,再一层层展示其原理。

2.1、 竞态条件

​ 定义:

​ 在并发编程中,由于不恰当的执行时序而出现不正确的结果。

​ 案例:

​ 这是一个线程不安全的方法,我们的期望是每次获取queryTimes都会将queryTimes的值+1;但是当多线程并发访问时,它的工作情况并不如我们所预想的那般;

static int queryTimes = 0;
public static int getTimes(){
    queryTimes = queryTimes +1;
    return queryTimes;
}

运行结果:Java高并发测试代码 - undifinedException - 博客园

案例图解:
 

图解说明:

  • 当线程A进入方法获取到queryTimes=17时,线程B正准备进入方法;

  • 当线程B获取到queryTimes=18时,线程A还未处理值;

  • 当线程A处理queryTimes+1 = 18后,线程B随即处理queryTimes+1 = 18;

  • 此时线程A才将处理后到结果写入queryTimes,随后B也将18写入到queryTimes;

    ​ 根据上述,我们知道当竞态条件存在时,多个线程可能同时或者几乎同时读取到某个状态(值),然后将处理后到值进行写入,此时我们可以说发生了数据的"脏读"

​ 总结:

​ 竞态条件是指多线程同时对数据进行改变,读取到脏数据或写入错数据

2.2、 重排序、有序性、可见性

2.2.1、 指令重排序

​ 定义:

​ 计算机为了性能优化会对汇编指令进行重新排序,以便充分利用硬件的处理性能。

​ 案例:

int a;
int b;
int c;

...略...
  
a = 1;       // 步骤a
b = 2;			 // 步骤b
c = a + b;   // 步骤c

​ 案例图解:

​ 案例分析

  • 虽然代码顺序是步骤a、步骤b、步骤c
  • 但是从时间上以上三种情况都有可能
  • 原因是步骤a和步骤b并没有依赖关系
  • 所以为了能快点执行,计算机会调整步骤a和步骤b的顺序
  • 因为步骤c依赖于步骤a和步骤b,所以重排序也会在a和b之后

2.2.2、 有序性

​ 定义:

​ 在Java中,单线程总是顺序执行的!

​ 当编译器和处理器重排序时,必须保证,不管怎么重排序,单线程的执行结果不能被改变

2.2.3、 可见性

​ 定义:

​ 多线程中,若线程A中进行的每一步都可以被线程B观测到,则称线程A对线程B具有可见性。

​ 线程B不仅可以看到线程A处理的结果,还能准确的知道在处理过程中,每一个状态的改变,已经状态改变的顺序;

​ Java线程的通讯是透明的,线程之间不可以直接进行信息交换,所有的通讯必须同内存共享!所以多线程是天然不可见的,就是说如果不主动干涉的话,线程之间不可见,为什么呢,因为线程虽然第一步处理步骤a,第二步处理步骤b,但是先将步骤b的结果写入主内存,后将步骤a的结果写入主内存,则对观测线程来说,首先看到的是步骤b的结果,然后才是步骤a的结果!

2.3、内存模型

​ Java线程模型由主内存和工作内存组成;

如图:

​ 说明:

  • 工作内存和主内存两部分一起组成Java线程的内存模型
  • 工作内存是属于线程的,不同线程的工作内存之间不可共享,不可通讯
  • 工作内存通过Load操作从主内存中读取数据,通过Save操作将数据写入主内存
  • 线程之间的通讯:本质上是指通过主内存的数据共享

​ 解释可见性

​ 如图,Java线程之间是不可见的,因为线程的操作都在它本身的工作内存中完成,完成后的数据再写入主内存。我们称线程之间不可见是因为线程本身没有直接通讯机制;但是线程可以通过主内存进行数据交换,也可以说线程之间可通过内存通讯;

​ 解释有序性和无序性:

​ 单线程有序,是因为单线程的数据操作本身在它私有的工作内存中进行,不管如何重排序,单线程的执行结果不可被改变,所以写入主内存的结果总是正确的。

a = 1;       // 步骤a
b = 2;			 // 步骤b
c = a + b;   // 步骤c

​ 线程在被观测时无序,因为当线程A中顺序执行 a = 1、b = 1时,并不能保证先将a的值写回主内存,完全有可能先将b的值写入主内存,这是不可预测的。所以在线程B中观察线程A的处理顺序,是非常不可靠的!

因为线程之间只能通过主内存来进行数据交换,所以线程B读到a=0,b=1时,在线程A中可能已经时a=1,b=1。只不过还没有及时到将a的值写入主内存。这样线程B可能误以为线程A先执行的是b=1;

三、总结

​ 多线程为什么不安全?现在应该有答案了!究其根本,是因为线程之间无法准确的知道互相之间的状态。那么如何使得多线程安全呢,从内存角度来讲,保证线程的工作内存之间的可见性和有序性,是多线程并发安全的基础。例如volatile关键字和synchronized关键字,我们除了从作用上了解他们,还可以从更深层的内存语义上理解,他们之所以能够一定程度的解决线程安全问题,是因为他们约束了一定的内存处理方式!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在 Python 中,可以使用 `threading` 模块来启动多个线程并发执行。以下是一个简单的示例: ```python import threading def worker(): """线程要执行的任务""" print("Thread started") # do some work here print("Thread finished") threads = [] for i in range(5): t = threading.Thread(target=worker) threads.append(t) t.start() # 等待所有线程执行完毕 for t in threads: t.join() print("All threads finished") ``` 在上面的示例中,我们定义了一个 `worker` 函数,表示线程要执行的任务。然后,我们创建了 5 个线程,并将它们添加到 `threads` 列表中。接着,我们依次启动每个线程,并等待它们执行完毕。最后,我们输出一条消息表示所有线程都已经执行完毕。 需要注意的是,在多线程编程中,需要考虑线程安全问题。通常情况下,多个线程同时访问共享的数据会导致竞态条件和死锁等问题。因此,需要采取适当的同步机制来确保线程安全。 ### 回答2: 在Java中,我们可以使用多线程实现并发执行。以下是使用多线程并发启动线程的步骤: 1. 创建一个实现Runnable接口的类。这个接口定义了要在新线程中执行的任务。 2. 实现Runnable接口的run()方法,将要在新线程中执行的代码写在这个方法里面。 3. 创建一个Thread对象,将刚刚创建的Runnable对象作为参数传递给Thread的构造方法。 4. 调用Thread对象的start()方法,启动新线程。 这样就可以创建并发执行的线程。每个线程都有自己的独立的执行路径,并且可以同时执行。以下是一个示例代码: ```java public class MyRunnable implements Runnable { public void run() { // 在这里编写要在新线程中执行的代码 System.out.println("线程开始执行"); try { Thread.sleep(1000); // 暂停1秒 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程执行结束"); } } public class Main { public static void main(String[] args) { // 创建一个Runnable对象 Runnable myRunnable = new MyRunnable(); // 创建一个Thread对象,并将Runnable对象作为参数传递给Thread的构造方法 Thread thread1 = new Thread(myRunnable); Thread thread2 = new Thread(myRunnable); // 启动新线程 thread1.start(); thread2.start(); } } ``` 运行这段代码,将创建两个线程并发执行MyRunnable类中的代码。 使用多线程并发启动线程可以提高程序的执行效率,特别是在处理多个任务或者处理需要耗费较长时间的任务时。同时需要注意线程安全的问题,如对共享资源的访问需要进行同步处理,以避免出现数据错乱或者其他异常情况。 ### 回答3: 在Java中,可以通过多种方式启动线程。其中一种常用的方式是使用多线程并发的方式启动线程。 多线程并发启动可以提高效率,加快程序执行速度。以下是通过多线程并发的方式启动线程的步骤: 1. 创建线程类:首先,需要创建一个继承Thread类的线程类,或者实现Runnable接口的线程类。这个类将定义线程的具体执行逻辑。 2. 实例化线程对象:在主线程中,创建线程对象的实例。可以使用new关键字来实例化线程对象。 3. 调用start()方法:通过调用线程对象的start()方法,来启动新的线程。start()方法会自动调用线程类中的run()方法。每次调用start()方法,都会启动一个新的线程。 4. 处理线程的并发执行:在多线程并发启动后,可以使用join()方法来等待线程执行结束。join()方法会使主线程进入等待状态,直到该线程执行完毕才会继续执行。 以下是一个简单的示例代码,演示如何使用多线程并发启动线程: ```java public class MyThread extends Thread { @Override public void run() { // 线程执行逻辑 System.out.println("线程执行中..."); } public static void main(String[] args) { // 创建线程对象 MyThread thread1 = new MyThread(); MyThread thread2 = new MyThread(); MyThread thread3 = new MyThread(); // 并发启动线程 thread1.start(); thread2.start(); thread3.start(); try { // 等待线程执行结束 thread1.join(); thread2.join(); thread3.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("所有线程执行完毕。"); } } ``` 通过以上步骤,我们可以使用多线程并发的方式启动线程。这样可以充分利用计算机的多核资源,提高程序的执行效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值