Java并发中的上下文切换、死锁、资源限制

在Java并发编程中,上下文切换、死锁和资源限制是开发者经常需要面对的问题。这些问题不仅会影响程序的性能,还可能导致程序无法正常运行。本文将深入探讨这些问题的原理、影响以及如何在实际开发中避免或解决它们。

目录

[1. 上下文切换(Context Switching)](#1. 上下文切换(Context Switching))

[1.1 什么是上下文切换?](#1.1 什么是上下文切换?)

[1.2 上下文切换的代价](#1.2 上下文切换的代价)

[1.3 如何减少上下文切换?](#1.3 如何减少上下文切换?)

示例:线程池减少上下文切换

多线程一定比单线程快吗?

[2. 死锁(Deadlock)](#2. 死锁(Deadlock))

[2.1 什么是死锁?](#2.1 什么是死锁?)

[2.2 死锁示例](#2.2 死锁示例)

[2.3 如何避免死锁?](#2.3 如何避免死锁?)

[3. 资源限制(Resource Contention)](#3. 资源限制(Resource Contention))

[3.1 什么是资源限制?](#3.1 什么是资源限制?)

[3.2 如何解决资源限制?](#3.2 如何解决资源限制?)


1. 上下文切换(Context Switching)

1.1 什么是上下文切换?

上下文切换是指CPU从一个线程(或进程)切换到另一个线程的过程。在切换过程中,操作系统需要保存当前线程的状态(如寄存器、程序计数器等),并加载新线程的状态。

1.2 上下文切换的代价

上下文切换会带来以下开销:

  • 时间开销:保存和恢复线程状态需要时间。

  • CPU缓存失效:切换线程后,CPU缓存中的数据可能不再有效,导致缓存命中率下降。

  • 调度开销:操作系统需要决定下一个运行的线程。

1.3 如何减少上下文切换?

  • 减少线程数量:使用线程池控制线程数量,避免创建过多线程。

  • 使用非阻塞算法:减少线程间的竞争,降低切换频率。

  • 优化锁的使用:减少锁的争用,例如使用读写锁或无锁数据结构。

示例:线程池减少上下文切换

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

public class ContextSwitchDemo {

public static void main(String[] args) {

ExecutorService executor = Executors.newFixedThreadPool(4); // 使用线程池

for (int i = 0; i < 10; i++) {

executor.submit(() -> {

System.out.println("Task executed by " + Thread.currentThread().getName());

});

}

executor.shutdown();

}

}

多线程一定比单线程快吗?
  • 测试代码:比较并发和串行执行相同的累加任务的时间。

public class ConcurrencyTest {

private static final long count = 10000l;

public static void main(String[] args) throws InterruptedException {

concurrency();

serial();

}

private static void concurrency() throws InterruptedException {

long start = System.currentTimeMillis();

Thread thread = new Thread(new Runnable() {

@Override

public void run() {

int a = 0;

for (long i = 0; i < count; i++) {

a += 5;

}

}

});

thread.start();

int b = 0;

for (long i = 0; i < count; i++) {

b--;

}

long time = System.currentTimeMillis() - start;

thread.join();

System.out.println("concurrency :" + time+"ms,b="+b);

}

private static void serial() {

long start = System.currentTimeMillis();

int a = 0;

for (long i = 0; i < count; i++) {

a += 5;

}

int b = 0;

for (long i = 0; i < count; i++) {

b--;

}

long time = System.currentTimeMillis() - start;

System.out.println("serial:" + time+"ms,b="+b+",a="+a);

}

}

  • 结论:并发执行不一定比串行快,特别是任务次数较少时。原因是线程的创建和上下文切换的开销。测试表明,当任务量不够大时,线程的管理和切换反而可能让程序变慢。

2. 死锁(Deadlock)

2.1 什么是死锁?

死锁是指两个或多个线程互相持有对方所需的资源,导致所有线程都无法继续执行。死锁的四个必要条件是:

  • 互斥(Mutual Exclusion):资源只能被一个线程或进程占有,且其他线程或进程必须等待。例如,一个线程占用某个锁,其他线程就无法访问该资源,直到锁被释放。
  • 占有且等待(Hold and Wait):线程或进程已经持有某些资源,同时又请求其他资源,而这些资源当前被其他线程或进程占用。
  • 不可剥夺(No Preemption):已经分配给线程或进程的资源,在没有释放之前不能被其他线程或进程强行抢占。只有线程或进程自己释放资源,其他线程才能获得资源。
  • 循环等待(Circular Wait):存在一个资源的等待链,其中每个线程或进程都在等待下一个线程或进程所持有的资源,形成一个闭环。

2.2 死锁示例

public class DeadLockDemo {

private static String A = "A";

private static String B = "B";

public static void main(String[] args) {

new DeadLockDemo().deadLock();

}

private void deadLock() {

Thread t1 = new Thread(new Runnable() {

@Override

public void run() {

synchronized (A) {

try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); }

synchronized (B) {

System.out.println("1");

}

}

}

});

Thread t2 = new Thread(new Runnable() {

@Override

public void run() {

synchronized (B) {

synchronized (A) {

System.out.println("2");

}

}

}

});

t1.start();

t2.start();

}

}

代码中的Thread-1Thread-2相互等待对方的锁,造成死锁。

2.3 如何避免死锁?

  • 破坏循环等待条件
    资源的顺序分配:最常见的解决方法是对资源加锁时,按照一定的顺序来申请资源,避免出现循环等待。例如,为每个资源分配一个唯一的编号,然后线程总是按照资源编号的顺序来申请资源。如果线程按顺序申请资源,就不会出现循环等待的情况。
  • 破坏占有且等待条件
    一次性申请所有资源:要求线程在执行时一次性申请它需要的所有资源,而不是在持有一部分资源时,再去申请其他资源。这样可以避免线程在持有部分资源的同时等待其他资源,从而避免占有且等待条件。
  • 破坏非抢占条件
    抢占资源:当线程申请资源失败时,系统可以强制剥夺线程持有的资源并将其返回给资源池。被抢占的线程可以在稍后重新尝试获取资源。这种方法通过破坏非抢占条件来避免死锁。
  • 破坏互斥条件
    使用共享资源:通过将资源的互斥性降低,即允许多个线程共享资源,来避免死锁。比如,对于读写操作,可以使用 读写锁,使得多个线程可以同时读取共享资源,但写操作仍然是独占的。此方法只适用于资源可以共享的场景,通常是读取操作较多的情况。
  • 银行家算法

3. 资源限制(Resource Contention)

3.1 什么是资源限制?

资源限制是指多个线程竞争有限的资源(如CPU、内存、I/O等),导致性能下降。常见的资源限制包括:

  • CPU限制:线程数量超过CPU核心数,导致频繁的上下文切换。

  • 内存限制:内存不足导致频繁的垃圾回收。

  • I/O限制:磁盘或网络I/O成为瓶颈。

3.2 如何解决资源限制?

  • 增加资源:例如增加CPU核心数、内存容量或I/O带宽。

  • 优化资源使用:例如使用缓存减少I/O操作,或使用更高效的算法减少CPU消耗。

  • 限制并发数:通过线程池或信号量控制并发线程数量

相关推荐
PXM的算法星球几秒前
java(spring boot)实现向deepseek/GPT等模型的api发送请求/多轮对话(附源码)
java·gpt·microsoft
被程序耽误的胡先生4 分钟前
java中 kafka简单应用
java·开发语言·kafka
刀客1234 分钟前
python小项目编程-中级(1、图像处理)
开发语言·图像处理·python
卷卷的小趴菜学编程9 分钟前
c++之多态
c语言·开发语言·c++·面试·visual studio code
F202269748616 分钟前
Spring MVC 对象转换器:初级开发者入门指南
java·spring·mvc
冷琴199629 分钟前
基于Python+Vue开发的反诈视频宣传管理系统源代码
开发语言·vue.js·python
楠枬37 分钟前
网页五子棋——对战后端
java·开发语言·spring boot·websocket·spring
kyle~39 分钟前
thread---基本使用和常见错误
开发语言·c++·算法
YXWik61 小时前
23种设计模式
java·设计模式