多线程的出现是为了提升多核系统的资源利用率,加速程序处理。在多核系统中,CPU增加了缓存,来平衡内存读写较慢带来的速度影响(带来了可见性的问题);同时,操作系统支持线程和进程,它们可以分时复用CPU资源,避免其他低速设备降低CPU的利用率(带来了原子性的问题);为了提高缓存命中率,编译系统支持指令重排序,在指令重排序的过程中,可能会将读数据放到写数据后。
现代CPU可能会对指令进行调度和重排序,以优化执行效率。例如:
数据写入:可能被编译器优化到一系列连续的内存写操作中,减少内存访问延迟。
数据读取:被安排在写操作之后,以便读取时能够利用缓存中的数据。
线程不安全是指程序在多线程并发执行的情况下,代码中出现执行结果不一致的情况。这种情况是往往是由多线程并发请求和写入共享数据造成的。
public void test() {
Book book = new Book();
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i=0; i<100; i++) {
executorService.execute(() -> {
book.add();
});
}
assert book.getCount() != 100;
}
class Book {
private Integer count = 0;
public void add() {
count++;
}
public Integer getCount() {
return count;
}
}
本质是应用程序没有同时满足有序性、可见性和原子性造成的。
可见性:一个线程对共享变量的修改,另一个线程可以立刻察觉到。由于高速缓存的存在,CPU在处理数据时,都是先将数据由内存加载到高速缓存中,然后处理完成后再由高速缓存写入内存。这个过程,有可能会发生线程A对共享变量的修改,线程B没有感知到,从而造成线程不安全的情况。
原子性:即程序执行过程中的一个操作或者多个操作要么全部执行且执行过程中不会被打断,要么全部不执行。这个问题是由CPU的分时复用造成的。
int i=1;
// 假设线程A、B都在执行i=i+1的操作
i = i + 1;
这里需要注意的是:i += 1
需要三条 CPU 指令
当线程A在执行到第二步后失去了CPU使用权后,线程B将整个程序执行完成了,这个时候线程A再执行最后一步的时候,整个程序的执行结果就会出错(预期是3实际是2)。
**有序性:**即程序执行的时候按照代码编写的顺序执行。由于指令重排序的存在,可能会造成程序执行时,其执行顺序与代码编写顺序不同。
public class VisibilityExample {
private boolean flag = false;
private int value = 0;
public void writer() {
value = 42; // 1
flag = true; // 2
}
public void reader() {
if (flag) { // 3
System.out.println(value); // 4
}
}
}
在这段代码中,由于writer方法中的value和flag之间没有依赖关系,因此在指令重排序后,是可能造成flag先置为true,然后才设置value=42的,这种情况下,如果存在另一个线程请求reader方法,就可能会出现flag为true,但是value还没有被赋值的情况。