先从Java内存模型说起:
Java内存模型是什么?
引用大师的一句话:“The Java Memory Model describes what behaviors are legal in multithreaded code, and how threads may interact through memory.”
翻译过来就是:Java内存模型描述了在多线程代码中哪些行为是合法的,以及线程如何通过内存进行交互。
所以其他涉及到多线程的编程语言都会涉及到内存模型,如C/C++等,并不是Java所特有的。
那为什么会出现内存模型呢?
归结到底是因为CPU的频率无法再提高了,计算机开始面向多核化发展。为了充分利用多核带来的性能提升,便引入的并发编程,并发编程的难度是比较大的。
为了弥补cpu与缓存直接的速度落差,提供了多级高速缓存,使缓存更加接近cpu,处理速度更快。与此同时,就会出现缓存不一致问题,在多线程的状况下,每个cpu的计算结果会暂存在高速缓存中,而cpu1的缓存对于cpu2来说是不可见的,对于多个线程处理同一个共享数据时就会产生问题。如何解决?这里的问题根源就是可见性!
另外,cpu是一种很宝贵的资源,其内部运行采用了“流水线”的思想,比如一条指令的执行包含“取指、译码、执行、访问存储器、写回”等步骤,当一条指令进行译码的同时,cpu又可以继续取指操作。为了能更好地保证流水线的“完美”(尽量让cpu的每个步骤不闲着),在不影响单线程执行结果前提下,cpu会适当移动代码执行顺序(指令重排序),这样做cpu它觉得很完美,能够提高自身的执行效率,它不会管其他cpu怎么这么样。可是在多cpu协作的情况下,有时这是不允许的。如:
Class Reordering {
int x = 0, y = 0;
public void writer() {
x = 1;
y = 2;
}
public void reader() {
int r1 = y;
int r2 = x;
}
}
假入有两个线程,一个执行writer,一个执行reader,当reader读取到r1==2时,常理x读到的应该是1(因为x=1在y=2前面)。但实际是不一定的,cpu可能对xy的赋值语句调换了顺序,导致x可能读到0。这个的问题根源就是有序性!
针对这两个问题,让我这种码农来处理显然是不现实的,所以大师们在计算机指令的层面已经为我们实现好了,并提供相应的语法,并发类供我们使用。解决这些问题的过程中,大师们设定了一些规则,避免了上述错误情况的发生,这就是JAVA内存模型。
参考:http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#finalWrong
Java内存模型中有哪些规则?
其中最重要的就是happens-before原则,我认为这个原则表达的是“哪些情况下后面的代码对前面的代码是可见的”,接下来让我们看看有哪些情况:
引入大师的说法:
- Each action in a thread happens before every action in that thread that comes later in the program's order.
- An unlock on a monitor happens before every subsequent lock on that same monitor.
- A write to a volatile field happens before every subsequent read of that same volatile.
- A call to start() on a thread happens before any actions in the started thread.
- All actions in a thread happen before any other thread successfully returns from a join() on that thread.
翻译一下:
1、单线程中,前面的代码应该happens before于后面的代码。(这个很好理解,先来后到)
2、对同一个监视器(锁)的解锁应该happens before于后面的加锁。(一个监视器只能同时被一个线程持有,前一个线程解锁,后面的线程才能加锁,这也是synchronized遵守的规则之一)
3、volatile字段的写入应该happens before于后面对同一个volatile字段的读取。
4、主线程中启动子线程,子线程能看到启动前主线程中的所有操作。
5、主线程中启动子线程,然后子线程调用join方法,主线程等待子线程执行结束,执行结束返回后,主线程对看到子线程的所有操作。
另外还提供了synchronized、volatile、final三个关键字来解决可见性、有序性问题。
例子1:
单核cpu仍然存在线程安全问题,因为如果操作不是原子操作,你无法控制cpu在什么时机切换线程,我采用了阿里云上的一台单核服务器做实验,以下是实验代码:
public class OneCpuCoreTest implements Runnable {
private static int count;
@Override
public void run() {
int idx = 0;
while (idx++ < 100000) {
count += 1;
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new OneCpuCoreTest());
Thread thread2 = new Thread(new OneCpuCoreTest());
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count);
}
}
我们知道,自增操作是不具备原子性的,它包含取数据、+1、数据写回操作,所以这里的count应该是小于200000,count是线程不安全的。
PC机上(多核):
而在阿里云(单核cpu)上:
看似在单核cpu上是没有线程安全问题。但错了,这里是因为在一个cpu时间片内执行完了,所以不明显,当把循环次数调大。
结果就不一样了。
所以单核cpu上多线程仍然会存在线程安全问题,因为单核cpu仍然存在线程切换,在执行非原子操作的时候,仍然存在线程问题。