JVM-JVM支持高并发底层原理精讲

一、透彻掌握高并发-从理解JVM开始

二、从线程的开闭看JVM的作用

1.run方法

启动start方法,会调用底层C++方法,告诉操作系统当前线程处于可运行状态,而如果直接调用run方法,则就不是以线程的方式来运行了,只是当做一个普通的方法来执行。

2.stop方法 -不推荐使用,stop是强制执行,不管是否正在运行,在做什么,直接停止。

3.如何中断一个阻塞线程

关闭处于阻塞中的线程:

按照上面关闭普通线程的方式,来关闭阻塞中的线程,会发现报了一个异常,首先需要明确的是,

这个异常不是错误。

继续修改下代码

当线程即使处于阻塞的时候,线程不再收到信号,线程也是可以收到一个异常,可以这个异常理解为一个信号,就像闹钟一样就会响,强制这个线程做出一定的响应,而这个异常就是这个子线程的那种,当调用线程终止方法,就会触发这个异常。

这里为什么没有停止呢,这是因为当阻塞的时候,父线程只能给子线程发停止的信号,要不要停止子线程说了算。

再修改下代码

重新执行,子线程自己停止了线程(即Main方法的thread.interrupt()只是发了一个停止的信号,实际子线程停止是子线程自己负责执行)。

这也就是为什么一般写代码遇见wait、sleep要加这个异常。

大白话:

Java线程调用start(),JVM通过C++调用操作系统的线程接口,由操作系统创建一个线程,再由CPUrun(执行)这个线程;

同理Java调通stop或interrupt(),JVM通过C++调用操作系统的线程停止接口,再由CPU收到stop命令停止线程。

三、原子性问题的产生原因与解决方案

java 复制代码
package ch12_thread.class2;

/**
 * 测试线程的原子性
 */
public class CasExampleTest1 {
    private int i;
    public void incr(){
        i++;
    }
}
bash 复制代码
mac@MacdeMBP class2 % javap -v CasExampleTest1.class 
Classfile /Users/mac/IdeaProjects/OOM/JVMSample/src/main/java/ch12_thread/class2/CasExampleTest1.class
  Last modified 2024-1-11; size 309 bytes
  MD5 checksum 7baf13265d8f13f9acf4aacb6a6ef3b4
  Compiled from "CasExampleTest1.java"
public class ch12_thread.class2.CasExampleTest1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#14         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#15         // ch12_thread/class2/CasExampleTest1.i:I
   #3 = Class              #16            // ch12_thread/class2/CasExampleTest1
   #4 = Class              #17            // java/lang/Object
   #5 = Utf8               i
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               incr
  #12 = Utf8               SourceFile
  #13 = Utf8               CasExampleTest1.java
  #14 = NameAndType        #7:#8          // "<init>":()V
  #15 = NameAndType        #5:#6          // i:I
  #16 = Utf8               ch12_thread/class2/CasExampleTest1
  #17 = Utf8               java/lang/Object
{
  public ch12_thread.class2.CasExampleTest1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 6: 0

  public void incr();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
      LineNumberTable:
        line 9: 0
        line 10: 10
}
SourceFile: "CasExampleTest1.java"

改造成多线程代码

java 复制代码
package ch12_thread.class2;

public class AtomicExample {
    private int i = 0;

    public void incr() {
        i++;
    }

    public static void main(String[] args) throws InterruptedException {
        final AtomicExample atomicExample = new AtomicExample();
        Thread[] threads = new Thread[2];

        for(int j = 0; j < 2; j++){
            threads[j] = new Thread(() -> {
                for (int k = 0; k < 10000; k++){
                    atomicExample.incr();
                }
            });
            threads[j].start();
        }

        threads[0].join();
        threads[1].join();
        // 预期结果 20000
        System.out.println(atomicExample.i);
    }
}

运行结果:

结果13644与预期20000不符,CPU切换导致原子性问题。

修改代码, incr方法加同步锁

再次执行

执行结果与预期结果一致。

大白话:

