【JUC并发编程系列】深入理解Java并发机制:从用户态到内核态的探索(一、前置知识)

【JUC并发编程系列】深入理解Java并发机制:从用户态到内核态的探索(一、前置知识)

1.用户态与内核态区别

内核态(Kernel Mode):运行操作系统程序,操作硬件 (内核空间)

用户态(User Mode):运行用户程序(用户空间)

为了安全应用程序无法直接调用的硬件的功能,而是将这些功能封装成特定的函数。当应用程序需要硬件功能时(例如读写文件),就需要进行系统调用。当进程进行系统调用后就从用户态装换为内核态。

img

比如:线程的调度需要内核态调度实现

Linux使用了Ring3级别表示用户态,Ring0标识内核态

Ring0作为内核态,没有使用Ring1和Ring2。

Ring3状态不能访问Ring0的地址 空间,包括代码和数据。、

2. 线程安全同步的方式

  • 阻塞式:synchronized/ lock(aqs) 当前线程如果没有获取到锁,当前的线程就会被阻塞等待。

  • 非阻塞式:CAS 当前线程如果没有获取到锁,不会阻塞等待 一直不断重试

T1线程获取到锁 持久时间 几毫秒 1毫秒 唤醒t2线程

T2线程没有获取到锁 直接阻塞等待

阻塞----就绪状态—cpu调度

唤醒t2线程

如果没有获取到锁,当前的线程就会被阻塞等待------重量级锁

CAS 运行用户态

3. 传统锁有哪些缺点

在使用synchronized1.0版本 如果没有获取到锁的情况下则当前线程直接会变为阻塞的

状态----升级为重量级锁,在后期唤醒的成本会非常高。

C A S ----运行用户态 缺点:cpu飙高

偏向锁 →轻量级锁(cas)→重量级锁。

轻量级都是在用户态完成;

重量级需要用户态与内核态切换;

因为:重量级锁需要通过操作系统自身的互斥量(mutex lock,也称为互斥锁)来实现,然而这种实现方式需要通过用户态和核心态的切换来实现,但这个切换的过程会带来很大的性能开销,比如:Linux内核互斥锁–mutex。

Linux内核互斥锁mutex lock:

  1. atomic_t count; //指示互斥锁的状态:

​ 1:没有上锁,可以获得;

​ 0:被锁定,不能获得。

​ 负数:被锁定,且可能在该锁上有等待进程 ,初始化为没有上锁。

  1. spinlock_t wait_lock; //等待获取互斥锁中使用的自旋锁。在获取互斥锁的过程中,操作会在自旋锁的保护中进行。初始化为为锁定。

  2. struct list_head wait_list; //等待互斥锁的进程队列。

Synchronized 获取锁的流程:需要经历 用户态与内核态切换

唤醒他:内核态阻塞—唤醒 切换用户态重新cpu调度。

申请锁时,从用户态进入内核态,申请到后从内核态返回用户态(两次切换);没有申请到时阻塞睡眠在内核态。使用完资源后释放锁,从用户态进入内核态,唤醒阻塞等待锁的进程,返回用户态(又两次切换);被唤醒进程在内核态申请到锁,返回用户态(可能其他申请锁的进程又要阻塞)。所以,使用一次锁,包括申请,持有到释放,当前进程要进行四次用户态与内核态的切换。同时,其他竞争锁的进程在这个过程中也要进行一次切换。

CPU通过分配时间片来执行任务,一个CPU在一个时刻只能运行一个线程,当一个任务的时间片用完(时间片耗尽或出现阻塞等情况),CPU会转去执行另外一个线程切换到另一个任务。在切换之前会保存上一个任务的状态(当前线程的任务可能并没有执行完毕,所以在进行切换时需要保存线程的运行状态),当下次再重新切换到该任务,就会继续切换之前的状态运行。——任务从保存到再加载的过程就是一次上下文切换

