Java内存模型
Java内存模型,即:JVM。当程序执行并行操作时,如果数据的访问和操作不加以控制,那么必然对程序的正确性造成破坏。因此,我们需要在深入了解并行机制的前提下,再定义一种规则,来保证多个线程间可以有效地、正确地协同工作。而JMM就是为此而生的.
> 什么是并发和并行?
并行和并发都是多任务处理的概念,但它们的含义不同。
并行是指两个或多个任务在同一时刻执行,即在同一时刻有多个任务在同时进行。在计算机领域,多核 CPU 可以实现并行处理,即多个 CPU 内核同时执行不同的任务。在并行处理中,任务之间相互独立,不需要等待其他任务的完成。
并发是指两个或多个任务在同一时间段内执行,即在同一时间段内有多个任务在交替进行。在计算机领域,单核 CPU 可以通过轮流执行各个任务来实现并发处理。在并发处理中,任务之间可能会相互影响,需要考虑任务的顺序和优先级,也需要考虑任务之间的同步和通信问题。
简单来说,如果是多个任务同时执行,就是并行;如果是多个任务交替执行,就是并发。并行处理通常需要多核 CPU 来支持,可以提高处理速度;而并发处理可以在单核 CPU 上实现,但需要考虑任务之间的同步和通信问题。
JVM的关键技术点都围绕着多线程的原子性、可见性和有序性来创建的。
1.原子性(Atomicity)
定义:是指一个操作是不可中断的。即使是多线程一起执行的时候,一个操作一旦开始,就不会被其他线程锁干扰。
在 Java 中,我们可以使用 synchronized 关键字或者 java.util.concurrent.atomic 包中的原子类来保证操作的原子性。
在 Java 中,读取或写入一个变量(除了 long 和 double 类型)都是原子的。比如,以下的操作都是原子的:
(注:32 位 JVM 对 long、double 赋值操作为非原子操作,64 位 JVM 对 long、double 赋值操作为原子操作,不同 JVM 有不同实现,大致符合上述原则)
int i = 0;
i++;
i = i + 1;
但是,虽然 i++ 是一个原子操作,但在多线程环境下,如果没有合适的同步措施,它仍然可能导致线程安全问题。例如:
public class Counter {
private int count = 0;
public void incrrment() {
count++; //这里是非线程安全的
}
public int getCount() {
return count;
}
}
在这个例子中,increment 方法是非线程安全的。因为 count++ 这个操作虽然是原子的,但它包含了三个步骤:读取 count 的值,将值增加 1,然后将新的值写回 count。在这三个步骤中,如果有线程切换的话,就可能导致数据的不一致。
在 Java 内存模型中,对基本数据类型的变量(除了 long 和 double 比较特殊)的读取和写入操作都被保证是原子的。即,在执行这些操作时,不会被线程调度机制中断。换句话说,它们是不可分割的,要么完全执行,要么完全不执行。
然而,即使一个操作是原子性的,如果它不是线程安全的,那么在并发环境中,仍然可能存在问题。参考上述 Counter 示例。
increment() 方法中的 count++ 是一个原子操作,但它不是线程安全的。因为 count++ 是由获取 count,增加其值,并写回新值这三个步骤组成的。在这三个步骤之间,如果发生线程切换,可能导致数据的不一致性。
为了解决这个问题,Java 提供了 synchronized 和 java.util.concurrent.atomic 包中的类如 AtomicInteger,来实现更复杂操作的原子性。
如何使用 synchronized、Lock、Atomic 类来保证原子性
synchronized关键字,
确保同一时刻最多只有一个线程执行该代码段,从而保证了代码的原子性。
public class Synchronized {
private int sync = 0;
synchronized void increment() {
sync++;
}
synchronized int getSync(){
return sync;
}
}
在上面的例子中,使用synchronized关键字修饰了increment()和getSync()方法,这就说明,当一个线程进入increment()方法以后,其他在试图进入increment()或getSync()方法的线程就会被堵塞,直到第一个进入的线程退出这两个方法之一。
Lock接口
Java 中的 java.util.concurrent.locks.Lock 是一个接口,它定义了控制多线程访问共享资源的方式。它与 synchronized 不同的地方在于 Lock 提供了更多的灵活性和更好的性能。
public class LockDemo {
private int c = 0;
private Lock lock = new ReentrantLock();
void increment() {
lock.lock();
try {
c++;
}finally {
lock.unlock();
}
}
int getC() {
return c;
}
}
在以上示例中,我们使用了 ReentrantLock,它实现了 Lock 接口。在调用 increment() 方法时,我们先获取锁,然后尝试执行 count++,不论 count++ 是否成功,我们最后都会释放锁。
Atomic类
public class AtomicDemo {
private AtomicInteger c = new AtomicInteger(0);
void increment(){
c.incrementAndGet();
}
int getc(){
return c.get();
}
}
在以上示例中,我们使用了 AtomicInteger 类来实现计数器。其中 incrementAndGet() 方法会以原子方式将当前值加 1,并返回更新后的值。
2.可见性(Visibiliity)
定义:是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改
为了提高处理速度,每个线程都会在 CPU 中维护一份共享变量的本地缓存,而线程对共享变量的所有操作都会在自己的本地缓存中进行。如果线程 A 更改了一个共享变量,线程 B 有可能看不到线程 A 的修改,这就是所谓的可见性问题。
public class VisibilityDemo {
private boolean flag = true;
public void run(){
while (flag){
//do something
}
}
public void stop(){
flag = false;
}
}
在上述代码中,如果 run 和 stop 分别在两个不同的线程中调用,那么理论上 stop 能够使 run 方法的循环结束。然而,由于可见性问题,run 方法中看到的 flag 可能仍然是 true,因此循环可能永远不会结束。
解决这个问题最常用的方法是使用volatile关键字
private volatile boolean flag = true;
在这段代码中,我们将 flag 声明为 volatile。这告诉 JVM,每次在使用 flag 之前,都需要从主内存中读取它的值,而且每次更改 flag 的值后,都需要立即写回主内存。这样,就能保证所有线程看到的 flag 值是一致的。
除了 volatile 关键字,我们还可以使用 synchronized 关键字来保证共享变量的可见性。
synchronized 是 Java 提供的一种内置的同步机制。当一个线程进入一个 synchronized 方法或者代码块时,它会获取一个锁。当它退出 synchronized 方法或者代码块时,或者线程因为 wait() 方法等待,或者线程死亡,它都会释放这个锁。
这个锁不仅确保了同一时刻只有一个线程能执行 synchronized 代码块中的内容,而且还能保证 synchronized 代码块中的所有变量都同步到主内存,以及每次进入 synchronized 代码块时,都从主内存中拉取最新的变量值。
public class SynchronizedDemo {
private int count = 0;
public synchronized void increment(){
count++;
}
public synchronized int getCount(){
return count;
}
}
在这个示例中,increment() 和 getCount() 都是 synchronized 方法,这意味着每次只有一个线程能调用这些方法,这保证了 count 的可见性和原子性。
3.有序性(Ordering)
因为指令流水线的存在,CPU才能真正高效的执行。但是,流水线总是害怕被中断的。流水线满载时,性能确实相当不错,但是一旦中断,所有的硬件设备都会进入一个停顿期,再次满载又需要几个周期,因此,性能损失会比较大。所以,我们必须要想办法尽量不让流水线中断
public class Example {
private int a = 0;
private boolean flag = false;
public void writer(){
a = 1; //1
flag = true; //2
}
public void reader(){
if (flag){ //3
int i = a * a;//4
}
}
}
在这个例子中,我们期望的执行顺序是 writer 方法先执行(1,2),然后 reader 方法再执行(3,4)。但是,由于指令重排,writer 方法中设置 flag = true 的语句(2)可能会在设置 a = 1 的语句(1)之前执行。这样,当 reader 方法执行到检查 flag 的语句(3)时,可能会发现 flag 已经是 true 了,然后它去读取 a 的值,发现 a 还是 0,这就出现了我们不期望的结果
可以使用 volatile、synchronized 关键字和 Lock 接口来控制代码的执行顺序,从而避免由于指令重排导致的问题
synchronized 是 Java 提供的一种互斥同步的方法。当我们在代码块上使用 synchronized 时,Java 就会对这段代码进行加锁,确保同一时刻只有一个线程能执行这段代码。
public class Example {
private int a = 0;
private boolean flag = false;
public synchronized void writer(){
a = 1; //1
flag = true; //2
}
public synchronized void reader(){
if (flag){ //3
int i = a * a;//4
}
}
}
volatile 是 Java 提供的一种轻量级的同步机制。它可以确保变量的修改对所有线程立即可见,并阻止指令重排序。
public class Example {
private int a = 0;
private volatile boolean flag = false;
public void writer(){
a = 1; //1
flag = true; //2
}
public void reader(){
if (flag){ //3
int i = a * a;//4
}
}
}
happens-before 原则
定义:如果一个操作 A happens-before 另一个操作 B,那么在 A 完成之前,所有对共享变量的修改,必须对执行操作 B 的线程可见。此外,操作 A 和操作 B 的执行顺序也将被严格保证(也就是说,操作 A 不会在操作 B 之后执行)。
- 程序顺序规则:在一个线程中,按照代码的顺序,写在前面的操作先行发生于写在后面的操作。(一个线程内保证语义的串行性。)
- 锁定规则:一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
- volatile 变量规则:对一个 volatile 域的写操作,先行发生于后面对这个 volatile 域的读操作。
- 传递性:A先于B,B先于C,那么A必然先于C。
- 线程启动规则:Thread 对象的 start 方法先行发生于此线程的每一个动作。
- 线程结束规则:线程中的所有操作都先行发生于对此线程的 join 的返回。(Thread.join())
- 中断规则:对线程 interrupt 方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
- 对象的构造函数执、结束先于finalize()方法。