Java-并发编程之可见性与原子性

本文深入探讨Java并发编程中的可见性和原子性概念,通过实例解释为何多线程环境下变量值更新可能出现问题,并介绍如何利用volatile、Atomic类及synchronized关键字确保数据的正确性和程序的稳定性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java-并发编程之可见性与原子性

一、并发编程-什么是可见性?

要了解Java并发编程内存可见性之前,先来简单了解一下JMM即Java内存模型(Java memory model)。

1.1-Java并发共享内存模型

 (1)Java所有变量都存储在主内存中;
 (2)每个线程都有自己独立的工作内存,里面保存该线程的使用到的变量副本(该副本就是主内存中该变量的一份拷贝)

1.2-CPU的缓存机制

我们知道Java的内存机制之后再来看,cpu由于处理速度一般会比主存(也就是主板上插着的内存条)快很多,为了避免cpu一直快车等慢车的情况发生。所以cpu特地设置了缓存位置被称为local cache。一般分为三级,分别为L1-L3,可以在windows的任务管理器中查看到。一般L1缓存是cpu单个核心独占的缓存,L2缓存一般是2个核心公用一个,L3缓存一般是四个核心或者八个核心共用一个(这个具体要看cpu的设计方式);并且L1-L3缓存的速度依次降低。

在这里插入图片描述
在这里插入图片描述

Jvm内存模型中,不同线程拥有各自的私有工作内存(栈区、本地方法栈、程序计数器),当线程需要读取或修改某个变量时,不能直接去操作主内存中的变量,而是需要将这个变量读取到线程的工作内存的变量副本中,当该线程修改其变量副本的值后,其它线程并不能立刻读取到新值,需要将修改后的值刷新到主内存中,其它线程才能从主内存读取到修改后的值。

1.3-举例说明

  1. 首先申明一个变量a,当单一线程时,我对变量进行修改之后在进行输出,可以正确输出修改之后的变量值false。这种下面一行代码对之前代码的可见性时显而易见的。

  2. 此时我们再添加一个线程,尝试在子线程中获取变量值的情况。

代码

public class Concurrent {
    static boolean a = true;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while(a){}//因为线程启动开销大,时间长
        }).start();
        Thread.sleep(1000) ;//防止线程在运行到while(a)时,a已经被赋值false。
        a = false;
        System.out.println(a);
    }
}

正常分析来说首先变量a为true,启动线程之后由于等待1s之后a才被赋为false。那么子线程应该先进入while循环,之后由于a被赋值不满足循环条件而退出。但是执行结果却如下所示:

在这里插入图片描述

虽然打印输出了false,但是此时子线程B还没有结束,说明线程B获取的还是false。

原因分析:此时就引出了今天的话题线程之间的不可见性–>及线程A修改的变量值,线程B不能马上获取到最新值导致线程B一致死在循环中出不来。

那为什么这么久还不能更新呢?因为是空语句导致,线程没有机会去主存中加载新的值,只要修改线程循环体,哪怕只是sleep 1ms,都可以使线程B有喘息时间去获取最新值。

解决1:

public class Concurrent {
    static boolean a = true;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while(a){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        Thread.sleep(1000) ;
        a = false;
        System.out.println(a);
    }
}

在这里插入图片描述

1.4 -解决可见性

面对问题线程A写变量,线程B读变量场景

添加volatile关键字,使变量的值在主存local cache之间强制刷新

volatile的实现原理

  1. 当一个线程修改了共享变量,CPU的嗅探机制会发现副本与主内存数据的不一致,通过汇编lock前缀指令,锁定共享变量主内存区域,并将新值写回到主内存;

  2. 写回内存的操作会使其他CPU中缓存了该数据的地址失效。(MESI协议,即缓存一致性协议)

代码:

public class Concurrent {
    static volatile boolean a = true;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while(a){
//                try {
//                    Thread.sleep(1);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
            }
        }).start();
        Thread.sleep(1000) ;
        a = false;
        System.out.println(a);
    }
}

