当前位置: 首页 > news >正文

八股学习-JUC java并发编程

本文仅供个人学习使用,参考资料:JMM(Java 内存模型)详解 | JavaGuide 

线程基础概念

用户线程:由用户空间程序管理和调度的线程,运行在用户空间。

内核线程:由操作系统内核管理和调度的线程,运行在内核空间。

二者的区别和特点:用户献线程创建和切换成本低,但不可以利用多核,内核态线程,创建和切换成本高,可以利用多核。

jdk1.2之后的线程都是操作系统的线程,即基于原生线程实现(Native Threads)。

线程模型:一对一,多对一,多对多,这里就不说了。

在Windows和Linux中,java线程采用的都是一对一的模型。

线程和进程的区别:

线程是进程划分为更小的运行单位。线程和进程的最大的不同之处在于基本上各进程是独立的,而各线程不一定,同一进程中的线程极有可能会相互影响。线程执行开销小,但是不利于资源的管理和保护;而进程正相反。

创建线程的方式:严格来说,java只用一种方式可以创建线程:new Thread().start()

什么时候会发生线程的上下文切换?

  • 主动让出 CPU,比如调用了 sleep(), wait() 等。
  • 时间片用完。
  • 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
  • 被终止或结束运行

Thread.sleep()和Object.wait()方法对比

这两个方法都能暂停线程的执行,

  • Thread.sleep():定义在 Thread 类中,是一个静态方法。

  • Object.wait():定义在 Object 类中,是一个实例方法。

Thread.sleep()方法能让当前线程暂停执行指定的时间,进入(Timed_watiting状态)不释放任何锁资源,主要用于延迟执行或定时任务。

Object.wait()让当前线程进入等待状态,进入waiting或者Timed_waiting状态,必须持有对象的监视器锁*(即在synchronized块中调用),调用后会释放锁。主要用于线程间通信,等待其他线程通过notify()或者notifyAll()唤醒。

对于object.wait()的理解:

想象你和朋友合租,共用卫生间(共享资源):

  1. synchronized(lock):卫生间的门锁,一次只能一个人用。

  2. lock.wait():你进去后发现没纸了,于是出来并把钥匙挂回门口(释放锁),坐在沙发上等(等待)。

  3. lock.notify():室友买了纸后喊一声“有纸了!”,你听到后可以去抢钥匙。

这样设计保证了安全和效率!

可以直接调用Thread类中的run方法吗?

当直接执行run()方法中的内容时,会把run()方法当成一个main线程下普通方法,这并不是真正的多线程工作,只有new一个Thread,然后线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。

单核cpu支持java多线程吗?怎么实现的?

单核 CPU 是支持 Java 多线程的。操作系统通过时间片轮转的方式,将 CPU 的时间分配给不同的线程。尽管单核 CPU 一次只能执行一个任务,但通过快速在多个线程之间切换,可以让用户感觉多个任务是同时进行的。

os主要通过两种线程调度方式来管理多线程的执行:

  • 抢占式调度(Preemptive Scheduling):操作系统决定何时暂停当前正在运行的线程,并切换到另一个线程执行。(时间片轮转,公平性较好,cpu利用率高)
  • 协同式调度(Cooperative Scheduling):线程执行完毕后,主动通知系统切换到另一个线程。这种方式可以减少上下文切换带来的性能开销,但公平性较差,容易阻塞。

使用多线程可能带来什么问题?

内存泄露,死锁,线程不安全等等。

死锁的四个条件:

互斥条件,请求与保持条件,不剥夺条件,循环等待条件。

如何预防死锁?

破坏请求与保持:一次性申请所有资源

破坏不剥夺:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放

破坏循环等待:通过按序申请资源,释放资源则反序释放。

如何避免死锁?

银行家算法评估。

JAVA内存模型(JMM)

JMM主要定义了对于一个共享变量,当另一个线程对这个共享变量执行写操作后,这个线程对这个共享变量的可见性。

问题引出:

现代编译器在单线程下会对指令进行重排序来优化性能,但是没有义务保证多线程间的语义也一致。

常见的指令重排序有两种:

  • 编译器优化重排:编译器(包括 JVM、JIT 编译器等)在不改变单线程程序语义的前提下,重新安排语句的执行顺序。
  • 指令并行重排:现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

对着两种重排序的处理方式也不一样:

  • 对于编译器,通过禁止特定类型的编译器重排序的方式来禁止重排序。

  • 对于处理器,通过插入内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)的方式来禁止特定类型的处理器重排序。

内存屏障是一种 CPU 指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障指令执行的有序性。另外,为了达到屏障的效果,它会在处理器写入值时,强制将写缓冲区中的数据刷新到主内存;在读取值之前,使处理器本地缓存中的相关数据失效,强制从主内存中加载最新值,从而保障变量的可见性。

