1. 在Java程序中保证多线程的运行安全
- 使用synchronized关键字
- 可以修饰方法。例如,当一个方法被声明为 synchronized 时,同一时刻只有一个线程可以访问该方法。比如:
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
在这个 Counter 类中, increment 方法被 synchronized 修饰。当多个线程访问 increment 方法时,Java会保证同一时刻只有一个线程能够执行该方法内的代码,从而避免了多个线程同时修改 count 变量导致的数据不一致问题。
- 也可以用于代码块。可以指定一个对象作为锁,例如:
public class SharedResource {
private Object lock = new Object();
private int data;
public void modifyData(int newData) {
synchronized (lock) {
data = newData;
}
}
}
这里,只有获得 lock 对象锁的线程才能执行 synchronized 代码块中的 data = newData 语句,这样可以对 data 变量的访问进行控制,保证数据的一致性。
- 使用ReentrantLock类
- ReentrantLock 是一种可重入锁。它提供了比 synchronized 关键字更灵活的锁机制。例如:
import java.util.concurrent.locks.ReentrantLock;
public class BankAccount {
private double balance;
private ReentrantLock lock = new ReentrantLock();
public void deposit(double amount) {
lock.lock();
try {
balance += amount;
} finally {
lock.unlock();
}
}
}
在 BankAccount 类的 deposit 方法中,首先通过 lock.lock() 获取锁,然后在 try - finally 块中执行对 balance 变量的操作。在 finally 块中通过 lock.unlock() 释放锁,这样可以确保锁一定会被释放,即使在操作过程中出现异常。
2. 读写锁可用于什么场景中
- 数据读取频繁、写入不频繁的场景
- 例如,在一个缓存系统中,多个线程可能会频繁地读取缓存中的数据,但写入操作(如更新缓存数据)相对较少。使用读写锁(如 ReentrantReadWriteLock )可以允许多个线程同时读取缓存数据,提高读取性能。只有当有线程需要写入数据时,才会排他性地获取写锁,此时其他线程(无论是读线程还是写线程)都需要等待。
3. 锁是什么?有什么用?有哪几种锁?
- 锁的定义和作用
- 锁是一种用于控制多个线程对共享资源访问的机制。在多线程环境中,多个线程可能会同时访问和修改共享资源,这可能会导致数据不一致、竞争条件等问题。锁可以确保在同一时刻只有一个(对于排它锁)或多个(对于共享锁)线程能够访问共享资源,从而保证程序的正确性和数据的一致性。
- 锁的种类
- 排它锁(Exclusive Lock)
- 也称为互斥锁。例如, synchronized 关键字在修饰方法或代码块时就是一种排它锁机制。在Java中, ReentrantLock 默认也是排它锁。当一个线程获取了排它锁后,其他线程不能再获取该锁,直到持有锁的线程释放锁。这种锁适用于对共享资源进行修改的操作,如写入数据到数据库、更新文件内容等场景。
- 共享锁(Shared Lock)
- 读写锁中的读锁就是一种共享锁。多个线程可以同时获取共享锁来读取共享资源,但是当有一个线程获取了排它锁(写锁)时,其他线程既不能获取排它锁也不能获取共享锁。这种锁适用于多个线程对共享资源进行读取操作的场景,如读取数据库中的数据、读取配置文件等。
- 可重入锁(Reentrant Lock)
- 像 ReentrantLock 和被 synchronized 修饰的方法或代码块都是可重入的。这意味着一个线程可以多次获取同一个锁。例如,在一个递归调用的方法中,如果该方法被 synchronized 修饰,线程在递归调用过程中可以多次获取该锁而不会发生死锁。
- 自旋锁(Spin Lock)
- 自旋锁是一种比较特殊的锁。当一个线程尝试获取自旋锁而该锁已经被其他线程占用时,这个线程不会立即进入阻塞状态,而是会在一个循环中不断地检查锁是否被释放,这个过程就像线程在“自旋”。自旋锁适用于锁被占用的时间很短的情况,因为它避免了线程在阻塞和唤醒过程中的开销,但如果锁被长时间占用,会浪费CPU资源。
4. 什么是死锁?
- 死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象。例如,线程A持有资源R1并且等待资源R2,而线程B持有资源R2并且等待资源R1,这样两个线程就会一直等待下去,无法继续执行,导致程序出现停滞状态。
5. 怎么防止死锁?
- 破坏死锁产生的必要条件
- 互斥条件:有些资源本身的性质决定了它们在使用时必须是互斥的,比如打印机,在打印一份文档时不能同时被多个任务使用。但对于一些可以共享的资源,如只读的数据结构,可以尽量采用共享访问的方式,避免强制互斥。
- 请求和保持条件:可以采用一次性申请所有资源的策略。例如,一个线程在开始执行任务前,就把它需要的所有资源都申请好,如果无法获取全部资源,就等待,直到所有资源都可用。这样就避免了线程在持有部分资源的情况下又去请求其他资源。
- 不可剥夺条件:可以设置资源的超时机制。如果一个线程长时间占用某个资源而没有释放,系统可以强制剥夺该资源并分配给其他等待的线程。不过这种方式需要谨慎使用,因为可能会导致数据不一致等问题。
- 循环等待条件:可以对资源进行排序。每个线程按照相同的顺序请求资源。例如,有资源A、B、C,所有线程都必须先请求A,再请求B,最后请求C。这样就可以避免循环等待的情况。
- 采用资源分配图算法进行死锁检测和恢复
- 可以定期地检查系统中的资源分配情况,构建资源分配图。如果发现存在循环等待的情况(即死锁),可以通过终止一个或多个涉及死锁的线程,或者抢占一些线程的资源来恢复系统的正常运行。不过这种方式可能会导致部分线程的工作丢失,需要根据具体的应用场景谨慎使用。