(1)在早期32位,这时一个Long型数据非常大62位,会将数据分为低32位、高32位,最后合在一起,这时中间被打断,这个数据可能就不准确了,而CPU层保证内存操作原子性就是说CPU去读取数据保证内存操作的原子性,中间不会发生中断,保证数据读取准确。

(2)CPU和数据之间通过公共总线通信,当CPU-1读取变量时,给总线加锁,保证这个变量不能被其他CPU读取,当CPU-1操作完,将数据存回内存后,在放开总线锁,其他CPU继续访问操作。

(3)对Cache加锁,后续课程解释。

大白话:

临界区加锁,就是对临界区资源(数据)加锁,保证操作安全,操作完后解锁。缺点,耗时,不需要加锁的也加锁了,影响性能。

四、CAS与乐观锁原理

乐观锁 - 将数据比较判断提交到汇编层面执行,保证数据一致性,没有发生冲突继续执行,如果发生冲突再想办法解决。

前面的代码是通过加synchronized同步锁完成的,现在通过在不加锁使用原子类完成

五、可见性问题的本质

java 复制代码
package ch12_thread.class5;

public class VolatileExample {

    public static boolean stop = false;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
           int i = 0;
           while (!stop) {
               i++;
           }
            System.out.println("finish while ...");
        });

        t1.start();
        System.out.println("t1 start ...");
        Thread.sleep(1000);
        stop = true;
    }
}

上面的子线程是否收到stop变量的变化,并最终终止循环输入"finish while ..."

执行结果:

发现在主线程改变静态变量的值,子线程是看不到的变化的。

继续修改代码,给stop变量加上voliatile参数

运行结果:

造成这种情况的原因:

线程内部、CPU有缓存,当变量改变时,线程之间、CPU之间感知不到。

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值

这里涉及到MSEI缓存一致性协议,具体讲解见

白话MESI缓存一致性协议_msei-CSDN博客

六、顺序性问题的本质和valatile的源码实现原理screenflow

java 复制代码
package class6;

/**
 * 顺序性问题演示
 */
public class MemoryReorderingExample {
    private static int x = 0, y = 0;
    private static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        int count = 0;
        while (true) {
            x = 0;
            y = 0;
            a = 0;
            b = 0;

            count++;

            Thread t1 = new Thread(() -> {
                a = 1;
                x = b;
            });

            Thread t2 = new Thread(() -> {
                b = 1;
                y = b;
            });

            // 情况1.t1先执行,y=1, x=0
            // 情况2.t2先执行,x=1, y=0
            // 情况3.t1和t2同时执行,x=1, y=1

            t1.start();
            t2.start();
            t1.join();
            t2.join();

            if(x == 0- && y == 0){
                System.out.println("第" + count + "次 x=" + x + "y=" + y);
                break;
            }
        }
    }
}

运行结果:

正常情况下,不应该出现x==0&&y==0,但是实际测试结果,出现了x==0&&y==0。

为什么会出现这种情况呢,其实是因为出现了指令重排序问题。

当t1线程的x=b,从a=1代码的下面,移动到a=1代码的上面,且t2线程的y=a,从b=1代码的下面移动到b=1代码的上面,就会计算出x==0&&y==0,这种问题就是顺序性问题。

编译器优化场景举例:

上图这种情况,编译器认为左边代码太消耗资源,会自动优化成右边代码。

那么针对前面的这种重排序问题,怎么解决呢?

最简单的解决方法,是加锁!

多运行一些时间,没有发现问题

刚才的案例里,使用synchronized关键字,主要作用是加锁,最大缺点是性能低。

大白话:

java层面设置volatile,JVM转换成C++程序,C++通过操作系统操作一系列硬件指令,通过操作内存屏障保证顺序性问题,通过lock锁操作缓存行,保证不同的缓存之间是一致的。

七、Java里的对象到底是什么

大白话:

这里的加锁即synchronized,加锁后具体是哪种锁(偏向锁、轻量级锁、重量级锁) ,都有可能。

堆中对象能否使用,基于以下几种状态判断:

1.无锁 - 堆中的对象无锁,可以直接使用;

2.偏向锁 - 堆中对象被线程占用,对象的对象头会存在偏向锁,保存线程ID,别的线程发现这个对象中有线程信息了,被标记偏向锁,就不能使用了;

