2023年2月3日 11:04 周五Java实现CAS的原理[非javaguide]
#
i是非线程安全的,因为**i不是原子操作;可以使用synchronized和CAS实现加锁**
synchronized是悲观锁,一旦获得锁,其他线程进入后就会阻塞等待锁;而CAS是乐观锁,执行时不会加锁,假设没有冲突,如果因为冲突失败了就重试,直到成功
其中i为V,5为E,6为N
CAS是一种原子操作,它是一种系统原语,是一条CPU原子指令,从CPU层面保证它的原子性(不可能出现说,判断了对比了i为5之后,正准备更新它的值,此时该值被其他线程改了)
当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
Java实现CAS的原理 - Unsafe类
}
}
...
2022年12月7日 13:49 周三转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
如果没有特殊说明,针对的都是HotSpot虚拟机
前言
#
- 对于Java程序员,虚拟机自动管理机制,不需要像C/C++程序员为每一个new 操作去写对应的delete/free 操作,不容易出现内存泄漏 和 内存溢出问题
- 但由于内存控制权交给Java虚拟机,一旦出现内存泄漏和溢出方面问题,如果不了解虚拟机是怎么样使用内存,那么很难排查任务
运行时数据区域
#
Java虚拟机在执行Java程序的过程中,会把它管理的内存,划分成若干个不同的数据区域
JDK1.8之前:
- 线程共享
堆,方法区【永久代】(包括运行时常量池)
- 线程私有
虚拟机栈、本地方法栈、程序计数器
- 本地内存(包括直接内存)
![ly-20241212141952681](img/ly-20241212141952681.png)
JDK1.8之后:
1.8之后整个永久代改名叫"元空间",且移到了本地内存中
规范(概括):
线程私有:程序计数器,虚拟机栈,本地方法栈
线程共享:堆,方法区,直接内存(非运行时数据区的一部分)
Java虚拟机规范对于运行时数据区域的规定是相当宽松的,以堆为例:
- 堆可以是连续,也可以不连续
- 大小可以固定,也可以运行时按需扩展
- 虚拟机实现者可以使用任何垃圾回收算法管理堆,设置不进行垃圾收集
程序计数器
#
是一块较小内存空间,看作是当前线程所执行的字节码的行号指示器
java程序流程
![ly-20241212141953092](img/ly-20241212141953092.png)
字节码解释器,工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令
分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器
而且,为了线程切换后恢复到正确执行位置,每条线程需要一个独立程序计数器,各线程计数器互不影响,独立存储,我们称这类内存区域为**“线程私有”**的内存
总结,程序计数器的作用
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制
- 多线程情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切回来的时候能够知道该线程上次运行到哪
程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随线程创建而创建,线程结束而死亡
Java虚拟机栈
#
- Java虚拟机栈,简称"栈",也是线程私有的,生命周期和线程相同,随线程创建而创建,线程死亡而死亡
- 除了Native方法调用的是通过本地方法栈实现的,其他所有的Java方法调用都是通过栈来实现的(需要和其他运行时数据区域比如程序计数器配合)
- 方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈,每一个方法调用结束后,都会有一个栈帧被弹出。
- 栈由一个个栈帧组成,每个栈帧包括局部变量表、操作数栈、动态链接、方法返回地址。
栈为先进后出,且只支持出栈和入栈
![Java 虚拟机栈](img/ly-20241212141953237.png)
局部变量表:存放编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是一个指向一个代表对象的句柄或其他与此对象相关的位置)
![局部变量表](img/ly-20241212141953378.png)
操作数栈 作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。计算过程中产生的临时变量也放在操作数栈中
动态链接 主要服务一个方法需要调用其他方法的场景。
在 Java 源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用(Symbilic Reference)保存在 Class 文件的常量池里。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用。
...
2022年12月6日 17:13 周二转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
Java8被引入的一个非常有用的用于异步编程的类【没看】
简单介绍
#
CompletableFuture同时实现了Future和CompletionStage接口
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
}
CompletableFuture
除了提供了更为好用和强大的 Future
特性之外,还提供了函数式编程的能力。
![img](img/ly-20241212141948027.png)
Future接口有5个方法:
boolean cancel(boolean mayInterruptIfRunning)
:尝试取消执行任务。boolean isCancelled()
:判断任务是否被取消。boolean isDone()
: 判断任务是否已经被执行完成。get()
:等待任务执行完成并获取运算结果。get(long timeout, TimeUnit unit)
:多了一个超时时间。
![img](img/ly-20241212141948313.png)
CompletionStage<T>
接口中的方法比较多,CompoletableFuture的函数式能力就是这个接口赋予的,大量使用Java8引入的函数式编程
常见操作
#
创建CompletableFuture
#
两种方法:new关键字或 CompletableFuture自带的静态工厂方法 runAysnc()
或supplyAsync()
通过new关键字
这个方式,可以看作是将CompletableFuture当作Future来使用,如下:
我们通过创建了一个结果值类型为 RpcResponse<Object>
的 CompletableFuture
,你可以把 resultFuture
看作是异步运算结果的载体
CompletableFuture<RpcResponse<Object>> resultFuture = new CompletableFuture<>();
如果后面某个时刻,得到了最终结果,可以调用complete()方法传入结果,表示resultFuture已经被完成:
// complete() 方法只能调用一次,后续调用将被忽略。
resultFuture.complete(rpcResponse);
通过isDone()检查是否完成:
public boolean isDone() {
return result != null;
}
获取异步结果,使用get() ,调用get()方法的线程会阻塞 直到CompletableFuture完成运算:
rpcResponse = completableFuture.get();
...
2022年12月5日 17:31 周一转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
本文来自一枝花算不算浪漫投稿, 原文地址:
https://juejin.cn/post/6844904151567040519open in new window。 感谢作者!
思维导图
![img](img/ly-20241212141945045.png)
目录
#
ThreadLocal代码演示
#
简单使用
public class ThreadLocalTest {
private List<String> messages = Lists.newArrayList();
public static final ThreadLocal<ThreadLocalTest> holder = ThreadLocal.withInitial(ThreadLocalTest::new);
public static void add(String message) {
holder.get().messages.add(message);
}
public static List<String> clear() {
List<String> messages = holder.get().messages;
holder.remove();
System.out.println("size: " + holder.get().messages.size());
return messages;
}
public static void main(String[] args) {
ThreadLocalTest.add("一枝花算不算浪漫");
System.out.println(holder.get().messages);
ThreadLocalTest.clear();
}
}
/* 结果
[一枝花算不算浪漫]
size: 0
*/
简单使用2
...
2022年12月5日 09:24 周一转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
文章开头先用例子介绍几种类型的api使用
package com.aqs;
import lombok.*;
import java.util.concurrent.atomic.*;
@Data
@Getter
@Setter
@AllArgsConstructor
@ToString
class User {
private String name;
//如果要为atomicReferenceFieldUpdater服务,必须加上volatile修饰
public volatile Integer age;
}
public class AtomicTest {
public static void main(String[] args) {
System.out.println("原子更新数值---------------");
AtomicInteger atomicInteger = new AtomicInteger();
int i1 = atomicInteger.incrementAndGet();
System.out.println("原子增加后为" + i1);
System.out.println("原子更新数组---------------");
int[] a = new int[3];
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(a);
int i = atomicIntegerArray.addAndGet(1, 3);
System.out.println("数组元素[" + 1 + "]增加后为" + i);
System.out.println("数组为" + atomicIntegerArray);
System.out.println("原子更新对象---------------");
User user1 = new User("ly1", 10);
User user2 = new User("ly2", 20);
User user3 = new User("ly3", 30);
AtomicReference<User> atomicReference = new AtomicReference<>(user1);
boolean b = atomicReference.compareAndSet(user2, user3);
System.out.println("更新" + (b ? "成功" : "失败"));
System.out.println("引用里值为"+atomicReference.get());
boolean b1 = atomicReference.compareAndSet(user1, user3);
System.out.println("更新" + (b1 ? "成功" : "失败"));
System.out.println("引用里值为"+atomicReference.get());
System.out.println("原子更新对象属性---------------");
User user4=new User("ly4",40);
AtomicReferenceFieldUpdater<User, Integer> atomicReferenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(User.class, Integer.class, "age");
boolean b2 = atomicReferenceFieldUpdater.compareAndSet(user4, 41, 400);
System.out.println("更新"+(b2?"成功":"失败"));
System.out.println("引用里user4值为"+atomicReferenceFieldUpdater.get(user4));
boolean b3 = atomicReferenceFieldUpdater.compareAndSet(user4, 40, 400);
System.out.println("更新"+(b3?"成功":"失败"));
System.out.println("引用里user4值为"+atomicReferenceFieldUpdater.get(user4));
System.out.println("其他使用---------------");
User user5=new User("ly5",50);
User user6=new User("ly6",60);
User user7=new User("ly7",70);
AtomicMarkableReference<User> userAtomicMarkableReference=new AtomicMarkableReference<>(user5,true);
boolean b4 = userAtomicMarkableReference.weakCompareAndSet(user6, user7, true, false);
System.out.println("更新"+(b4?"成功":"失败"));
System.out.println("引用里值为"+userAtomicMarkableReference.getReference());
boolean b5 = userAtomicMarkableReference.weakCompareAndSet(user5, user7, false, true);
System.out.println("更新"+(b5?"成功":"失败"));
System.out.println("引用里值为"+userAtomicMarkableReference.getReference());
boolean b6 = userAtomicMarkableReference.weakCompareAndSet(user5, user7, true, false);
System.out.println("更新"+(b6?"成功":"失败"));
System.out.println("引用里值为"+userAtomicMarkableReference.getReference());
System.out.println("AtomicStampedReference使用---------------");
User user80=new User("ly8",80);
User user90=new User("ly9",90);
User user100=new User("ly10",100);
AtomicStampedReference<User> userAtomicStampedReference=new AtomicStampedReference<>(user80,80);//版本80
//...每次更改stamp都加1
//这里假设中途被改成81了
boolean b7 = userAtomicStampedReference.compareAndSet(user80, user100,81,90);
System.out.println("更新"+(b7?"成功":"失败"));
System.out.println("引用里值为"+userAtomicStampedReference.getReference());
boolean b8 = userAtomicStampedReference.compareAndSet(user80, user100,80,90);
System.out.println("更新"+(b8?"成功":"失败"));
System.out.println("引用里值为"+userAtomicStampedReference.getReference());
}
}
/*
原子更新数值---------------
原子增加后为1
原子更新数组---------------
数组元素[1]增加后为3
数组为[0, 3, 0]
原子更新对象---------------
更新失败
引用里值为User(name=ly1, age=10)
更新成功
引用里值为User(name=ly3, age=30)
原子更新对象属性---------------
更新失败
引用里user4值为40
更新成功
引用里user4值为400
其他使用---------------
更新失败
引用里值为User(name=ly5, age=50)
更新失败
引用里值为User(name=ly5, age=50)
更新成功
引用里值为User(name=ly7, age=70)
AtomicStampedReference使用---------------
更新失败
引用里值为User(name=ly8, age=80)
更新成功
引用里值为User(name=ly10, age=100)
Process finished with exit code 0
*/
原子类介绍
#
- 在化学上,原子是构成一般物质的最小单位,化学反应中是不可分割的,Atomic指一个操作是不可中断的,即使在多个线程一起执行时,一个操作一旦开始就不会被其他线程干扰
- 原子类–>具有原子/原子操作特征的类
- 并发包java.util.concurrent 的原子类都放着
java.util.concurrent.atomic
中
![ly-20241212141944757](img/ly-20241212141944757.png)
- 根据操作的数据类型,可以将JUC包中的原子类分为4类(基本类型、数组类型、引用类型、对象的属性修改类型)
2022年11月30日 14:48 周三转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
Semaphore [ˈseməfɔː(r)]
- 何为 AQS?AQS 原理了解吗?
CountDownLatch
和 CyclicBarrier
了解吗?两者的区别是什么?- 用过
Semaphore
吗?应用场景了解吗? - ……
AQS简单介绍
#
AQS,AbstractQueueSyschronizer,即抽象队列同步器,这个类在java.util.concurrent.locks包下面
![ly-20241212141944167](img/ly-20241212141944167.png)
AQS是一个抽象类,主要用来构建锁和同步器
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
}
AQS 为构建锁和同步器提供了一些通用功能的实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock
,Semaphore
,其他的诸如 ReentrantReadWriteLock
,SynchronousQueue
,FutureTask
(jdk1.7) 等等皆是基于 AQS 的。
AQS原理
#
AQS核心思想
#
面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来
AQS 核心思想是,如果被请求的共享资源(AQS内部)空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。
[ 搜索了一下,CLH好像是人名 ]
在 CLH 同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。
CLH队列结构
![ly-20241212141944445](img/ly-20241212141944445.png)
2022年11月29日 16:58 周二转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
JDK提供的容器,大部分在java.util.concurrent包中
- ConcurrentHashMap:线程安全的HashMap
- CopyOnWriteArrayList:线程安全的List,在读多写少的场合性能非常好,远好于Vector
- ConcurrentLinkedQueue:高效的并发队列,使用链表实现,可以看作一个线程安全的LinkedList,是一个非阻塞队列
- BlockingQueue:这是一个接口,JDK内部通过链表、数组等方式实现了该接口。表示阻塞队列,非常适合用于作为数据共享的通道
- ConcorrentSkipListMap:跳表的实现,是一个Map,使用跳表的数据结构进行快速查找
ConcurrentHashMap
#
- HashMap是线程不安全的,并发场景下要保证线程安全,可以使用Collections.synchronizedMap()方法来包装HashMap,但这是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来性能问题
- 建议使用ConcurrentHashMap,不论是读操作还是写操作都能保证高性能:读操作(几乎)不需要加锁,而写操作时通过锁分段(这里说的是JDK1.7?)技术,只对所操作的段加锁而不影响客户端对其他段的访问
CopyOnWriteArrayList
#
//源码
public class CopyOnWriteArrayList<E>
extends Object
implements List<E>, RandomAccess, Cloneable, Serializable
- 在很多应用场景中,读操作可能会远远大于写操作
- 我们应该允许多个线程同时访问List内部数据(针对读)
- 与ReentrantReadWriteLock读写锁思想非常类似,即读读共享、写写互斥、读写互斥、写读互斥
- 不一样的是,CopyOnWriteArrayList读取时完全不需要加锁,且写入也不会阻塞读取操作,只有写入和写入之间需要同步等待。
CopyOnWriteArrayList是如何做到的
#
CopyOnWriteArrayList
类的所有可变操作(add,set 等等)都是通过创建底层数组的新副本来实现的。当 List 需要被修改的时候,并不修改原有内容,而是对原有数据进行一次复制,将修改的内容写入副本。写完之后,再将修改完的副本替换原来的数据,这样就可以保证写操作不会影响读操作了。- 从
CopyOnWriteArrayList
的名字就能看出 CopyOnWriteArrayList
是满足 CopyOnWrite
的 - 在计算机,如果你想要对一块内存进行修改时,我们不在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后呢,就将指向原来内存指针指向新的内存(注意,是指向,而不是重新拷贝★重要★),原来的内存就可以被回收掉了
CopyOnWriteArrayList 读取和写入源码简单分析
#
2022年11月29日 11:31 周二转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
线程池知识回顾
#
1. 为什么要使用线程池
#
- 池化技术的思想,主要是为了减少每次获取资源(线程资源)的消耗,提高对资源的利用率
- 线程池提供了一种限制和管理资源(包括执行一个任务)的方法,每个线程池还维护一些基本统计信息,例如已完成任务的数量
好处:
- 降低资源消耗
- 提高响应速度
- 提高线程的可管理性
2. 线程池在实际项目的使用场景
#
线程池一般用于执行多个不相关联的耗时任务,没有多线程的情况下,任务顺序执行,使用了线程池的话可让多个不相关联的任务同时执行。
![ly-20241212141942375](img/ly-20241212141942375.png)
3. 如何使用线程池
#
一般是通过 ThreadPoolExecutor
的构造函数来创建线程池,然后提交任务给线程池执行就可以了。构造函数如下:
/**
* 用给定的初始参数创建一个新的ThreadPoolExecutor。
*/
public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
int maximumPoolSize,//线程池的最大线程数
long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
使用代码:
...
2022年11月23日 14:40 周三转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
一 使用线程池的好处
#
- 池化技术:减少每次获取资源的消耗,提高对资源的利用率
- 线程池提供一种限制和管理资源(包括执行一个任务)的方式,每个线程池还维护一些基本统计信息,例如已完成任务的数量
- 线程池的好处
- 降低资源消耗(重复利用,降低线程创建和销毁造成的消耗)
- 提高响应速度(任务到达直接执行,无需等待线程创建)
- 提高线程可管理性(避免无休止创建,使用线程池统一分配、调优、监控)
二 Executor框架
#
Java5之后,通过Executor启动线程,比使用Thread的start方法更好,更易于管理,效率高,还能有助于避免this逃逸的问题
this逃逸,指的是构造函数返回之前,其他线程就持有该对象的引用,会导致调用尚未构造完全的对象
例子:
public class ThisEscape {
public ThisEscape() {
new Thread(new EscapeRunnable()).start();
// ...
}
private class EscapeRunnable implements Runnable {
@Override
public void run() {
// 通过ThisEscape.this就可以引用外围类对象, 但是此时外围类对象可能还没有构造完成, 即发生了外围类的this引用的逃逸
}
}
}
处理办法 //不要在构造函数中运行线程
public class ThisEscape {
private Thread t;
public ThisEscape() {
t = new Thread(new EscapeRunnable());
// ...
}
public void init() {
//也就是说对象没有构造完成前,不要调用ThisEscape.this即可
t.start();
}
private class EscapeRunnable implements Runnable {
@Override
public void run() {
// 通过ThisEscape.this就可以引用外围类对象, 此时可以保证外围类对象已经构造完成
}
}
}
Executor框架不仅包括线程池的管理,提供线程工厂、队列以及拒绝策略。
...
2022年11月21日 10:57 周一引用自https://github.com/Snailclimb/JavaGuide
从CPU缓存模型说起
#
redis是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题,CPU缓存则是为了解决CPU处理速度和内存处理速度不对等的问题
我们把内存看作外存的高速缓存,程序运行时把外存的数据复制到内存,由于内存的处理速度远高于外存,这样提高了处理速度
总结,CPU Cache缓存的是内存数据,用于解决CPU处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题
CPU Cache示意图:
CPU Cache通常分为三层,分别叫L1,L2,L3 Cache
工作方式: 先复制一份数据到CPUCache中,当CPU需要用的时候就可以从CPUCache中读取数据,运算完成后,将运算得到的数据,写回MainMemory中,此时,会出现内存缓存不一致的问题,例子:执行了i++,如果两个线程同时执行,假设两个线程从CPUCach中读取的i=1,两个线程做了1++运算完之后再写回MainMemory,此时i=2 而正确结果为3
CPU为了解决内存缓存不一致问题,可以通过制定缓存一致协议(比如MESI协议)或其他手段。这个缓存一致协议,指的是在 CPU 高速缓存与主内存交互的时候需要遵守的原则和规范
操作系统,通过内存模型MemoryModel定义一系列规范来解决这个问题
Java内存模型
#
![ly-20241212141939466](img/ly-20241212141939466.png)
指令重排序
#
另外,内存系统也会有“重排序”,但又不是真正意义上的重排序。在 JMM 里表现为主存和本地内存的内容可能不一致,进而导致程序在多线程下执行可能出现问题。
即Java源代码会经历 编译器优化重排—>指令并行重排—>内存系统重排,最终编程操作系统可执行的指令序列
极其重要★:指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致,所以在多线程下指令重排可能导致一些问题
编译器和处理器的指令重排序的处理方式不一样。对于编译器,通过禁止特定类型的编译器重排序的方式来禁止重排序。对于处理器,通过插入内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)的方式来禁止特定类型的处理器重排序。指令并行重排和内存系统重排都属于是处理器级别的指令重排序。
内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种 CPU 指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障指令执行的有序性。另外,为了达到屏障的效果,它也会使处理器写入、读取值之前,将主内存的值写入高速缓存,清空无效队列,从而保障变量的可见性。
JMM(JavaMemoryMode)
#
什么是 JMM?为什么需要 JMM?
#
一般来说,编程语言也可以直接复用操作系统层面的内存模型。不过,不同的操作系统内存模型不同。如果直接复用操作系统层面的内存模型,就可能会导致同样一套代码换了一个操作系统就无法执行了。Java 语言是跨平台的,它需要自己提供一套内存模型以屏蔽系统差异。
实际上,对于Java来说,可以把JMM看作是Java定义的并发编程相关的一组规范,除了抽象了线程和主内存之间的关系之外,还规定了从Java源代码到CPU可执行指令的转化过程要遵守哪些和并发相关的原则和规范,主要目的是为了简化多线程编程,增强程序可移植性。
为什么要遵守这些并发相关的原则和规范呢?因为在并发编程下,CPU多级缓存和指令重排这类设计会导致程序运行出问题,比如指令重排,为此JMM抽象了happens-before原则
JMM 说白了就是定义了一些规范来解决这些问题,开发者可以利用这些规范更方便地开发多线程程序。对于 Java 开发者说,你不需要了解底层原理,直接使用并发相关的一些关键字和类(比如 volatile
、synchronized
、各种 Lock
)即可开发出并发安全的程序。
...