Java 内存模型
Java 内存模型是什么
我们知道的 JVM 内存区域有:堆和栈,这是一种泛的分法,也是按运行时区域的
一种分法,堆是所有线程共享的一块区域,而栈是线程隔离的,每个线程互不共享。
线程不共享区域每个线程的数据区域包括程序计数器、虚拟机栈和本地方法栈,它
们都是在新线程创建时才创建的。
程序计数器( Program Counter Rerister )
程序计数器区域一块内存较小的区域,它用于存储线程的每个执行指令,每个线程
都有自己的程序计数器,此区域不会有内存溢出的情况。
虚拟机栈( VM Stack )
虚拟机栈描述的是 Java 方法执行的内存模型,每个方法被执行的时候都会同时创
建一个栈帧( Stack Frame )用于存储局部变量表、操作数栈、动态链接、方法出口
等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从
入栈到出栈的过程。
本地方法栈( Native Method Stack )
本地方法栈用于支持本地方法( native 标识的方法,即非 Java 语言实现的方法)。
虚拟机栈和本地方法栈,当线程请求分配的栈容量超过 JVM 允许的最大容量时抛
出 StackOverflowError 异常。
线程共享区域
线程共享区域包含:堆和方法区。
堆( Heap )
堆是最常处理的区域,它存储在 JVM 启动时创建的数组和对象, JVM 垃圾收集也
主要是在堆上面工作。
如 果 实 际 所 需 的 堆 超 过 了 自 动 内 存 管 理 系 统 能 提 供 的 最 大 容 量 时 抛 出 1
8
OutOfMemoryError 异常。
方法区( Method Area )
方法区是可供各条线程共享的运行时内存区域。存储了每一个类的结构信息,例如
运行时常量池( Runtime Constant Pool )、字段和方法数据、构造函数和普通方法
的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法。当创建类
和接口时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大内
存空间后就会抛出 OutOfMemoryError
运行时常量池( Runtime Constant Pool )
运行时常量池是方法区的一部分,每一个运行时常量池都分配在 JVM 的方法区中,
在类和接口被加载到 JVM 后,对应的运行时常量池就被创建。运行时常量池是每
一个类或接口的常量池( Constant_Pool )的运行时表现形式,它包括了若干种常量:
编译器可知的数值字面量到必须运行期解析后才能获得的方法或字段的引用。如果
方法区的内存空间不能满足内存分配请求,那 Java 虚 拟 机 将 抛 出 一 个
OutOfMemoryError 异常。栈包含 Frames ,当调用方法时, Frame 被推送到堆栈。
一个 Frame 包含局部变量数组、操作数栈、常量池引用。
什么是乐观锁和悲观锁
乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐
观锁认为竞争不总是会发生,因此它不需要持有锁,将比较 - 替换这两个动作作为
一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有
相应的重试逻辑。
悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,
悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的
锁,就像 synchronized ,不管三七二十一,直接上了锁就操作资源了。
实例
乐观锁和悲观锁是并发编程中常用的两种锁机制,用于解决多线程访问共享资源的同步问题。
乐观锁: 乐观锁机制是指在访问共享资源之前,先假设其他线程不会修改该资源,从而避免加锁带来的性能开销。如果发生冲突,则进行冲突检测并处理。
以下是使用乐观锁的示例代码:
import java.util.concurrent.atomic.AtomicInteger;public class OptimisticLockExample {private AtomicInteger count = new AtomicInteger(0);public void increment() {int oldValue;int newValue;do {oldValue = count.get(); // 获取当前值newValue = oldValue + 1; // 计算新值} while (!count.compareAndSet(oldValue, newValue)); // CAS操作,如果当前值与期望值相等,则更新为新值}public static void main(String[] args) {OptimisticLockExample example = new OptimisticLockExample();Thread thread1 = new Thread(() -> {for (int i = 0; i < 1000; i++) {example.increment();}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 1000; i++) {example.increment();}});thread1.start();thread2.start();try {thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Count: " + example.count.get());}
}
悲观锁: 悲观锁机制是指在访问共享资源之前,先假设其他线程会修改该资源,因此加锁保证资源的独占性。只有获得锁的线程能够访问该资源。
以下是使用悲观锁的示例代码:
import java.util.concurrent.locks.ReentrantLock;public class PessimisticLockExample {private int count = 0;private ReentrantLock lock = new ReentrantLock();public void increment() {lock.lock(); // 加锁try {count++; // 访问共享资源} finally {lock.unlock(); // 解锁}}public static void main(String[] args) {PessimisticLockExample example = new PessimisticLockExample();Thread thread1 = new Thread(() -> {for (int i = 0; i < 1000; i++) {example.increment();}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 1000; i++) {example.increment();}});thread1.start();thread2.start();try {thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Count: " + example.count);}
}
以上示例代码分别演示了乐观锁和悲观锁的实现。乐观锁使用CAS操作来保证资源的更新,而悲观锁使用ReentrantLock来实现锁的获取和释放。
总结
乐观锁和悲观锁是并发编程中常用的两种锁机制,用于解决多线程环境下的数据一致性问题。
- 乐观锁: 乐观锁的核心思想是假设并发情况下不会发生冲突,因此不需要加锁。当多个线程同时访问共享资源时,乐观锁采用一种检测机制来判断是否存在冲突。如果存在冲突,就放弃当前操作,通过重试或回滚来保证数据的一致性。
乐观锁的实现方式主要有两种:
- 版本号机制:在数据表中增加一个版本号字段,每次更新数据时,同时更新版本号。当多个线程尝试更新同一条数据时,只有其中一个线程能够成功,其余的线程会检测到版本号不一致而放弃操作。
- CAS(Compare and Swap)操作:通过比较当前值和期望值是否相等,如果相等则更新值,否则重试。
乐观锁适用于读操作较多的场景,能够提高并发性能,但在写操作较多的场景下容易引发冲突。
- 悲观锁: 悲观锁的核心思想是假设并发情况下会发生冲突,因此默认加锁保证数据的一致性。当一个线程对共享资源进行读或写操作时,悲观锁会将资源加锁,其他线程需要等待锁释放之后才能访问。
悲观锁的实现方式主要有两种:
- synchronized关键字:在方法或代码块上加上synchronized关键字,保证同一时间只有一个线程能够访问该代码块或方法。
- ReentrantLock类:通过ReentrantLock和Condition实现对共享资源的加锁和解锁,能够更灵活地控制锁的获取和释放。
悲观锁适用于写操作较多的场景,能够确保数据的一致性,但会降低并发性能。