什么是JMM?

可以把 JMM 看作是 Java 定义的并发编程相关的一组规范,除了抽象了线程和主内存之间的关系之外,其还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。

说白了就是定义了一些规范来解决这些问题,开发者可以利用这些规范更方便地开发多线程程序。对于 Java 开发者说,你不需要了解底层原理,直接使用并发相关的一些关键字和类(比如 volatilesynchronized、各种 Lock)即可开发出并发安全的程序。

java如何抽象线程和内存之间的关系

在现有的java内存模型下,线程可以把变量保存到本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写,这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。

给出JMM的抽象示意图:

主内存vs本地内存

主内存(Main Memory)
  • 是什么:所有线程共享的内存区域,存储全局变量(如堆中的对象、静态变量等)。

  • 特点

    • 线程间共享,所有线程都能“看到”主内存中的数据。

    • 速度慢:访问主内存需要经过总线、缓存等,效率较低。

本地内存(Local Memory)
  • 是什么:每个线程独有的内存区域(实际是 JMM 的抽象概念,可能对应 CPU 缓存、寄存器等)。

  • 特点

    • 线程私有,其他线程无法直接访问。

    • 速度快:本地内存是线程的“工作副本”,用于缓存主内存中的数据。

    • 线程对变量的操作(读/写)优先在本地内存中进行,之后才会同步到主内存。

为什么会出现本地内存?

  • 性能优化:直接操作主内存太慢,本地内存(如 CPU 缓存)能大幅提升线程运行速度。

  • 副作用:本地内存的缓存机制会导致线程间数据不一致(需要开发者处理)。

然而由于二者的存在可能导致可见性和原子性的问题

可见性问题:

public class VisibilityProblem {
    private static boolean flag = true; // 主内存中的变量

    public static void main(String[] args) {
        // 线程 A:1秒后修改 flag
        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = false; // 修改后可能只更新了本地内存,未同步到主内存
        }).start();

        // 线程 B:循环检测 flag
        new Thread(() -> {
            while (flag) { // 可能一直读取本地内存中的旧值
                // 空循环
            }
            System.out.println("线程 B 检测到 flag 已修改");
        }).start();
    }
}

结果可能:线程 B 永远无法退出循环,因为它读取的一直是自己本地内存中的旧值 flag = true

如何解决可见性问题:

方法 1:使用 volatile 关键字
  • 强制变量的读写直接操作主内存,跳过本地内存。

  • 修改示例代码:

    private static volatile boolean flag = true; // 添加 volatile
方法 2:使用 synchronized 同步块
  • 进入同步块时,会清空本地内存,从主内存重新加载变量。

  • 退出同步块时,会将本地内存的修改强制写回主内存。

并发编程的三个特性:

原子性:一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。(synchronized锁和各种lock)

可见性:

当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。

在 Java 中,可以借助synchronizedvolatile 以及各种 Lock 实现可见性。

如果我们将变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。

有序性:

由于指令重排序问题,代码的执行顺序未必就是编写代码时候的顺序。

我们上面讲重排序的时候也提到过:

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

在 Java 中,volatile 关键字可以禁止指令进行重排序优化。

Volatile关键字

如果一个变量被volatile关键字声明了,那么在java内存模型中读取它时就变成了这样的方式:

一个变量如果用volatile关键字修饰,就能保证数据的可见性,但是不能保证数据的原子性,synchronized关键字二者都能保证。

如何禁止指令重排序

在 Java 中,volatile 关键字除了可以保证变量的可见性,还有一个重要的作用就是防止 JVM 的指令重排序。 如果我们将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。

在 Java 中,Unsafe 类提供了三个开箱即用的内存屏障相关的方法,屏蔽了操作系统底层的差异:

public native void loadFence();
public native void storeFence();
public native void fullFence();

通过这三个指令也能实现和volatile禁止重排序一样的效果,只是很麻烦。

这里给出一个例题,来自javaguide,同时也是我快手日常实习一面挂掉的一道题

解释并手写一下双重检验锁方式实现单例模式

代码:

public class Singleton{
    private volatile static Singleton uniqueInstance;
    private Singleton(){   
    }
    public static Singleton getUniqueInstance(){
        if(uniqueInstance==null){
            synchronized(Singleton.class){
                if(uniqueInstance == null){
                    uniqueInstance =new Sngleton();
                }
            }
        }
        return uniqueInstance;
    }

uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

