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消耗。

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

相关推荐
seabirdssss27 分钟前
使用Spring Boot DevTools快速重启功能
java·spring boot·后端
喂完待续35 分钟前
【序列晋升】29 Spring Cloud Task 微服务架构下的轻量级任务调度框架
java·spring·spring cloud·云原生·架构·big data·序列晋升
benben04438 分钟前
ReAct模式解读
java·ai
轮到我狗叫了1 小时前
牛客.小红的子串牛客.kotori和抽卡牛客.循环汉诺塔牛客.ruby和薯条
java·开发语言·算法
yudiandian20141 小时前
【QT 5.12.12 下载 Windows 版本】
开发语言·qt
高山有多高2 小时前
详解文件操作
c语言·开发语言·数据库·c++·算法
狂奔的sherry2 小时前
单例模式(巨通俗易懂)普通单例,懒汉单例的实现和区别,依赖注入......
开发语言·c++·单例模式
Volunteer Technology2 小时前
三高项目-缓存设计
java·spring·缓存·高并发·高可用·高数据量
EnigmaCoder2 小时前
【C++】引用的本质与高效应用
开发语言·c++
栗子~~3 小时前
bat脚本- 将jar 包批量安装到 Maven 本地仓库
java·maven·jar