从多核计算机的缓存一致性谈起,深入理解volatile和syncronized

一、多核处理器下的多线程怎么工作

多核处理器下多线程工作细节
每个多线程有一个CPU核心/处理器 有相应的缓存
缓存保存了需要执行函数的局部变量和全局变量/共享变量副本
代码举例

// 包含线程方法的类
class MyThreads {
    private int i = 0;    //小范围的全局变量,每个线程都可以使用它,每个线程都有它的副本在cache中

    public void th1() {
        while (i == 0) {
            // 空转,等待i的值被更改
        }
        System.out.println(Thread.currentThread().getName() + "结束");
    }

    public void th2() {
        try {
            Thread.sleep(2000); // 线程休眠2秒
            i = 1; // 改变i的值,使得th1线程结束循环
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

线程1一直空转,没有打印System.out.println(Thread.currentThread().getName() + “结束”);
我们可以看到虽然线程Thread2已经把i 修改为1了,但是线程Thread1没有读取到i修改后的值,线程一直在运行

探讨实例变量i的存放

i=0为共享变量 内存保留一份
线程1 的 cache缓存保留一份 i=0
线程2 的 cache缓存保留一份 i=0
总共三份
th2函数改变的是 线程B的cache缓存中的 i =1
并没有同步到 内存 以及线程A的cache缓存

请添加图片描述
若加上volatile int i
volatile实现了缓存一致性(MESI)
当B的CPU改变了 共享变量 i 的值 B 中cache , 变量i发生改变并把内存中的i也改变。 内存和线程b的cpu同时改变。
通知A的cache 变量i失效 若使用变量 i 需要重新从内存读取
本质上 volatile 修饰 的变量,内存中和 改变变量值的线程 所在的cache同时改变
其他线程的cache中的变量立刻失效,若使用需重新从内存读取

volatile本质上 间接 确保了 三份变量i 的内容保持一致也就是 内存和所有缓存中的变量保持一致

这就是volatile 可见性的作用 但并不保证原子性
第二部分我们探讨 volatile不保证原子性和syncronized保证原子性

**下面给出上述程序的具体调用过程**
//线程创建基本用静态代理
// 第一个线程类
class Thread1 extends Thread {
    private MyThreads myThreads;
    public String name;

    public Thread1(MyThreads myThreads,string name) {
        this.myThreads = myThreads;
         this.name =name;
    }

    @Override
    public void run() {
        myThreads.th1();
    }
}

// 第二个线程类
class Thread2 extends Thread {
   private MyThreads myThreads;
    public String name;

    public Thread2(MyThreads myThreads,string name) {
        this.myThreads = myThreads;
         this.name =name;
    }

    @Override
    public void run() {
        myThreads.th2();
    }
}


// 主类,包含main方法
public class MainThreadExample {
    public static void main(String[] args) {
        // 创建MyThreads类的实例
        MyThreads myThreads = new MyThreads();

        // 创建并启动第一个线程
        Thread1 thread1 = new Thread1(myThreads,"a");
        thread1.start();

        // 创建并启动第二个线程
        Thread2 thread2 = new Thread2(myThreads,"b");
        thread2.start();
    }
}

二、volatile和syncronized的区别

1.volatile不保证原子性示例

public class VolatileAtomicityExample {

    private static volatile int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread addThread = new Thread(() -> {
           
                count++;
            
        });

        Thread subtractThread = new Thread(() -> {
           
                count--;
            
        });

        addThread.start();
        subtractThread.start();

        addThread.join();
        subtractThread.join();

        System.out.println("Final count is: " + count);
    }
}

count已经被volatile修饰 ,保证了可见性,但不能保证并行代码发生错误为什么呢?
volatile保证的是 线程1修改变量,线程2读取变量保证变量是最新的
在这里插入图片描述

让我们从计算机硬件的角度来分析 i++ 这个操作:

  1. 读取(Read):首先,处理器需要从内存中读取变量 i 的当前值到寄存器中。 内存->cache->CPU寄存器
  2. 修改(Modify):然后,处理器将寄存器中的值增加1。 CPU寄存器 做运算
  3. 写回(Write Back):最后,处理器将修改后的值写回到内存中的 i。 CPU寄存器->cache->内存

给出一种 两个线程的具体执行过程 表明出现错误的可能:
如果线程1在i++第一步 读取 执行完后 跳转到线程2 做运算i-- 再跳转回线程1 执行i++的后两步 修改和写回 发生意外
线程1执行i++的第一步读取i
线程1:cache i=0;CPU寄存器 i=0
线程2:cache i=0 CPU寄存器 i=0
内存 i=0
接着线程2做减法
线程1:cache i=0;(volatile的作用下 失效,若使用需重新向内存读取)CPU寄存器 i=0
线程2:cache i=-1 CPU寄存器 i=-1
内存 i=-1
接着跳转到线程1做加法 执行i++的后两步 但不需要从内存读取i的值 因为 CPU寄存器已经有了
线程1:cache i=1;CPU寄存器 i=1
线程2:cache i=-1(volatile的作用下 失效,若使用需重新向内存读取) CPU寄存器 i=-1
内存 i=1
这时发生错误 正常情况加法 减法后 变量应该i=0 但这时i=1

本质上volatile 不保证 cpu 寄存器(register)中的共享变量更新导致错误

2.syncronized保证原子性且保证可见性 加强版本的volatile

版本一 使用syncronized 或者 使用volatile 和 AtomicInteger类型保证原子性
syncronized 在进入 函数或模块前 会刷新 内存 和cache 所有 共享变量的值 确保 内存和缓存一致
函数结束后 也会刷新内存和cache的值 确保 结束后其他线程使用 共享变量获得最新的值
加锁确保整个运算过程原子性

public class SynchronizedAtomicityExample {

    private static int count = 0;

    // 同步方法,确保每次只有一个线程能够进入这个方法
    public static synchronized void increment() {
        count++;
    }

    // 同步方法,确保每次只有一个线程能够进入这个方法
    public static synchronized void decrement() {
        count--;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread addThread = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                increment(); // 调用同步方法
            }
        });

        Thread subtractThread = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                decrement(); // 调用同步方法
            }
        });

        addThread.start();
        subtractThread.start();

        addThread.join();
        subtractThread.join();

        // 由于我们使用的是同步方法,所以这里不需要同步块来读取count的值
        System.out.println("Final count is: " + count);
    }
}

总结

本文仅仅简单介绍了syncronized和volatile的使用

  • 21
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值