  1. uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. uniqueInstance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

虽然这个volatile能保证多线程环境下变量的可见性,但是保证不了变量的原子性,只有synchronized锁和ReentranLock锁才能保证原子性和可见性。

悲观锁和乐观锁

悲观锁:每次获取资源都要上锁,其他线程想要拿到资源就要阻塞,直至锁被释放。共享资源每次只给一个线程使用,其他线程阻塞,用完后再把资源转让给其他线程。

典型代表:synchronized和ReentranLock锁。

问题:高并发的场景下激烈的锁竞争会造成线程阻塞,导致频繁的上下文切换,增加系统的性能开销。还存在死锁问题。

乐观锁:乐观锁不加锁,不停的执行,只是在提交修改的时候去验证对应的资源是否被其他线程修改了

典型代表:版本号机制或者CAS算法

在 Java 中java.util.concurrent.atomic包下面的原子变量类(比如AtomicIntegerLongAdder)就是使用了乐观锁的一种实现方式 CAS 实现的。

问题:乐观锁相对悲观锁不存在锁竞争造成阻塞的问题,但是在写占比非常多的时候,会频繁的失败和重试,这样也会非常影响性能。

综上,悲观锁适用于写比较多的情况,乐观锁适用于读比较多的情况。

乐观锁的实现方式:

版本号机制和CAS算法(java并没有直接实现CAS,CAS相关的实现是通过C++内联汇编的形式实现的,JNI调用)

sun.misc包下的Unsafe类提供了compareAndSwapObjectcompareAndSwapIntcompareAndSwapLong方法来实现的对Objectintlong类型的 CAS 操作

Java中如何实现CAS?

使用Unsafe类,AtomicInteger的底层实现就是利用了Unsafe类提供的方法。

CAS操作可能会因为并发冲突而失败,因此通常会与while循环搭配使用,在失败后不断重试,直到操作成功。这就是 自旋锁机制 。

CAS的ABA问题怎么解决?

ABA问题的解决思路是在变量前面追加上版本号或者时间戳。

如果版本号和预期值都相等,那么就可以更新。

CAS的缺点:

CAS会采用自旋操作来进行重试,也就是不成功就一直循环直到执行成功,会带来很大的CPU开销

CAS操作只对单个共享变量有效,当需要操作多个共享变量时,CAS就无能为力,jdk1.5开始提供了AtomicReference类,通过将多个变量封装在一个对象中,我们可以使用AtomicReference来执行 CAS 操作。

除了 AtomicReference 这种方式之外,还可以利用加锁来保证。

synchronized关键字

synchronized锁的应用

synchronized 是 Java 中的一个关键字,翻译成中文是同步的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

synchronized锁的主要使用方式有以下三种:

1.修饰实例方法(锁当前的对象实例)

synchronized void method() {
    //业务代码
}

进入同步代码前,要获得当前对象实例的锁

2.修饰静态方法(锁当前类)

synchronized static void method() {
    //业务代码
}

给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁

3.修饰代码块

synchronized(this) {
    //业务代码
}
  • synchronized(object) 表示进入同步代码库前要获得 给定对象的锁
  • synchronized(类.class) 表示进入同步代码前要获得 给定 Class 的锁

注意构造方法不能使用synchronized关键字修饰,不过可以在构造方法内部使用synchronized。


                

相关文章:

  • svn-1.7.22安装
  • ESP8266通过AT指令配置双向透传
  • C++常用多线程模式
  • 【病毒分析】伪造微软官网+勒索加密+支付威胁,CTF中勒索病毒解密题目真实还原!
  • 机器学习面试重点第二部分(动画版)
  • 服务创造未来 东隆科技携多款产品亮相慕尼黑
  • Idea中使用Git插件_合并当前分支到master分支_冲突解决_很简单---Git工作笔记005
  • Debezium + Kafka-connect 实现Postgres实时同步Hologres
  • Spring Boot配置与注解的使用
  • Leetcode 1277. 统计全为 1 的正方形子矩阵 动态规划
  • 【C++】动态规划从入门到精通
  • PH2D数据集: 用人类演示数据提升人形机器人操作能力,助力跨实体学习
  • Java并发(知识整理)
  • 在 Hugging Face Spaces 上使用 Gradio 免费运行 ComfyUI 工作流
  • 前后端Vue 跨越端口问题解决
  • Ollama + Open WebUI 本地部署DeepSeek
  • vue+echarts实现饼图组件(实现左右联动并且数据量大时可滚动)
  • MongoDB慢日志查询及索引创建
  • Vim每行末尾添加字符方法
  • django+vue3实现前后端大文件分片下载
  • 安庆市重点工程建设局网站/广州百度
  • 运营网站费用/情感营销案例
  • 网站建设岗位招聘/北京疫情最新消息情况
  • 苏州专业做网站比较好的公司/个人网站建站流程
  • wordpress复制数据库/福州网站优化公司
  • 无极网站/爱站