Java内存模型(Java Memory Model, JMM)是定义在Java虚拟机(JVM)中如何处理多线程间共享变量读写的规范。
它为Java并发编程提供了一套规则,确保程序在多核处理器上的正确执行。
Java内存模型的主要目标:
定义程序中变量的访问规则,即在什么条件下,一个线程对共享变量的修改对另外一个线程可见。
提供一种机制来控制多线程编程中的同步访问,以避免线程之间的数据不一致。
Java内存模型的主要组件:
主内存(Main Memory):
主内存是所有线程共享的内存区域,存放所有Java对象实例以及类的静态字段。
当线程需要读取变量时,它会从主内存中获取这些变量的值。类似地,当线程需要写入变量时,变量值最终会被更新回主内存。主内存也是线程间共享变量的媒介,它保持了线程间的变量可见性。
工作内存(Working Memory):
工作内存与主内存相对应,是线程私有的内存区域。
每个线程都有自己的工作内存,它包含了线程使用到的变量的主内存副本。
当线程操作变量时,它实际是在自己的工作内存中进行操作。
例如,当线程执行一个计算任务时,它将读取的变量从主内存复制到工作内存,进行运算处理后,再将结果更新回主内存。
这个过程保证了线程在执行时拥有所需数据的快速访问,因为访问工作内存比访问主内存要快。
主内存与工作内存的交互
主内存和工作内存之间的交互关系由JMM的内存一致性模型规定。以下是这种交互的几个核心操作:
锁定(Lock)和解锁(Unlock):当一个线程需要同步访问共享变量时,它会通过锁定来确保其他线程不能同时修改该变量。
读取(Read)和加载(Load):线程从主内存读取变量,然后加载到工作内存中。
使用(Use)和赋值(Assign):线程可以操作工作内存中的变量,执行计算。
存储(Store)和写入(Write):线程将变更后的变量值存储在工作内存中,然后写入到主内存。
同步机制:
为了保证主内存与工作内存中变量值的一致性,JMM提供了同步机制。这些机制包括volatile关键字、synchronized块、锁机制以及原子变量操作等。
volatile变量:
对volatile变量的读写操作直接作用于主内存,确保变量的可见性和禁止指令重排序。(后面有代码解释)
synchronized块:
进入synchronized块时,会清空工作内存中的变量值,强制重读主内存。
退出synchronized块时,会将工作内存中的共享变量的最新值刷新回主内存。
原理:
当线程进入synchronized方法或代码块时,它将自动获得锁。出现以下两种情况之一:
如果锁是可用的(即当前没有其他线程持有),线程就会获得锁并继续执行。
如果锁不可用,则线程将被阻塞(挂起执行)直到锁被释放。
当线程离开synchronized方法或代码块时,无论是通过正常的执行路径还是通过抛出异常的方式,它将自动释放锁。
作用域:(文章后有代码解释)
Synchronized 方法:
用synchronized关键字修饰方法分为两种情况:
实例方法:
锁定的是调用该方法的对象实例(this)。
当一个线程进入一个对象的同步实例方法时,它自动获得那个对象的锁,那么其他线程就无法同时进入这个对象的任何其他同步实例方法。
静态方法:
锁定的是这个类的所有对象共用的类对象(Class对象)。
如果一个线程访问类的同步静态方法,它持有的是类锁,因此其他线程不能同时执行这个类的任何同步静态方法。
Synchronized 代码块:
synchronized还可以用来修饰代码块,而不是整个方法。
这允许锁定任何对象,为线程提供更细粒度的控制,从而减小锁的范围,提高效率。
同步某个对象:
可以指定锁定一个给定的对象。
不同的代码块可以锁定不同的对象,这样它们就可以并行执行。
同步类对象:
可以锁定类的Class对象,跟静态同步方法类似,这样线程在访问类的任何其他同步代码块时都必须等待释放类锁。
同步一个资源: 为了保护非同步方法中的临界资源,可以创建一个特定的对象来作为锁。
锁:
获取锁时,清空工作内存;释放锁时,将修改刷新回主内存。
原理:
当某个线程(或进程)需要访问共享资源时,它会尝试获得与该资源关联的锁:
如果锁是可用的,即没有其他线程持有该锁,那么请求锁的线程会获得该锁并进入其临界区,这是一段只允许单个线程进入的代码。
如果锁不可用,即已被另一个线程持有,请求锁的线程可能会进入等待状态,直到锁被释放。
一旦线程完成了对共享资源的操作,它将释放锁,这样其他线程就可以请求并获得锁来访问资源。
锁的类型
互斥锁(Mutex):互斥锁是最简单的锁类型,用于保证同一时间只有一个线程可以持有锁。这保证了对共享资源的独占访问。
读写锁(Read-Write Lock):读写锁允许多个线程同时读取资源,但在写入时需要独占访问。当锁处于写模式时,其他线程既不能读也不能写。
自旋锁(Spinlock):自旋锁是一种忙等待锁,当线程试图获得锁而锁已被其他线程持有时,线程将循环等待,直到锁变得可用。
递归锁(Recursive Lock):递归锁允许同一个线程多次获得锁。该线程必须释放锁与其获得次数相同的次数才能真正释放该锁。
条件变量(Condition Variable):条件变量不是锁本身,而是建立在锁之上的同步原语,允许线程在某些条件尚未满足时挂起,并且当条件满足时,允许其他线程唤醒它。
性能和死锁
锁是一个强大的工具,但如果不当使用,可能会导致性能问题或死锁。以下是一些要点:
锁粒度:精细的锁(锁定小的资源区域)可能导致更高的并发度,而粗粒度的锁(锁定大的资源区域)可能导致较低的并发度。
死锁:当两个或多个线程在等待对方释放锁时,可能发生死锁。这是一种情况,线程永远等待一个永远不会发生的事件。
锁竞争:当多个线程频繁请求相同的锁时,会发生锁竞争,可能会导致性能下降。
原子变量操作:
CAS(Compare-And-Swap)比较和替换
是一种同步机制,用于在多线程环境中安全地更新共享资源
CAS操作涉及三个操作数:
内存位置(V):这是CAS操作要更新的变量的地址。
预期原值(A):这是我们预期在内存位置V中找到的值。
新值(B):如果内存位置V的当前值与预期原值A匹配,应将V的值更新为这个新值B。
这是CAS的基本工作原理:
读取当前值,比较当前值,条件更新
Compare-And-Swap(CAS)操作通常返回一个布尔值,表示操作是否成功。
CAS操作的基本工作原理是这样的:
它将内存位置的当前值与一个旧的预期值进行比较。
如果当前值与预期值相匹配,就将该内存位置更新为一个新的给定值。
CAS操作会返回一个布尔值,指示更新是否成功执行。
线程锁 AQS
是Java中用于构建锁和其他同步组件的一个框架(ReadWriteLock读写锁)
AQS使用一个int类型的变量来表示同步状态,并通过一个内部的FIFO队列来管理那些在同步状态上等待的线程(被包装成Node)。
同步状态:AQS内部有一个volatile int类型的变量来表示同步状态。
FIFO队列 (First In, First Out)在FIFO队列中,插入操作(入队)通常发生在队列的尾部,而移除操作(出队)则发生在队列的头部。这就意味着,所有的元素都将按照它们被加入队列的顺序来移除。
AQS使用了一个内部类Node来表示等待队列中的每一个元素,每个Node包含了线程的引用、等待状态和连接前后节点的链接。AQS通过头尾节点的引用管理队列。
获取操作:线程尝试获取资源,如果成功,则执行任务;如果失败,它将会被包装成节点加入队列。
释放操作:释放当前持有的资源,并且可能会唤醒队列中的后续节点(线程)。
两种同步模式:
独占模式(Exclusive):
这种模式下,一个时间点只能有一个线程成功地获取同步状态。
其他任何线程都无法获取同步状态,直到它被当前拥有它的线程释放。这是实现互斥锁的一种方式。
共享模式(Shared):
在这种模式下,同步状态可以被多个线程同时获取。
例如,在实现一个计数信号量或读写锁的“读”锁时,通常会使用共享模式,因为多个线程应该能够同时读取。
代码解释:
下面是一个使用volatile关键字的简单Java代码示例。在这个例子中,有两个线程:一个用来修改一个volatile变量的值,另一个用来读取这个变量的值。使用volatile保证了当变量值被修改时,其他线程能够立即看到这个更改。
public class VolatileExample {
// 定义一个volatile变量
private volatile boolean active;
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
public void runExample() {
// 创建一个线程修改active变量的值
Thread thread1 = new Thread(() -> {
while (!isActive()) {
// 在这个循环中等待active变量变为true
}
System.out.println("Thread 1 sees active is true and exits the loop");
});
// 创建另一个线程设置active变量的值为true
Thread thread2 = new Thread(() -> {
// 模拟一些初始化工作,花费一些时间
try {
Thread.sleep(1000); // 1秒后设置active为true
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Thread 2 setting active to true");
setActive(true);
});
// 启动两个线程
thread1.start();
thread2.start();
}
public static void main(String[] args) {
new VolatileExample().runExample();
}
}
在这个例子中,active是一个volatile变量。Thread 1在一个循环中等待active变为true。Thread 2在睡眠一秒后将active设置为true。
如果active不是volatile变量,Thread 1可能会无限循环,因为它可能不会看到Thread 2对active的更改。但是,因为active被声明为volatile,JVM保证了active的更改对所有线程立即可见。所以,当Thread 2修改了active后,Thread 1能够看到这个更改并退出循环。
Synchronized 实例方法:
在这个例子中,increment()和getCount()都是同步的实例方法,它们锁定的是Counter对象的实例。
public class Counter {
private int count = 0;
// Synchronized instance method
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
Synchronized 静态方法:
这里,staticIncrement()和getStaticCount()是同步的静态方法,它们锁定的是StaticCounter类的Class对象。
public class StaticCounter {
private static int staticCount = 0;
// Synchronized static method
public static synchronized void staticIncrement() {
staticCount++;
}
public static synchronized int getStaticCount() {
return staticCount;
}
}
Synchronized 代码块 - 同步某个对象:
在这里,我们使用synchronized块锁定一个私有的lock对象,而不是整个方法。
public class BlockCounter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
// Synchronized block using a private lock object
synchronized (lock) {
count++;
}
}
public int getCount() {
synchronized (lock) {
return count;
}
}
}
Synchronized 代码块 - 同步类对象:
对于静态变量,我们可以使用synchronized代码块并锁定Class对象,如BlockStaticCounter.class。
public class BlockStaticCounter {
private static int staticCount = 0;
public void staticIncrement() {
// Synchronized block using the class object as the lock
synchronized (BlockStaticCounter.class) {
staticCount++;
}
}
public int getStaticCount() {
synchronized (BlockStaticCounter.class) {
return staticCount;
}
}
}
Synchronized 代码块 - 同步一个资源:
在这个例子中,我们专门为计数器的增量操作创建了一个锁对象resourceLock。
public class ResourceCounter {
private volatile int count = 0;
private final Object resourceLock = new Object();
public void safeIncrement() {
// Synchronized block using a resource-specific lock
synchronized (resourceLock) {
count++;
}
}
public int getCount() {
// No need to synchronize if you're only reading a single atomic value
return count;
}
}