执行结果:

在这里插入图片描述

二、并发编程-什么是原子性?

2.1-问题引入

代码

public class Concurrent {
    static volatile int a = 0;
    public static void main(String[] args) throws InterruptedException {
        for(int i =0;i<100000;i++){
            new Thread(()->{
                a++;
            }).start();
            new Thread(()->{
                a++;
            }).start();
        }
        System.out.println(a);
    }
}

这里开辟了两条线程,分别每个线程对原始数字a进行++操作一万次.

执行结果:

在这里插入图片描述

理想的结果因该是每个线程++1万次,最终输出结果为20000。但是实际的结果确实输出的数值总是小于等于两万次,并且这里的变量a已经通过volatile修饰强制刷新了,那每次结果可能不尽相同这是什么原因呢?

2.2-问题分析

自增操作a++过程分析:

第一步:需要去主存中取到当前变量的值;
第二步:将当前获取的变量值进行自增;
第三步:将更改过的变量值重新刷写回主内存。

看似简单的一步操作其实是包含这三个步骤的,那么上面的问题是怎么发生的?

实质上由于多线程两个线程执行之间独立进行,并没有明显的执行先后顺寻。那么这两个线程在执行时候,是存在同时获取到变量的值并进行++操作的可能的,按上结果分析就是两万次中刚好有9次卡在了,两个线程同时获取了变量值并进行++操作,导致本该++两万次由于同时操作而变成了19991次。这里就算是volatile同样不能保证准确,因为volatile只能保证一个写一个读,读的能获取到最新值,而这里是既有读操作又有基于读操作的写操作。

**如何解决:**将读操作和写操作合为一步,要么同时发生,要么同时不发生。

2.3- 原子性与原子类

原子性就是计算机中的一个不能再分割的动作,要么做完、要么不做,中间不会被打断。

==面对问题:==多线程之间既有读操作也有基于读操作的写操作

---->使用java中的Atomic类实现可见性。

import java.util.concurrent.atomic.AtomicInteger;

public class Concurrent {
    static AtomicInteger a = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        for(int i =0;i<100000;i++){
            new Thread(()->{
                a.getAndAdd(1);
            }).start();
            new Thread(()->{
                a.getAndAdd(1);
            }).start();
        }
        Thread.sleep(1000);
        System.out.println(a);
    }
}

··· 保证原子性的前提条件是一定要保证可见性的,那这里为什么AtomicInteger为什么不使用volatile关键字修饰呢?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0IylvH7M-1624955866007)(Java-并发编程之可见性与原子性/image-20210629155028400.png)]

进入内部我们可以看到,Atomic在实现的时候,value值就已经使用volatile进行修饰了。

这里顺带提一点:

Volatile不具有传递性,用volatile修饰的对象内部属性不具有可见性

反之用volatile修饰的内部属性也不能保证所在对象的可见在性

2.4- Synchronized关键字

也可以使用sync关键字来保证原子性的实现。

import java.util.concurrent.atomic.AtomicInteger;

public class Concurrent {
    static AtomicInteger a = new AtomicInteger(0);
    static int b = 0;
    public static void main(String[] args) throws InterruptedException {
        for(int i =0;i<100000;i++){
            new Thread(()->{
//                a.getAndAdd(1);
                synchronized (Concurrent.class){
                    b++;
                }
            }).start();
            new Thread(()->{
//                a.getAndAdd(1);
                synchronized (Concurrent.class){
                    b++;
                }
            }).start();
        }
        Thread.sleep(1000);
        System.out.println(b);
    }
}

执行结果:

在这里插入图片描述

这里使用了同步代码块,同一时间只有一个线程可以进入代码段,使用sync强制保证原子性。

三、总结

  1. **量级:**volatile<Atomic<Synchronized;

  2. volatile可以保证可见性一个线程写另一个线程读,保证读取最新值;

  3. Atomic可以保证原子性(包含可见性),既有读操作又有写操作时;

  4. Synchronized可以保证整个代码块中的操作都是原子性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值