• 0X JavaSE-- 并发编程()


    ThreadGroup

    线程组可以对线程进行批量控制。

    • 每个 Thread 必然存在于一个 ThreadGroup 中,Thread 不能独立于 ThreadGroup 存在。
    • 执行 main()方法的线程名字是 main。
    • 如果在 new Thread 时没有显式指定,那么默认将父线程(当前执行 new Thread 的线程)线程组设置为自己的线程组。
    hread testThread = new Thread(() -> {
        System.out.println("testThread当前线程组名字:" +
                Thread.currentThread().getThreadGroup().getName());
        System.out.println("testThread线程名字:" +
                Thread.currentThread().getName());
    });
    
    testThread.start();
    System.out.println("执行main所在线程的线程组名字: " + Thread.currentThread().getThreadGroup().getName());
    System.out.println("执行main方法线程名字:" + Thread.currentThread().getName());
    
    执行main所在线程的线程组名字: main
    testThread当前线程组名字:main
    testThread线程名字:Thread-0
    执行main方法线程名字:main
    
    • ThreadGroup 是一个标准的向下引用的树状结构,父 ThreadGroup 可以引用其子 ThreadGroup 和子线程,但子 ThreadGroup 和子线程不能直接引用父 ThreadGroup,也不允许平级引用。这样设计可以防止"上级"线程被"下级"线程引用而无法被 GC 回收。
    • 线程组可以起到统一控制线程的优先级和检查线程权限的作用。

    线程优先级

    • 线程优先级可以被人为指定,但 JVM只是给操作系统传达这个建议值,线程最终在操作系统中的优先级还是由操作系统决定。
      • 不过通常情况下,高优先级的线程将会比低优先级的线程有更高的概率得到执行。

    JMM Java内存模型

    Java 内存模型(Java Memory Model,JMM)定义了 Java 程序中的变量、线程如何和主存以及工作内存进行交互的规则。它主要涉及到多线程环境下的共享变量可见性、指令重排等问题,是理解并发编程中的关键概念。

    作用

    Java 线程之间的通信由 JMM 控制。

    Java内存模型和运行时内存区域

    Java 内存模型(JMM)和 Java 运行时内存区域的区别可大着呢。

    Java 内存模型(Java Memory Model,JMM)

    • 定义:Java 内存模型描述了多线程程序中变量(包括实例字段、静态字段和数组元素)如何从主内存写入和读取,以及如何在主内存和工作内存(线程本地内存)之间传递。
    • 主要内容:
      • 可见性(Visibility): 当一个线程修改了某个变量的值,其他线程是否能立即看到这个修改。、
      • 原子性(Atomicity): 一个操作是不可分割的,不会被其他线程的操作所中断。
      • 有序性(Ordering): 程序执行的顺序是否与代码中的顺序一致。
    • 几个关键:
      • 主内存和工作内存: 所有变量都存储在主内存中,每个线程还有自己的工作内存,线程对变量的所有操作(读取和赋值)都必须在工作内存中进行。
      • volatile 关键字: 保证变量的可见性和有序性,但不保证原子性。
      • synchronized 关键字: 保证进入临界区的线程对变量的可见性和原子性。
      • happens-before 原则: 如果一个操作 happens-before 另一个操作,那么第一个操作的结果对第二个操作是可见的,且第一个操作的执行顺序排在第二个操作之前。

    Java Runtime Memory Areas Java 运行时内存区域
    在这里插入图片描述

    • 定义:JVM 在运行时将内存分为若干个不同的区域,用来存储不同类型的数据。这些区域包括:
      • 程序计数器(Program Counter Register):每个线程都有一个独立的程序计数器,用于记录当前线程执行的字节码指令的地址。
      • Java 虚拟机栈(JVM Stack):每个线程都有一个独立的虚拟机栈,用于存储局部变量表、操作数栈、动态链接和方法出口等信息。
      • 本地方法栈(Native Method Stack):与 Java 虚拟机栈类似,不过它服务于虚拟机使用到的本地方法(如 JNI)。
      • (Heap):堆是所有线程共享的一块内存区域,几乎所有的对象实例都在这里分配。堆是垃圾收集器管理的主要区域,根据垃圾收集器的不同,可以细分为新生代(Eden、From Survivor、To Survivor)和老年代。
      • 方法区(Method Area):方法区是所有线程共享的一块内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器(JIT)编译后的代码等数据。在 JDK 8 之后,方法区称为元空间(Metaspace)。
      • 运行时常量池(Runtime Constant Pool):运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
    共享堆内存带来的问题

    对每一个线程来说,栈都是私有的,而堆是共有的。

    也就是说,在栈中的变量(局部变量、方法定义的参数、异常处理的参数)不会在线程之间共享,也就不会有内存可见性的问题,也不受内存模型的影响。而在堆中的变量是共享的,一般称之为共享变量。

    所以,内存可见性针对的是堆中的共享变量。

    在这里插入图片描述
    根据 JMM 的规定,线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主存中读取。
    因此,各线程在本地内存中修改了共享变量,必须再写回到主内存,其它线程才能看到,如果还没写回到主内存,其它线程就访问了这个共享变量,就会造成可见性问题。

    volatile关键字就是解决这个问题的。
    对于可能被多线程并发修改访问的共享变量,加上 volatile关键字后,能确保:在本地内存修改的共享变量,立刻在主内存中生效。
    用术语来说就是:保证多线程操作共享变量的可见性以及禁止指令重排序

    在更底层,JMM 通过内存屏障来实现内存的可见性以及禁止重排序。为了程序员更方便地理解,设计者提出了 happens-before 的概念(下文会细讲),它更加简单易懂,从而避免了程序员为了理解内存可见性而去学习复杂的重排序规则,以及这些规则的具体实现方法。

    指令重排序

    计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排。
    因为指令重排序可以提高性能。

    处理器中普遍采用流水线技术,
    但是流水线技术最害怕中断,恢复中断的代价是比较大的,所以我们要想尽办法不让流水线中断。

    指令重排就是减少中断的一种技术。

    • 指令重排一般分为以下三种:
      • 编译器优化重排。编译器在不改变单线程程序语义的前提下,重新安排语句的执行顺序。
      • 指令并行重排。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序。
      • 内存系统重排。由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。

    指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致。所以在多线程下,指令重排序可能会导致一些问题。

    程序可分为正确同步(使用 Volatile、Synchronized 等关键)、未正确同步的。

    • 对于已经正确同步的程序

      • 会改变程序执行结果的重排序,JMM 要求编译器和处理器都禁止这种重排序。
      • 不会改变程序执行结果的重排序,JMM 对编译器和处理器不做要求,允许这种重排序。
    • 对于未正确同步的程序

      • JMM 只提供最小安全性:线程读取到的值,要么是之前某个线程写入的值,要么是默认值,不会无中生有。(被后发线程先改变)

    并发编程的两个问题

    • 并发编程的线程之间存在两个问题:
      • 线程间如何通信?即:线程之间以何种机制来交换信息
      • 线程间如何同步?即:线程以何种机制来控制不同线程间发生的相对顺序
    • 有两种并发模型可以解决这两个问题:
      • 消息传递并发模型
      • 共享内存并发模型

    这两种模型之间的区别如下图所示:
    在这里插入图片描述

    Java 内存模型(JMM)定义了 Java 程序中的变量、线程如何和主存以及工作内存进行交互的规则。它主要涉及到多线程环境下的共享变量可见性、指令重排等问题,是理解并发编程中的关键概念。
    Java 内存模型(JMM)主要针对的是多线程环境下,如何在主内存与工作内存之间安全地执行操作。
    Java 运行时内存区域描述的是在 JVM 运行时,如何将内存划分为不同的区域,并且每个区域的功能和工作机制。主要包括方法区、堆、栈、本地方法栈、程序计数器。
    指令重排是为了提高 CPU 性能,但是可能会导致一些问题,比如多线程环境下的内存可见性问题。
    happens-before 规则是 JMM 提供的强大的内存可见性保证,只要遵循 happens-before 规则,那么我们写的程序就能保证在 JMM 中具有强的内存可见性。

    volatile

    • volatile 作用
      • 保证可见性:当写一个 volatile 变量时,JMM 会把该线程在本地内存中的变量强制刷新到主内存中去;
      • 禁止指令重排
      • 但不保证原子性:
    • volatile 的实际应用
      • 在单例模式中,双重检查锁定(Double-Checked Locking)使用 volatile 确保线程安全地创建单例实例。
      • volatile 适用于需要在多个线程间共享并及时更新的状态变量,例如计数器、标志变量。

    volatile 会禁止指令重排

    重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。
    使用 volatile 关键字修饰共享变量可以禁止这种重排序。

    当使用 volatile 关键字来修饰一个变量时,Java 内存模型会插入内存屏障(一个处理器指令,可以对 CPU 或编译器重排序做出约束)来确保以下两点:

    • 写屏障(Write Barrier):当一个 volatile变量被写入时,写屏障确保在该屏障之前的所有变量的写入操作都提交到主内存。
    • 读屏障(Read Barrier):当读取一个 volatile变量时,读屏障确保在该屏障之后的所有读操作都从主内存中读取。

    “也就是说,编译器和处理器执行到 volatile 变量时,必须将其前面的所有语句都必须执行完,后面所有得语句都未执行。且前面语句的结果对 volatile 变量及其后面语句可见。”

    public class VolatileExample {
        private volatile boolean running = true;
    
        public void start() {
            Thread worker = new Thread(() -> {
                while (running) {
                    // 执行一些工作
                }
                System.out.println("Thread stopped.");
            });
            worker.start();
        }
    
        public void stop() {
            running = false;
        }
    
        public static void main(String[] args) throws InterruptedException {
            VolatileExample example = new VolatileExample();
            example.start();
            
            Thread.sleep(1000); // 主线程等待一段时间
            example.stop();     // 停止工作线程
        }
    }
    

    volatile 不适用的场景

    下面是变量自加的示例:

    public class volatileTest {
        public volatile int inc = 0;
        public void increase() {
            inc++;
        }
        public static void main(String[] args) {
            final volatileTest test = new volatileTest();
            for(int i=0;i<10;i++){
                new Thread(){
                    public void run() {
                        for(int j=0;j<1000;j++)
                            test.increase();
                    };
                }.start();
            }
            while(Thread.activeCount()>1)  //保证前面的线程都执行完
                Thread.yield();
            System.out.println("inc output:" + test.inc);
        }
    }
    

    测试输出:

    inc output:8182
    “为什么呀?二哥?” 看到这个结果,三妹疑惑地问。

    “因为 inc++不是一个原子性操作(前面讲过),由读取、加、赋值 3 步组成,所以结果并不能达到 10000。”我耐心地回答。

    “哦,你这样说我就理解了。”三妹点点头。

    怎么解决呢?

    01、采用 synchronized(下一篇会讲,戳链接直达),把 inc++ 拎出来单独加 synchronized 关键字:

    public class volatileTest1 {
    public int inc = 0;
    public synchronized void increase() {
    inc++;
    }
    public static void main(String[] args) {
    final volatileTest1 test = new volatileTest1();
    for(int i=0;i<10;i++){
    new Thread(){
    public void run() {
    for(int j=0;j<1000;j++)
    test.increase();
    };
    }.start();
    }
    while(Thread.activeCount()>1) //保证前面的线程都执行完
    Thread.yield();
    System.out.println(“add synchronized, inc output:” + test.inc);
    }
    }
    02、采用 Lock,通过重入锁 ReentrantLock 对 inc++ 加锁(后面都会细讲,戳链接直达):

    public class volatileTest2 {
    public int inc = 0;
    Lock lock = new ReentrantLock();
    public void increase() {
    lock.lock();
    inc++;
    lock.unlock();
    }
    public static void main(String[] args) {
    final volatileTest2 test = new volatileTest2();
    for(int i=0;i<10;i++){
    new Thread(){
    public void run() {
    for(int j=0;j<1000;j++)
    test.increase();
    };
    }.start();
    }
    while(Thread.activeCount()>1) //保证前面的线程都执行完
    Thread.yield();
    System.out.println(“add lock, inc output:” + test.inc);
    }
    }
    03、采用原子类 AtomicInteger(后面也会细讲,戳链接直达)来实现:

    public class volatileTest3 {
    public AtomicInteger inc = new AtomicInteger();
    public void increase() {
    inc.getAndIncrement();
    }
    public static void main(String[] args) {
    final volatileTest3 test = new volatileTest3();
    for(int i=0;i<10;i++){
    new Thread(){
    public void run() {
    for(int j=0;j<100;j++)
    test.increase();
    };
    }.start();
    }
    while(Thread.activeCount()>1) //保证前面的线程都执行完
    Thread.yield();
    System.out.println(“add AtomicInteger, inc output:” + test.inc);
    }
    }
    三者输出都是 1000,如下:

    add synchronized, inc output:1000
    add lock, inc output:1000
    add AtomicInteger, inc output:1000
    #volatile 实现单例模式的双重锁
    下面是一个使用"双重检查锁定"(double-checked locking)实现的单例模式(Singleton Pattern)的例子。

    public class Penguin {
    private static volatile Penguin m_penguin = null;

    // 一个成员变量 money
    private int money = 10000;
    
    // 避免通过 new 初始化对象,构造方法应为 public 或 private
    private Penguin() {}
    
    public void beating() {
        System.out.println("打豆豆" + money);
    }
    
    public static Penguin getInstance() {
        if (m_penguin == null) {
            synchronized (Penguin.class) {
                if (m_penguin == null) {
                    m_penguin = new Penguin();
                }
            }
        }
        return m_penguin;
    }
    

    }
    在这个例子中,Penguin 类只能被实例化一次。来看代码解释:

    声明了一个类型为 Penguin 的 volatile 变量 m_penguin,它是类的静态变量,用来存储 Penguin 类的唯一实例。
    Penguin() 构造方法被声明为 private,这样就阻止了外部代码使用 new 来创建 Penguin 实例,保证了只能通过 getInstance() 方法获取实例。
    getInstance() 方法是获取 Penguin 类唯一实例的公共静态方法。
    第一次 if (null == m_penguin) 检查是否已经存在 Penguin 实例。如果不存在,才进入同步代码块。
    synchronized(penguin.class) 对类的 Class 对象加锁,确保在多线程环境下,同时只能有一个线程进入同步代码块。在同步代码块中,再次执行 if (null == m_penguin) 检查实例是否已经存在,如果不存在,则创建新的实例。这就是所谓的“双重检查锁定”,一共两次。
    最后返回 m_penguin,也就是 Penguin 的唯一实例。
    其中,使用 volatile 关键字是为了防止 m_penguin = new Penguin() 这一步被指令重排序。因为实际上,new Penguin() 这一行代码分为三个子步骤:

    步骤 1:为 Penguin 对象分配足够的内存空间,伪代码 memory = allocate()。
    步骤 2:调用 Penguin 的构造方法,初始化对象的成员变量,伪代码 ctorInstanc(memory)。
    步骤 3:将内存地址赋值给 m_penguin 变量,使其指向新创建的对象,伪代码 instance = memory。
    如果不使用 volatile 关键字,JVM 可能会对这三个子步骤进行指令重排。

    为 Penguin 对象分配内存
    将对象赋值给引用 m_penguin
    调用构造方法初始化成员变量
    这种重排序会导致 m_penguin 引用在对象完全初始化之前就被其他线程访问到。具体来说,如果一个线程执行到步骤 2 并设置了 m_penguin 的引用,但尚未完成对象的初始化,这时另一个线程可能会看到一个“半初始化”的 Penguin 对象。

    假如此时有两个线程 A 和 B,要执行 getInstance() 方法:

    public static Penguin getInstance() {
    if (m_penguin == null) {
    synchronized (Penguin.class) {
    if (m_penguin == null) {
    m_penguin = new Penguin();
    }
    }
    }
    return m_penguin;
    }
    线程 A 执行到 if (m_penguin == null),判断为 true,进入同步块。
    线程 B 执行到 if (m_penguin == null),判断为 true,进入同步块。
    如果线程 A 执行 m_penguin = new Penguin() 时发生指令重排序:

    线程 A 分配内存并设置引用,但尚未调用构造方法完成初始化。
    线程 B 此时判断 m_penguin != null,直接返回这个“半初始化”的对象。
    这样就会导致线程 B 拿到一个不完整的 Penguin 对象,可能会出现空指针异常或者其他问题。

    于是,我们可以为 m_penguin 变量添加 volatile 关键字,来禁止指令重排序,确保对象的初始化完成后再将其赋值给 m_penguin。

    #小结
    “好了,三妹,我们来总结一下。”我舒了一口气说。

    volatile 可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在 JVM 底层 volatile 是采用“内存屏障”来实现的。

    观察加入 volatile 关键字和没有加入 volatile 关键字时所生成的汇编代码就能发现,加入 volatile 关键字时,会多出一个 lock 前缀指令,lock 前缀指令实际上相当于一个内存屏障(也称内存栅栏),内存屏障会提供 3 个功能:

    它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
    它会强制将对缓存的修改操作立即写入主存;
    如果是写操作,它会导致其他 CPU 中对应的缓存行无效。
    最后,我们学习了 volatile 不适用的场景,以及解决的方法,并解释了双重检查锁定实现的单例模式为何需要使用 volatile。

    synchronized

    synchronized 可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到 synchronized 的另外一个重要的作用,synchronized 可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代 volatile 功能)。

    synchronized 关键字最主要有以下 3 种应用方式:

    同步方法,为当前对象(this)加锁,进入同步代码前要获得当前对象的锁;
    同步静态方法,为当前类加锁(锁的是 Class 对象),进入同步代码前要获得当前类的锁;
    同步代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

    synchronized 同步方法

    synchronized 同步方法

    synchronized 同步方法

  • 相关阅读:
    打印字符串,数组,对象,函数的原型方法
    Git从入门到项目实战,一篇文章吃透Git
    2022宁夏杯B题思路分析+代码(大学生就业问题分析)
    C/C++实现简单高并发http服务器
    A Philosophy of Software Design读书笔记——模块的接口要通用,实现要深入
    Python-语句
    electron实战之Electron+Vue+Vite+ElementPlus操作本地配置文件
    Memtester框架是什么
    Docker | docker容器导出以及常见问题的处理
    PostgreSQL — 安装及常用命令
  • 原文地址:https://blog.csdn.net/m0_46671240/article/details/139886707