130.Java深入学习之JVM五

最后一点整理的是关于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的简单学习断断续续总结完 实际上还远远不够

也并未达到连续性的强度学习 后续大概其他事情完事后 会结合一些书籍再深入总结 杀青