文章目录
【JUC并发编程系列】深入理解Java并发机制:从用户态到内核态的探索(一、前置知识)
1.用户态与内核态区别
内核态(Kernel Mode):运行操作系统程序,操作硬件 (内核空间)
用户态(User Mode):运行用户程序(用户空间)
为了安全应用程序无法直接调用的硬件的功能,而是将这些功能封装成特定的函数。当应用程序需要硬件功能时(例如读写文件),就需要进行系统调用。当进程进行系统调用后就从用户态装换为内核态。
比如:线程的调度需要内核态调度实现
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:
- atomic_t count; //指示互斥锁的状态:
1:没有上锁,可以获得;
0:被锁定,不能获得。
负数:被锁定,且可能在该锁上有等待进程 ,初始化为没有上锁。
-
spinlock_t wait_lock; //等待获取互斥锁中使用的自旋锁。在获取互斥锁的过程中,操作会在自旋锁的保护中进行。初始化为为锁定。
-
struct list_head wait_list; //等待互斥锁的进程队列。
Synchronized 获取锁的流程:需要经历 用户态与内核态切换
唤醒他:内核态阻塞—唤醒 切换用户态重新cpu调度。
申请锁时,从用户态进入内核态,申请到后从内核态返回用户态(两次切换);没有申请到时阻塞睡眠在内核态。使用完资源后释放锁,从用户态进入内核态,唤醒阻塞等待锁的进程,返回用户态(又两次切换);被唤醒进程在内核态申请到锁,返回用户态(可能其他申请锁的进程又要阻塞)。所以,使用一次锁,包括申请,持有到释放,当前进程要进行四次用户态与内核态的切换。同时,其他竞争锁的进程在这个过程中也要进行一次切换。
CPU通过分配时间片来执行任务,一个CPU在一个时刻只能运行一个线程,当一个任务的时间片用完(时间片耗尽或出现阻塞等情况),CPU会转去执行另外一个线程切换到另一个任务。在切换之前会保存上一个任务的状态(当前线程的任务可能并没有执行完毕,所以在进行切换时需要保存线程的运行状态),当下次再重新切换到该任务,就会继续切换之前的状态运行。——任务从保存到再加载的过程就是一次上下文切换
上下文切换只能发生在内核态中。内核态是 CPU 的一种有特权的模式,在这种模式下只有内核运行并且可以访问所有内存和其他系统资源。其他的程序,如应用程序,在最开始都是运行在用户态,但是他们能通过系统调用来运行部分内核的代码
而CAS 是在用户态完成而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少 。
4. 发生CPU上下文切换的原因
-
通过调用下列方法会导致自发性上下文切换:
Thread.sleep()
Object.wait()
Thread.yeild()
Thread.join()
LockSupport.park()
-
发生下列情况可能导致非自发性上下文切换:
- 切出线程的时间片用完
- 有一个比切出线程优先级更高的线程需要被运行
- 虚拟机的垃圾回收动作
5. 如何避免上下文切换
- 无锁并发编程。多线程竞争锁时,多线程竞争锁时,加锁、释放锁会导致比较多的上下文切换;
- CAS算法,Java的Atomic包使用CAS算法来更新数据,而不需要加锁,可能会消耗cpu资源。
- 使用最少线程,避免创建不需要的线程;
- 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
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算法实现线程安全的数据更新。
- 基本类型原子类: 如
AtomicInteger
、AtomicLong
等。 - 数组原子类: 如
AtomicIntegerArray
、AtomicLongArray
等。 - 引用类型原子类: 如
AtomicReference
、AtomicStampedReference
等。 - 字段更新器: 如
AtomicIntegerFieldUpdater
、AtomicLongFieldUpdater
等。
- 基本类型原子类: 如
- 底层实现: 原子类通常基于
Unsafe
类实现,通过反射获取Unsafe
实例,并使用它的方法来实现CAS操作。
6.6 上下文切换
- 发生原因: 上下文切换通常发生在线程的时间片结束、更高优先级的线程到达或者线程主动让出CPU时。
- 避免方法:
- 使用无锁编程技术。
- 使用CAS算法减少锁的使用。
- 减少线程数量,避免不必要的线程创建。
- 使用协程技术来实现更高效的并发控制。