各种锁的理解

公平锁非公平锁

公平锁和非公平锁是在多线程环境下用来控制对共享资源访问的锁。公平锁会按照请求的顺序来获取锁,而非公平锁则不考虑等待队列中的顺序,有可能插队获取锁。

公平锁

公平锁会按照线程请求锁的顺序来获取锁,即先到先得。当一个线程释放锁后,等待时间最长的线程会获得锁。公平锁的实现会维护一个等待队列,新来的线程会排队等待获取锁。

示例代码:

Lock fairLock = new ReentrantLock(true); // 创建一个公平锁
fairLock.lock();
try {
    // 访问共享资源
} finally {
    fairLock.unlock();
}

非公平锁

非公平锁不考虑等待队列中的顺序,有可能新来的线程会插队获取锁,这样可能会导致等待时间较长的线程一直无法获取锁。

示例代码:

Lock unfairLock = new ReentrantLock(false); // 创建一个非公平锁
unfairLock.lock();
try {
    // 访问共享资源
} finally {
    unfairLock.unlock();
}

在实际应用中,公平锁和非公平锁的选择取决于具体的需求和性能要求。

可重入锁

可重入锁是指同一个线程可以多次获得同一把锁,而不会发生死锁。在 Java 中,ReentrantLock 就是一种可重入锁的实现。

特点

可重入锁的特点是同一个线程可以多次获取同一把锁,而不会被阻塞。这样可以避免死锁的发生,也方便了编程,使得同步代码块内部可以调用其他同步方法。

示例代码



// 演示可重入锁是什么意思,可重入,就是可以重复获取相同的锁,synchronized和ReentrantLock都是可重入的
// 可重入降低了编程复杂性
public class WhatReentrant {
	public static void main(String[] args) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				synchronized (this) {
					System.out.println("第1次获取锁,这个锁是:" + this);
					int index = 1;
					while (true) {
						synchronized (this) {
							System.out.println("第" + (++index) + "次获取锁,这个锁是:" + this);
						}
						if (index == 10) {
							break;
						}
					}
				}
			}
		}).start();
	}
}
package com.test.reen;

import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;

// 演示可重入锁是什么意思
public class WhatReentrant2 {
	public static void main(String[] args) {
		ReentrantLock lock = new ReentrantLock();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					lock.lock();
					System.out.println("第1次获取锁,这个锁是:" + lock);

					int index = 1;
					while (true) {
						try {
							lock.lock();
							System.out.println("第" + (++index) + "次获取锁,这个锁是:" + lock);
							
							try {
								Thread.sleep(new Random().nextInt(200));
							} catch (InterruptedException e) {
								e.printStackTrace();
							}
							
							if (index == 10) {
								break;
							}
						} finally {
							lock.unlock();
						}

					}

				} finally {
					lock.unlock();
				}
			}
		}).start();
	}
}

使用ReentrantLock的注意点

ReentrantLock 和 synchronized 不一样,需要手动释放锁,所以使用 ReentrantLock的时候一定要手动释放锁,并且加锁次数和释放次数要一样

自旋锁详解

自旋锁是一种轻量级的锁机制,主要用于多线程环境中控制对共享资源的访问。与传统的阻塞锁不同,自旋锁在获取锁时不会让线程进入休眠状态,而是通过循环不断尝试获取锁,直到成功为止。这种机制适用于锁持有时间较短的场景,因为它可以减少上下文切换的开销。

自旋锁的工作原理

自旋锁的基本原理是,当一个线程尝试获取锁时,如果锁已经被其他线程占用,它会在一个循环中不断检查锁的状态,而不是被挂起。这个过程称为“自旋”。一旦锁被释放,线程会立即获得锁并继续执行。

自旋锁的优缺点

优点

  1. 低延迟:自旋锁避免了线程的上下文切换,适合于锁持有时间短的场景。

  2. 简单实现:自旋锁的实现相对简单,通常只需要一个原子变量来表示锁的状态。

缺点

  1. CPU占用高:在自旋期间,线程会持续占用CPU资源,可能导致系统性能下降。

  2. 不适合长时间持有锁:如果锁持有时间较长,自旋锁会导致大量线程自旋,浪费CPU资源。

自旋锁的实现

在Java中,自旋锁可以通过java.util.concurrent.locks.Lock接口的实现类来实现。以下是一个简单的自旋锁实现示例:

示例代码

import java.util.concurrent.atomic.AtomicBoolean;

public class SpinLock {
    private final AtomicBoolean locked = new AtomicBoolean(false);

    public void lock() {
        while (!locked.compareAndSet(false, true)) {
            // 自旋,等待锁释放
        }
    }

    public void unlock() {
        locked.set(false);
    }

    public static void main(String[] args) {
        SpinLock spinLock = new SpinLock();

        Runnable task = () -> {
            spinLock.lock();
            try {
                // 访问共享资源
                System.out.println(Thread.currentThread().getName() + " acquired the lock.");
                // 模拟工作
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                spinLock.unlock();
                System.out.println(Thread.currentThread().getName() + " released the lock.");
            }
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);
        thread1.start();
        thread2.start();
    }
}