3.轻量级锁 - 对象竞争比较弱,就会采用轻量级锁;

4.重量级锁 - 好多线程都来访问同一个对象,对象竞争比较强,这个对象加重量级锁,第一个线程处理完了,第二个线程再使用,依次使用。

通过JOL查看Java对象信息

XML 复制代码
<dependencies>
    <dependency>
      <groupId>org.openjdk.jol</groupId>
      <artifactId>jol-core</artifactId>
      <version>0.9</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>
java 复制代码
package class6;

public class MyObject {
}
java 复制代码
package class6;

import org.openjdk.jol.info.ClassLayout;

// -XX:+UseCompressedOops 默认开启的压缩所有指针
// -XX:+UseCompressedClassPointers 默认开启的压缩对象头的类型执行Klass Pointer
// Oops : Ordinary Object Pointers
public class JOLSample {
    public static void main(String[] args) {
        ClassLayout layout = ClassLayout.parseInstance(new MyObject());
        System.out.println(layout.toPrintable());
    }
}

开启压缩后

占了12个字节,不够8的整数倍,还差4个字节,这四个字节,也就是说还可以放其他信息。

八、synchronized锁的状态与实现原理

1.无锁

对应

说明是无锁状态。

2.偏向锁

在同一时刻,有且只有一个线程执行了这个同步锁方法,而且并没有发生竞争的情况,这个时候锁的状态就是偏向锁。

对应

3.轻量级锁

已经发生多线程冲突了,但是不太严重,具体实现CAS(乐观锁)。

当没有多线程访问的状态,就是轻量级锁。

对应锁标志位00

4.重量级锁

很多线程访问对象时,对象已经被锁住,这时,其他线程也来抢占对象,则升级为重量级锁。

演示代码:

java 复制代码
package class6;

import org.openjdk.jol.info.ClassLayout;

import java.util.concurrent.TimeUnit;

public class HeavyLockExample {
    public static void main(String[] args) throws InterruptedException {
        final HeavyLockExample heavy = new HeavyLockExample();
        System.out.println("加锁之前");
        System.out.println(ClassLayout.parseInstance(heavy).toPrintable());
        Thread t1 = new Thread(() -> {
           synchronized (heavy){
               try{
                   TimeUnit.SECONDS.sleep(2);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        t1.start();
        //确保t1线程已经运行
        TimeUnit.MILLISECONDS.sleep(500);
        System.out.println("t1线程抢占了锁");
        System.out.println(ClassLayout.parseInstance(heavy).toPrintable());
        synchronized (heavy) {
            System.out.printf("main线程来抢占锁");
            System.out.printf(ClassLayout.parseInstance(heavy).toPrintable());
        }
//        System.gc();
//        System.out.printf(ClassLayout.parseInstance(heavy).toPrintable());
    }
}

一开始,heavy无锁

t1抢占后,heavy变为轻量级锁

main线程,再去抢占对象,变为重量级锁

注:偏向锁不太好演示出来。

相关推荐
Z_z在努力4 分钟前
【杂类】Spring 自动装配原理
java·spring·mybatis
程序员爱钓鱼15 分钟前
Go语言实战案例-开发一个Markdown转HTML工具
前端·后端·go
小小菜鸡ing31 分钟前
pymysql
java·服务器·数据库
getapi34 分钟前
shareId 的产生与传递链路
java
桦说编程44 分钟前
爆赞!完全认同!《软件设计的哲学》这本书深得我心
后端
thinktik1 小时前
还在手把手教AI写代码么? 让你的AWS Kiro AI IDE直接读飞书需求文档给你打工吧!
后端·serverless·aws
我没想到原来他们都是一堆坏人2 小时前
(未完待续...)如何编写一个用于构建python web项目镜像的dockerfile文件
java·前端·python
沙二原住民2 小时前
提升数据库性能的秘密武器:深入解析慢查询、连接池与Druid监控
java·数据库·oracle
三毛20042 小时前
玳瑁的嵌入式日记D33-0908(SQL数据库)
jvm·数据库·sql