首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >实例解析JAVA并发编程的三大特性:可见性、原子性、有序性

实例解析JAVA并发编程的三大特性:可见性、原子性、有序性

原创
作者头像
小草飞上天
发布2025-01-31 21:27:08
发布2025-01-31 21:27:08
4791
举报
文章被收录于专栏:java学习java学习

前言

由于现在软硬件设施的快速升级,多核处理器的普及和分布式系统的广泛应用,使得并发编程已成为构建高性能、高响应性应用程序的关键技术。

并发编程核心就是多线程并发执行多个任务,充分利用多核处理器优势,以提高软件程序的性能,随着多核处理器的这种优势性能提升的同时,也带来了线程安全数据一致性等问题。

在多线程并发中,如何确保数据的共享、竞争、死锁、安全这些程序问题?我们就不得不说一说多线程并发编程中的三大特性了。

并发编程的三大特性

说到线程安全那我们就不得不了解一下并发编程的三大特性:可见性、原子性、有序性。

只有深入了解了三大特性,才能明白在线程安全时需要注意哪些方面。

可见性

可见性是说一个线程对共享变量的修改能够被其他线程及时看到

比如买票软件,共100张票,A线程来抢走了一张,那么B线程看到的必须就是99张。

我们看一个案例,当线程1启动后,进入循环。当线程2启动后,赋值flag=false,此时线程1中的循环一直不结束。说明flag在线程2中虽然修改了值,但这个修改对线程1是不可见的。

代码语言:txt
复制
public class T5 {

    public static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (flag) {

            }
            System.out.println("线程1结束");
        });

        t1.start();

        Thread.sleep(1000);

        Thread t2 = new Thread(() -> {
           flag = false;
            System.out.println("线程2设置flag为false");
        });

        t2.start();
    }

}

关键字: volatile 的声明代表了当前变量对于线程间是可见的、有序的。

volatile是jvm提供的一种轻量级的同步机制,它可以确定一个共享对象的可见性、有序性,但不能支持原子性。也就是说volatile实际上是支持了三大特性的两种特性。

下面的案例,当线程1启动后,进入循环。当线程2启动后,赋值flag=false,此时线程1中的循环结束。说明flag是线程可见的。

代码语言:javascript
复制
public class T5 {

    //通过volatile声明变量,确保可见性
    public static volatile boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (flag) {

            }
            System.out.println("线程1结束");
        });

        t1.start();

        Thread.sleep(1000);

        Thread t2 = new Thread(() -> {
           flag = false;
            System.out.println("线程2设置flag为false");
        });

        t2.start();
    }

}

打印结果为

原子性

原子性是说对于一个操作,在整个的执行过程中不能被中断,要么完全执行,要么完全不执行。

我们实现一个如果不保证原子性可能存在的问题:首先创建5个线程,每个线程都都执行add()操作。正常情况下我们打印total的时候,值应该是 5000.但实际的结果大家可以看下方的结果图,结果为:3401 。当然,这个3401不是固定的,每一次可能都不一样。

从这个案例中我们也看出来了。total因为volatile的修饰,成为了一个多线程间可见的变量了,但它不是原子性的。

代码语言:txt
复制
public class T6 {
    
    public static volatile Integer total = 0;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            new Thread(()->add() ).start();
        }

        Thread.sleep(1000);

        System.out.println(total);

    }

    public static void add(){
        for (int i = 0; i < 1000; i++) {
            total++;
        }
    }
}

结果

我们重点分析一下total++;这里,实际上的结果是因为total++ 并非是原子性导致的结果错误。

有序性

有序性是说代码在运行时没有按照开发者编写的顺序执行,而是按照编译器优化后的编译结果执行。

我们在写代码的时候,深信代码就是按我所写的顺序,从上往下执行的。比如下面的代码:

代码语言:txt
复制
public static void main(String[] args) {
    int a = 1;
    a++;
    System.out.print(a);
}

上面的代码,打印a的值,肯定就是2 ,这点毋庸置疑,这样可以说明我们的代码确实是从上往下执行的,但真的所有的代码都如此吗?

我们来看下面的这一段代码。

对于单例模式下的instance实例来说,当instance为null时,我们需要实例化。

一个类进行实例化,它需要经过以下的流程,首先jvm先将class文件进行加载(loading);然后链接(linking),链接阶段分为三个步骤,准备(preparation),初始化类中的静态变量赋默认值(static),最后是类的初始化过程(initializing)。

下面的代码正常情况下执行应该为:分配内存空间 - 初始化对象 - 将instance指向分配的内存地址。

但是对于单实例来说,如果这样执行会有问题吗?

分配内存空间 - 将instance指向分配的内存地址 - 初始化对象 。

答案是否定的,对于单实例来说,instance按上面先分配空间,然后直接指向空间,在初始化,不会有任何问题。

所以编译器会在编译的时候,选择这样优化代码。但如果并发的情况下,就会导致线程切换后,发现instance已经指向了空间,已经有数据了,所以不为null,直接返回对象。实际上这个对象还没有初始化。

代码语言:txt
复制
if(instance == null){
    instance = new Singleton();
}

指令重排

指令重排是指应用在程序执行的过程中,编译器或处理器为了优化性能,可能会对代码的执行顺序进行调整,这种调整在单线程环境下是不会对程序的最终结果有所变动的。然而,当我们处于在多线程环境下,就可能会导致一些问题的产生。

总结

熟悉并了解并发编程的三大特性,对于开发者来说是非常有必要的。只有熟悉了解其特性,我们才能够在并发的场景中,准确的发现并预处理相关的问题。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 并发编程的三大特性
  • 可见性
  • 原子性
  • 有序性
    • 指令重排
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档