• 多线程原子性、一致性与有序性


    作者:逍遥Sean
    简介:一个主修Java的Web网站\游戏服务器后端开发者
    主页:https://blog.csdn.net/Ureliable
    觉得博主文章不错的话,可以三连支持一下~ 如有需要我的支持,请私信或评论留言!

    前言:

    多线程原子性、一致性和有序性是指在多线程编程中,保证数据正确性和程序执行顺序的三个重要概念。

    1. 原子性:原子操作是指不可中断的一个操作,要么全部执行成功,要么全部执行失败,中间不会被其他线程干扰。多线程环境下,如果多个线程同时更新一个共享变量,就可能出现问题。原子性的解决方案包括使用原子类、锁和同步机制等。
    2. 一致性:一致性是指对于多个线程之间共享的数据,操作后数据的状态保持一致。在多线程环境下,如果多个线程访问同一个共享变量,并发修改它,就可能出现数据不一致的情况。一致性的解决方案包括使用锁和同步机制等。
    3. 有序性:有序性是指多线程环境下,保证程序执行顺序的一种机制。在多线程环境下,程序执行顺序不是按照代码顺序执行的,可能会因为CPU调度、指令重排等原因导致执行顺序变化。有序性的解决方案包括使用volatile关键字、synchronized关键字、建立happens-before关系等。

    多线程原子性、一致性与有序性

    原子性

    多线程原子性是指一个操作在多线程环境下执行时,保证这个操作的执行是不可被中断的,即要么全部完成,要么全部不完成。即使有多个线程同时调用这个操作,也不会造成数据的混乱、错误或不一致性。这种保证是由操作系统提供的,通常使用锁或者原子操作来实现。在多线程编程中,需要特别注意多线程访问共享资源时的原子性问题,以避免出现数据竞争、死锁、饥饿和其他线程安全问题。

    一致性

    多线程一致性是指在多线程程序中,多个线程之间对共享资源的访问是有序的、正确的、可预测的,并且保证数据的正确性和完整性

    在多线程程序中,如果多个线程同时访问同一个共享资源,可能会出现访问冲突、竞争条件等问题,导致程序的错误和不一致性。为了保证多线程程序的正确性,需要采取相应的同步机制,如锁、信号量等,来保证每个线程对共享资源的访问是有序的、正确的、可预测的,并且保证数据的正确性和完整性。

    同时,要保证多线程程序的正确性,还需要避免“竞态条件”(Race Condition)的出现。竞态条件是指在多线程程序中,多个线程之间对共享资源的访问顺序会发生变化,从而导致程序出现错误和不一致性。为避免竞态条件的出现,可以采用同步机制和原子操作等措施来保证多线程程序的正确性。

    有序性

    多线程有序性指的是多线程程序的执行顺序不是固定的,可能会出现线程执行顺序与我们期望的不一致的情况。

    在多线程编程中,由于线程的调度和执行是由操作系统决定的,而操作系统会根据一定的算法来决定哪个线程先执行,哪个线程后执行。因此,多线程程序的执行顺序是不确定的,可能会出现不同的执行结果。这种不确定性就是多线程的有序性问题。

    多线程有序性问题对程序的正确性会产生影响,可能会导致程序出现异常或者不可预期的结果。为了解决这个问题,我们通常使用同步机制来保证多线程程序的正确性,比如使用互斥锁、条件变量、原子操作等方式来对共享数据进行操作,以保证多线程程序的正确性。

    volatile

    在多线程并发的情况下,CPU 的指令重排可能会导致代码执行结果与预期不符,这种情况下需要使用 volatile 关键字来保证有序性。

    volatile 的作用是禁止编译器或处理器对代码进行指令重排序优化,从而保证代码执行的有序性。也就是说,使用 volatile 声明的变量,每次读取它的值时都会从内存中读取,每次写入它的值时都会立即刷新到内存中。

    因此,使用 volatile 关键字可以保证多线程并发访问同一个变量时,能够保证修改的顺序与读取的顺序是一致的,从而避免了数据的不一致和错误的结果。

    使用volatile关键字时,编译器与运行时会对它所修饰的变量的访问进行特殊的处理,保证可见性和有序性
    volatile并不能保证原子性,如果需要保证变量的操作具有原子性,还需要配合使用synchronized或者Atomic类型

    案例一

    下面是一个简单的示例,说明了如何使用volatile解决有序性问题:

    假设我们有两个线程,一个线程负责修改一个共享变量的值,另一个线程负责读取该共享变量的值。如果我们不使用volatile,那么另一个线程可能会看到共享变量的旧值,因为写入线程的更新可能尚未被刷新到主内存中。这是因为在没有同步的情况下,编译器和处理器可以重新排序操作,以提高性能。但是,这可能导致数据不一致性和意外行为。

    下面是使用volatile修饰共享变量的示例代码,以确保写入变量的值立即刷新到主内存,并且所需的happens-before关系已经建立:

    class SharedData {
        volatile int value;
    }
    
    class WriterThread implements Runnable {
        private SharedData data;
        
        public WriterThread(SharedData data) {
            this.data = data;
        }
        
        public void run() {
            // 修改共享变量的值
            data.value = 42;
        }
    }
    
    class ReaderThread implements Runnable {
        private SharedData data;
        
        public ReaderThread(SharedData data) {
            this.data = data;
        }
        
        public void run() {
            // 读取共享变量的值
            System.out.println(data.value);
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            SharedData data = new SharedData();
            Thread writerThread = new Thread(new WriterThread(data));
            Thread readerThread = new Thread(new ReaderThread(data));
            writerThread.start();
            readerThread.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    在上面的示例中,共享变量value被声明为volatile,这意味着写入线程对它的修改会立即刷新到主内存中,而读取线程会从主内存中读取最新的值。因此,读取线程不会看到过期的值,并且对共享变量的访问已建立必要的happens-before关系,以确保正确的顺序。

    案例二

    以下是一个简单的多线程买票java案例:

    public class Ticket implements Runnable {
        private int ticketCount = 10;
    
        public void run() {
            while (ticketCount > 0) {
                try {
                    // 模拟网络延迟,增加线程安全性
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (this) { // 使用 synchronized 关键字实现线程同步
                    if (ticketCount > 0) {
                        System.out.println(Thread.currentThread().getName() + "购买了一张票,剩余票数为:" + (--ticketCount));
                    } else {
                        System.out.println(Thread.currentThread().getName() + "发现票已经卖完了");
                    }
                }
            }
        }
    
        public static void main(String[] args) {
            Ticket ticket = new Ticket();
    
            new Thread(ticket, "用户1").start();
            new Thread(ticket, "用户2").start();
            new Thread(ticket, "用户3").start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    在这个案例中,我们创建了一个 Ticket 类实现了 Runnable 接口,重写了 run() 方法。在 run() 方法中,我们使用 synchronized 关键字来保证同一时刻只有一个线程能够进入临界区(购买票数减 1 的代码块)。这样就保证了不会出现多个线程同时购买同一张票的情况。

    main() 方法中,我们创建了三个线程启动来购买票,每个线程的名称分别为 “用户1”、“用户2” 和 “用户3”。运行这个程序,我们会看到这三个线程分别购买了 4、3 和 2 张票,最终票被卖完了。

    案例三

    下面是一个多线程买票的volatile案例:

    public class BuyTicket implements Runnable {
        private volatile int tickets = 10; // 票数
        private String name; // 线程名字
    
        public BuyTicket(String name) {
            this.name = name;
        }
    
        public void run() {
            while (tickets > 0) {
                synchronized (this) {
                    if (tickets > 0) {
                        System.out.println(name + "买到了一张票,剩余" + (--tickets) + "张票。");
                    }
                }
                try {
                    Thread.sleep(100); // 暂停100ms
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    在上面的案例中,tickets变量是被volatile修饰的,这表示每个线程都可以读取和修改它的值。同时,用synchronized关键字来保证线程安全,避免出现多个线程同时读取到同一份票数,产生数据竞争的情况。同时,在每个线程操作之后,都暂停100ms,以让其他线程有机会读取和修改票数,避免出现死循环的情况。

  • 相关阅读:
    743.网络延迟时间 | 1514.概率最大的路径(Dijkstra算法)
    浅学一下二叉树链式存储结构的遍历
    three物体围绕一周呈球形排列
    Java 文件NIO 中,为什么NIO比IO快?
    【Java分享客栈】SpringBoot整合WebSocket+Stomp搭建群聊项目
    MySQL高级SQL语句
    Chrome 108版(64-bit 108.0.5359.125)网盘下载
    计算机毕业设计 | springboot+vue会议室管理系统(附源码)
    九、Linux用户管理
    【RcoketMQ】RcoketMQ 5.0新特性(二)- Pop消费模式
  • 原文地址:https://blog.csdn.net/Ureliable/article/details/133999265