上下文切换只能发生在内核态中。内核态是 CPU 的一种有特权的模式,在这种模式下只有内核运行并且可以访问所有内存和其他系统资源。其他的程序,如应用程序,在最开始都是运行在用户态,但是他们能通过系统调用来运行部分内核的代码

而CAS 是在用户态完成而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少 。

4. 发生CPU上下文切换的原因

  1. 通过调用下列方法会导致自发性上下文切换:

    • Thread.sleep()
    • Object.wait()
    • Thread.yeild()
    • Thread.join()
    • LockSupport.park()
  2. 发生下列情况可能导致非自发性上下文切换:

    • 切出线程的时间片用完
    • 有一个比切出线程优先级更高的线程需要被运行
    • 虚拟机的垃圾回收动作

5. 如何避免上下文切换

  1. 无锁并发编程。多线程竞争锁时,多线程竞争锁时,加锁、释放锁会导致比较多的上下文切换;
  2. CAS算法,Java的Atomic包使用CAS算法来更新数据,而不需要加锁,可能会消耗cpu资源。
  3. 使用最少线程,避免创建不需要的线程;
  4. 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

6. 详细总结

6.1 用户态与内核态

  • 内核态(Kernel Mode): 内核态是指操作系统运行的环境,具有最高权限,可以直接访问硬件资源,执行关键操作,如管理内存、处理器和外设等。在内核态下,操作系统可以执行任何指令。
  • 用户态(User Mode): 用户态是指应用程序运行的环境,权限较低,不能直接访问硬件资源。应用程序必须通过系统调用请求内核来执行特定操作,如文件读写、网络通信等。

当应用程序需要执行特定的硬件操作时,如读写文件或访问设备,它需要从用户态发起系统调用进入内核态。系统调用完成后,程序会回到用户态继续执行。

6.2 线程安全同步方式

  • 阻塞式同步: 使用synchronized关键字或基于AQS的锁机制实现。当一个线程试图获取一个已经被占用的锁时,该线程会被阻塞,直到锁被释放。
  • 非阻塞式同步: 使用CAS(Compare and Swap)算法实现。当线程尝试获取锁时,如果锁不可用,则线程不会被阻塞,而是继续重试直到成功。

6.3 传统锁的缺点

  • 重量级锁: 在早期的JVM实现中,如synchronized 1.0版本,如果线程无法获取锁,会直接进入阻塞状态,这会导致线程状态的转换成本较高,尤其是在锁释放后的唤醒成本。
  • 上下文切换: 重量级锁涉及从用户态到内核态的转换,这会触发上下文切换,进而影响性能。

6.4 CAS算法

  • CAS算法: CAS是一种非阻塞算法,用于实现原子操作。它由三个操作数组成:内存值(V)、旧的预期值(E)和要修改的新值(N)。当且仅当预期值E等于内存值V时,才会将内存值V更新为N。
  • 优点: CAS算法运行在用户态,不需要进入内核态,减少了上下文切换的开销。
  • 缺点: 在某些情况下可能会导致CPU空转,从而消耗大量CPU资源。

6.5 原子类(Atomic类)

  • 原子类: Java并发包提供了一系列原子类,它们利用CAS算法实现线程安全的数据更新。
    • 基本类型原子类: 如AtomicIntegerAtomicLong等。
    • 数组原子类: 如AtomicIntegerArrayAtomicLongArray等。
    • 引用类型原子类: 如AtomicReferenceAtomicStampedReference等。
    • 字段更新器: 如AtomicIntegerFieldUpdaterAtomicLongFieldUpdater等。
  • 底层实现: 原子类通常基于Unsafe类实现,通过反射获取Unsafe实例,并使用它的方法来实现CAS操作。

6.6 上下文切换

  • 发生原因: 上下文切换通常发生在线程的时间片结束、更高优先级的线程到达或者线程主动让出CPU时。
  • 避免方法:
    • 使用无锁编程技术。
    • 使用CAS算法减少锁的使用。
    • 减少线程数量,避免不必要的线程创建。
    • 使用协程技术来实现更高效的并发控制。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值