使用自旋锁的注意事项

  1. 适用场景:自旋锁适合于锁持有时间短的场景,避免在高竞争情况下使用。

  2. 避免死锁:在使用自旋锁时,确保不会出现死锁的情况,尤其是在多个锁的情况下。

  3. CPU资源管理:要注意自旋锁对CPU资源的占用,避免在高负载情况下使用。

总结

自旋锁是一种高效的锁机制,适用于特定的多线程场景。通过合理的使用自旋锁,可以提高程序的性能,但也需要注意其潜在的缺点和适用范围。

死锁详解

死锁是指两个或多个线程在执行过程中,由于争夺资源而造成的一种互相等待的状态。此时,线程无法继续执行,因为每个线程都在等待其他线程释放它所需要的资源。

死锁的条件

死锁的发生通常需要满足以下四个条件:

  1. 互斥条件:至少有一个资源必须处于非共享模式,即某个资源只能被一个线程占用。

  2. 保持并等待条件:一个线程至少持有一个资源,并等待获取其他资源。

  3. 不剥夺条件:已经获得的资源在未使用完之前,不能被其他线程强行剥夺。

  4. 循环等待条件:存在一个线程等待链,其中每个线程都在等待下一个线程持有的资源。

死锁的示例

以下是一个简单的死锁示例,展示了两个线程如何因互相等待而导致死锁:

示例代码

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

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1: Holding lock 1...");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                System.out.println("Thread 1: Waiting for lock 2...");
                synchronized (lock2) {
                    System.out.println("Thread 1: Acquired lock 2!");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread 2: Holding lock 2...");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                System.out.println("Thread 2: Waiting for lock 1...");
                synchronized (lock1) {
                    System.out.println("Thread 2: Acquired lock 1!");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

在这个示例中,Thread 1 持有 lock1 并等待 lock2,而 Thread 2 持有 lock2 并等待 lock1,导致两个线程互相等待,从而形成死锁。

死锁的检测与解决

检测死锁

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

  1. 资源分配图:构建资源分配图,检测是否存在循环等待的情况。

  2. 超时机制:为线程设置超时,如果在一定时间内未能获得锁,则放弃请求并进行重试。

  3. 线程监控工具:使用工具监控线程状态,识别死锁。

解决死锁

解决死锁的方法主要有:

  1. 避免死锁:通过设计避免死锁的发生,例如遵循资源请求的顺序。

  2. 资源剥夺:允许系统强制剥夺某些资源,以打破死锁。

  3. 重启线程:在检测到死锁后,重启相关线程。

  4. 使用锁的超时机制:在请求锁时设置超时,如果未能获得锁,则释放已持有的锁并重试。

总结

死锁是多线程编程中常见的问题,了解其发生的条件和解决方案对于编写高效、可靠的并发程序至关重要。通过合理的设计和监控,可以有效地避免和处理死锁问题。

使用 jps 命令排查死锁

在 Java 程序中,如果怀疑存在死锁情况,可以通过 jps 命令结合 jstack 命令来排查死锁。以下是排查死锁的步骤:

  1. 使用 jps 命令查看 Java 进程的进程号: 打开命令行窗口,输入 jps 命令,可以列出当前系统中正在运行的 Java 进程以及它们的进程号。

  2. 找到可能存在死锁的 Java 进程号: 根据程序的特征和进程号,找到可能存在死锁的 Java 进程号。

  3. 使用 jstack 命令获取线程堆栈信息: 输入 jstack <进程号> 命令,可以获取该 Java 进程中所有线程的堆栈信息。

  4. 分析线程堆栈信息: 通过分析线程堆栈信息,查看是否存在相互等待的情况,以及可能导致死锁的原因。

  5. 解决死锁问题: 根据分析的结果

死锁的处理与避免

死锁一旦发生,通常会导致程序的某些部分无法继续执行,因此在多线程编程中,避免死锁是非常重要的。然而,除了避免死锁之外,还有一些方法可以处理已经发生的死锁。

死锁的处理方法

1. 检测与恢复

  • 检测机制:可以通过构建资源分配图,定期检查系统中是否存在循环等待的情况。一旦检测到死锁,可以采取措施进行恢复。

  • 恢复策略:一旦检测到死锁,可以选择以下策略:

    • 终止线程:强制终止一个或多个参与死锁的线程,以释放资源。

    • 资源剥夺:允许系统强制剥夺某些资源,打破死锁状态。

2. 超时机制

为线程设置超时限制,如果在规定时间内未能获得锁,则放弃请求并释放已持有的资源。这种方法可以有效避免长时间的死锁状态。

3. 重启线程

在检测到死锁后,可以选择重启相关线程。虽然这种方法可能会导致数据丢失,但在某些情况下,可以作为一种有效的解决方案。

死锁的避免策略

1. 资源请求顺序

确保所有线程按照相同的顺序请求资源,这样可以避免循环等待的情况。例如,如果多个线程需要访问多个资源,确保它们总是以相同的顺序请求这些资源。

2. 资源分配策略

采用更灵活的资源分配策略,例如:

  • 请求所有资源:线程在开始执行之前请求所有需要的资源,如果无法获得,则释放已获得的资源并重试。

  • 分配最少资源:尽量减少每个线程持有的资源数量,降低死锁的风险。

3. 使用锁的超时机制

在请求锁时设置超时,如果未能获得锁,则释放已持有的锁并重试。这种方法可以有效减少死锁的发生。