Skip to content

1线程的基本概念

1.1 什么是线程?

线程(Thread)是程序执行流的最小单元,是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间、文件句柄等,但每个线程都有自己独立的程序计数器、寄存器集合和栈空间。

1.2 线程与进程的区别

  • 资源占用:进程拥有独立的地址空间和系统资源,线程共享进程资源

  • 切换开销:线程切换比进程切换开销小,只需要保存和恢复寄存器上下文

  • 通信方式:进程间通信需要特殊的机制,线程间可以直接共享内存

  • 并发度:多线程并发比多进程并发更高效

1.3 线程的生命周期

线程的生命周期包括以下状态:

  • 新建(New):线程对象创建,但尚未启动

  • 就绪(Runnable):线程已启动,等待CPU调度

  • 运行(Running):CPU正在执行线程代码

  • 阻塞(Blocked):线程等待某个条件,暂时停止运行

  • 终止(Terminated):线程执行完毕或异常终止

2Java锁的类型与实现原理

2.1 锁的基本概念

锁是并发编程中用于控制多个线程对共享资源访问的机制,确保同一时间只有一个或有限个线程可以访问共享资源,从而保证数据一致性。

2.2 Java中的锁分类

2.2.1 内置锁(Synchronized)

Synchronized是Java内置的锁机制,它是一种可重入的互斥锁。使用Synchronized可以修饰方法或代码块,确保同一时间只有一个线程可以执行被修饰的代码。

                         `// 修饰方法

public synchronized void synchronizedMethod() { // 同步代码 }

// 修饰代码块 public void synchronizedBlock() { synchronized(this) { // 同步代码 } }`

2.2.2 Lock接口及其实现

Java 5引入了Lock接口,提供了比Synchronized更灵活的锁机制,包括可中断锁、超时锁、读写锁等。

  • ReentrantLock:可重入的互斥锁,与Synchronized类似

  • ReentrantReadWriteLock:读写锁,允许多个线程同时读,只允许一个线程写

  • StampedLock:改进的读写锁,提供乐观读模式

                           `// ReentrantLock示例
    

private final Lock lock = new ReentrantLock();

public void method() { lock.lock(); try { // 临界区代码 } finally { lock.unlock(); } }`

2.3 锁的实现原理

2.3.1 Synchronized的实现

Synchronized在Java 6之前是重量级锁,依赖于操作系统的互斥量实现。Java 6引入了锁升级机制,包括偏向锁、轻量级锁和重量级锁三个阶段,根据竞争情况自动升级。

  • 偏向锁:当只有一个线程访问时,使用偏向锁,减少锁开销

  • 轻量级锁:当有少量线程竞争时,使用CAS操作实现

  • 重量级锁:当竞争激烈时,升级为重量级锁,使用操作系统互斥量

2.3.2 AQS(AbstractQueuedSynchronizer)

AQS是Java并发包中锁机制的基础框架,它提供了一个基于FIFO队列的等待机制,用于实现各种同步器。ReentrantLock、CountDownLatch、Semaphore等都基于AQS实现。

3死锁的形成与检测

3.1 什么是死锁?

死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法继续执行下去。

3.2 死锁的形成条件

死锁的形成必须同时满足以下四个条件:

  • 互斥条件:资源不能被多个线程同时占用

  • 请求与保持条件:线程持有至少一个资源,同时请求新的资源

  • 不剥夺条件:线程已获得的资源不能被强制剥夺

  • 循环等待条件:多个线程形成头尾相接的循环等待链

3.3 死锁示例

                     `public class DeadlockExample {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();

public static void main(String[] args) {
    // 线程1:先获取lock1,再获取lock2
    Thread thread1 = new Thread(() -> {
        synchronized (lock1) {
            System.out.println("Thread 1: Holding lock1");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread 1: Waiting for lock2");
            synchronized (lock2) {
                System.out.println("Thread 1: Holding lock1 and lock2");
            }
        }
    });
    
    // 线程2:先获取lock2,再获取lock1
    Thread thread2 = new Thread(() -> {
        synchronized (lock2) {
            System.out.println("Thread 2: Holding lock2");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread 2: Waiting for lock1");
            synchronized (lock1) {
                System.out.println("Thread 2: Holding lock2 and lock1");
            }
        }
    });
    
    thread1.start();
    thread2.start();
}

}`

3.4 死锁的检测与预防

3.4.1 死锁检测

可以通过以下方式检测死锁:

  • jstack命令:生成线程快照,查看是否存在死锁

  • JConsole:可视化工具,可检测死锁

  • 程序内部检测:通过算法定期检测资源分配图

3.4.2 死锁预防

预防死锁的方法是破坏死锁的四个必要条件之一:

  • 破坏互斥条件:使用共享资源的副本

  • 破坏请求与保持条件:一次性获取所有资源

  • 破坏不剥夺条件:允许资源被剥夺

  • 破坏循环等待条件:按顺序获取资源

4线程死锁流程解释图

死锁形成过程详解

下图展示了两个线程因争夺资源而形成死锁的完整流程:

图详解:

  • 初始状态:系统中有两个线程(Thread A和Thread B)和两个资源(Resource 1和Resource 2)

  • 线程A获取Resource 1:Thread A成功获取Resource 1的锁

  • 线程B获取Resource 2:Thread B成功获取Resource 2的锁

  • 线程A请求Resource 2:Thread A尝试获取Resource 2,但Resource 2已被Thread B占用,Thread A进入等待状态

  • 线程B请求Resource 1:Thread B尝试获取Resource 1,但Resource 1已被Thread A占用,Thread B进入等待状态

  • 死锁形成:Thread A等待Thread B释放Resource 2,Thread B等待Thread A释放Resource 1,两者互相等待,形成死锁

🔄 交互式死锁模拟

通过以下模拟器,您可以直观地观察死锁的形成过程:

                        开始模拟
                        重置
                        单步执行
                        自动执行
                    
                    
                    
                        
                            A
                            线程A
                        
                        
                        
                            
                            
                        
                        
                        
                            R1
                            资源1
                        
                        
                        
                            R2
                            资源2
                        
                        
                        
                            
                            
                        
                        
                        
                            B
                            线程B

模拟步骤:

                        初始状态:线程A和线程B准备执行
                        线程A获取资源1
                        线程B获取资源2
                        线程A请求资源2,进入等待状态
                        线程B请求资源1,进入等待状态
                        死锁形成:两个线程互相等待
                    
                

                
                    死锁是并发编程中的常见问题,通过理解死锁的形成条件和检测方法,可以有效避免和解决死锁问题。

5死锁的解决方案

5.1 避免死锁的编程实践

  • 按固定顺序获取锁

  • 使用tryLock()尝试获取锁,设置超时时间

  • 减少锁的持有时间

  • 使用并发容器替代同步容器

  • 使用线程池管理线程

5.2 死锁恢复策略

当死锁发生时,可以采取以下恢复策略:

  • 终止线程:终止一个或多个死锁线程,释放资源

  • 资源剥夺:剥夺一个或多个线程的资源,分配给其他线程

  • 进程回滚:将线程回滚到某个安全状态,重新执行

                       **最佳实践:** 在并发编程中,应尽量避免使用多个锁,如必须使用多个锁,应确保所有线程按相同的顺序获取锁,以避免循环等待。
    

基于 VitePress 构建 | 技术知识库