一、多核处理器下的多线程怎么工作
每个多线程有一个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++
这个操作:
- 读取(Read):首先,处理器需要从内存中读取变量
i
的当前值到寄存器中。 内存->cache->CPU寄存器 - 修改(Modify):然后,处理器将寄存器中的值增加1。 CPU寄存器 做运算
- 写回(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的使用