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线程,再去抢占对象,变为重量级锁

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

相关推荐
魔道不误砍柴功2 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2342 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨2 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
Chrikk3 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*3 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue3 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man3 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
测开小菜鸟3 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
P.H. Infinity4 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天4 小时前
java的threadlocal为何内存泄漏
java