最后一点整理的是关于JMM和锁的知识一点补充
JMM
区分Java内存模型JMM JVM内存结构
前面讲的JVM内存结构就是内存怎么分区存东西
JMM即多线程怎么安全的读写存的那些东西
有重要的三个特性:
- 可见性
- 原子性
- 有序性
原子性 (操作不可被线程打断)
我们用一个例子:案例线程安全问题synchronized的使用
正常来说就是 a初值8 无论怎么加减都应该保持最终8不变 但是不上锁就会出现情况
java
public class JMM {
static int a=8;
static Object obj=new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
synchronized (obj){
for (int i = 0; i < 50000; i++) {
a++;
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (obj){
for (int i = 0; i < 50000; i++) {
a--;
}
}
});
thread1.start();
thread2.start();
thread1.join(); //等待线程结束
thread2.join();
System.out.println(a);
}
}
这个程序其实很常见了 就是上锁确定线程一个执行完了让其他执行
假设不加锁 出现什么情况
注释掉锁
编译产看参数
结果已经改变 注意加大循环次数才会模拟真实出现的情况
再加上锁 看运行
得到正确的想法
编译查看参数 多了什么东西保证的
使用命令:javap -c -p 查看编译后的隐藏信息
可以注意到两个进程多了monitorenter 就是上锁
后面的monitorexit 释放锁
理解到这就行了吗 nonono
有个问题就是到底是先进程1加完 然后进程再减完呢?
实际上:原子性同一时刻仅一个线程操作 无论哪一个
就是粗俗点意思:假设thread1先运行上锁 等干完了 再让thread2干 thread1干的期间 thread2也想干但是上锁了进不来
反正是一个进程弄完 然后另一个进程再弄
可见性 (即一个线程修改了变量 其他线程能够立即看到这个修改)
案例验证:
java
public class JMM2 {
static int a = 5;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (a > 0) ;
});
thread.start();
Thread.sleep(1000);
a = 0;
}
}
我理解的就是a初值 然后进程开始循环 停1秒 a置为0 就不进循环结束了
但运行结果
压根没停下来
为啥呢 肯定和JMM有关
JMM把a读到自己工作内存不会重新从主内存读取看不到 a永远是5
那不行啊 我得让他去主内存读啊 我让a0停下来啊
使用易变关键字volatile
作用就是:volatile每次从主内存读取 写刷新到主内存 程序将会结束
很简单在a那里加入关键字
运行后查看结果
有序性 (按顺序来)
如下程序 就是分析结果
java
public class JMM3 {
int num = 0;
boolean ok = false;
public void method1() {
num = 2;
ok = true;
}
public void method2(Fang r) {
if (ok) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
}
比如一先执行完 ok就是true r1就是4
或者二先执行 ok是false r1就是1
或者交叉运行r1最终r1还是1
但是会出现情况r1为0 那就是一就没执行直接干二了 这种就是乱序
为了验证加大循环次数也不一定能出现 需要借助工具
JCStress复现出现的问题
使用该语法编写后
java
@JCStressTest
@Outcome(id = "0", expect = Expect.ACCEPTABLE, desc = "发生乱序")
@Outcome(id = "4", expect = Expect.ACCEPTABLE, desc = "正常情况")
@Outcome(id = "1", expect = Expect.ACCEPTABLE, desc = "未进入if")
@State
public class JMM3 {
int num = 0;
boolean ok = false;
@Actor
public void method1() {
num = 2;
ok = true;
}
@Actor
public void method2(IntResult1 r) {
if (ok) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
}
有两种方式测试 一种是mvn直接打包jar包然后运行 但是能可以看到在本工程中要引入依赖然后打包 众所周知 这种spring版本和依赖的XX的问题我想你肯定知道这个java开发的恶心
所以我果断选择第二种就是去官网把JCStress项目下下来 然后打成jar包 然后把这个程序代码直接在里面测试 不用加啥依赖了
地址:https://github.com/openjdk/jcstress?utm_source=chatgpt.com#
大致得到如下目录

然后maven打包 在test.all/target目录下得到jar打包
然后再把我们要测试的程序代码放在这里 测试
javac -cp jcstress.jar JMM3.java编译
java -cp .;jcstress.jar org.openjdk.jcstress.Main JMM3 -t 5运行 限制每个测试5秒
出现0就表示过出现乱序了
解决方式同样是加入volatile 保证了可见性和有序性
我想过用锁关键字synchronized 肯定成 因为synchronized保证三个特性
至此三种特性讲完 第三种没错我完全跟着GPT做的 从安装到测试得到结果 出现错误就纠正错误
现在AI已经跟我三四年前不一样了
有句话说得好 怎么知道AI的不足 就是你用的时候感觉哪里还是机械性的工作感觉没意义的就能
比如说之前 填写每天的日记 即使AI自动生成也要点开登录复制进去 最近也不算最近吧
openclaw已经可以自动化了 就设想给它设置定时任务 让它去每天自动填写实习日记 哈哈哈
AI的一些作用就是来替代这些机械性的工作 用的过程哪里感觉垃圾就是AI还需要强化的
happens-before:实际上就是来概述关于JMM的可见性(比如volatile)
A happens-before B 即A对B是可见的 A先于B发生
CAS(Compare-And-Swap 乐观锁 原子操作)
对立的就是悲观锁
面试问题:悲观锁(synchronized)和乐观锁(例如CAS)
悲观锁就是我给你锁住别xx想用 等我用完了你们再用
乐观锁就是你们也可以用 修改了我就再重试
对于CAS的原理:
比如说要修改原来的值 不加锁会被其他进程影响
但CAS是原子操作比较替换 这里注意它不是一种锁而是自带校验的一种"锁"
使用它是因为乐观锁 不会阻塞情况失败就会重试
CAS(旧值 新值) 会让旧值和原来的值比较 若没变化则更改为新值
底层是UnSafe类实现
用个例子来验证:
java
public class Test_CAS {
static AtomicInteger atomicInteger = new AtomicInteger(100);
public static void main(String[] args) throws InterruptedException {
System.out.println("初始值:" + atomicInteger.get());
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
atomicInteger.getAndIncrement(); // 得到值并+1
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
atomicInteger.getAndDecrement(); // 得到值并-1
}
});
thread1.start();
thread2.start();
thread1.join(); //等待线程结束
thread2.join();
System.out.println("执行后:" + atomicInteger);
}
}
其中 AtomicInteger = CAS + volatile
打印结果并未变化
CAS 检查替换无限回旋
锁的优化和状态
四种状态:
-
无锁
-
偏向锁
-
轻量级锁
-
重量级锁
-
轻量级锁:很多情况同步代码块不会产生竞争(短时间竞争) 这个时候就不需要重量级锁 从而提升性能
-
偏向锁:一个线程反复使用锁 将锁分配给它
-
重量级锁:竞争严重 加锁阻塞
-
无锁 → 普通代码
偏向锁 → 单线程反复进 没竞争
轻量级锁 → 两个线程轻微竞争 CAS自旋解决 不阻塞
重量级锁 → 多线程 + 长时间占锁
java
我直接让AI生成了把这四段状态验证放一个程序里
public class Synchronized_4 {
static final Object lock = new Object();
public static void main(String[] args) throws Exception {
// ========= 1️⃣ 无锁 =========
System.out.println("1️⃣ 无锁阶段");
int a = 0;
for (int i = 0; i < 10000; i++) {
a++;
}
Thread.sleep(1000);
// ========= 2️⃣ 偏向锁 =========
System.out.println("2️⃣ 偏向锁阶段(单线程反复进入)");
for (int i = 0; i < 10000; i++) {
synchronized (lock) {
// 同一线程反复获取
}
}
Thread.sleep(1000);
// ========= 3️⃣ 轻量级锁 =========
System.out.println("3️⃣ 轻量级锁阶段(少量竞争)");
Runnable lightTask = () -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock) {
// 很短的同步块 → 自旋即可
}
}
};
Thread t1 = new Thread(lightTask);
Thread t2 = new Thread(lightTask);
t1.start();
t2.start();
t1.join();
t2.join();
Thread.sleep(1000);
// ========= 4️⃣ 重量级锁 =========
System.out.println("4️⃣ 重量级锁阶段(激烈竞争)");
Runnable heavyTask = () -> {
while (true) {
synchronized (lock) {
try {
Thread.sleep(100); // 故意长时间占锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
for (int i = 0; i < 4; i++) {
new Thread(heavyTask).start();
}
}
}
关于锁的优化我们讲两个:
- 锁消除
- 锁粗化
锁消除:不会被共享 没发生逃逸 直接删锁减少开销
java比如:
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
StringBuffer自带synchronized确保线程安全 但是这个压根没发生逃逸 就删除了锁
锁粗化:多个锁合成一个大锁
java比如:
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
每个都是实现了线程 都加锁 再解锁
所以直接优化:
java
StringBuffer sb = new StringBuffer();
synchronized(sb) {
sb.append("a");
sb.append("b");
sb.append("c");
}
一次加锁 三次操作
到这里吧JVM的简单学习断断续续总结完 实际上还远远不够
也并未达到连续性的强度学习 后续大概其他事情完事后 会结合一些书籍再深入总结 杀青











