** Java中的锁机制**
1. 公平锁 vs 非公平锁
-
公平锁 :公平锁的特点是多个线程按照请求锁的顺序来获取锁,即遵循 FIFO(先进先出)顺序。公平锁会避免"饥饿"现象,即后申请锁的线程不会比先申请的线程更早获取锁。Java中的
ReentrantLock可以通过构造函数指定是否公平锁。如果传入true,则创建公平锁。 -
非公平锁 :非公平锁则没有严格按照顺序分配锁,后申请的线程有可能比先申请的线程更早获得锁。这种锁的好处是性能较高,因为它减少了等待时间,避免了严格排队的开销。
ReentrantLock的默认锁是非公平的。synchronized也属于非公平锁。
举例:
- 公平锁:假设有三个线程
T1、T2、T3,当T1获取锁后,T2会等待,直到T1释放锁,T2才能获得锁。当T2释放锁时,T3会按顺序获取锁。 - 非公平锁:假设有三个线程
T1、T2、T3,即使T1先申请了锁,T2可能会因为某些条件(如线程调度)先获取到锁,导致T1等待。
2. 可重入锁(递归锁)
可重入锁的意思是同一个线程可以多次获取同一把锁,而不会发生死锁。一个线程获取锁后,可以进入该锁保护的代码块,即使它已经持有锁,也能继续获得该锁。这使得代码在调用嵌套方法时不会被阻塞。
举例:
ReentrantLock和synchronized都是可重入锁。比如,在一个方法中获取锁后,再调用另一个方法,若该方法也需要相同的锁,线程仍然可以继续执行。
代码示例:
java
class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void outerMethod() {
lock.lock();
try {
System.out.println("Outer method");
innerMethod(); // 内部方法依然能获取锁
} finally {
lock.unlock();
}
}
public void innerMethod() {
lock.lock();
try {
System.out.println("Inner method");
} finally {
lock.unlock();
}
}
}
在这个例子中,outerMethod 和 innerMethod 都需要 lock,但线程能够成功执行,因为 ReentrantLock 允许同一线程获取同一个锁。
3. 独享锁 vs 共享锁
-
独享锁 :独享锁是指同一时间只有一个线程可以持有锁。
ReentrantLock就是独享锁的实现。比如,synchronized也是独享锁。 -
共享锁 :共享锁允许多个线程同时持有锁。
ReadWriteLock中的读锁就是共享锁,多个线程可以同时获取读锁,而写锁是独享锁,只能有一个线程持有。
举例:
- 独享锁:一个线程获取锁后,其他线程必须等待锁被释放,直到锁的持有者释放锁。
- 共享锁:多个线程可以同时读取数据,只要没有线程进行写操作(获取写锁)。
4. 互斥锁 vs 读写锁
-
互斥锁 :互斥锁是指一次只能有一个线程持有锁。例如
ReentrantLock就是互斥锁,它保证每次只有一个线程可以执行临界区代码。 -
读写锁 :读写锁是一种特殊的锁,它分为读锁和写锁。多个线程可以同时持有读锁,但写锁是独占的。
ReadWriteLock提供了读写锁机制,适用于读操作远远多于写操作的场景,可以提高并发性能。
举例:
- 互斥锁:比如多个线程同时访问一个共享资源,但只有一个线程能够获取锁并执行。
- 读写锁:当多个线程同时读取数据时,可以同时获取读锁,但如果有一个线程尝试写数据,则所有的读线程都需要释放读锁,写线程才能获取写锁。
5. 乐观锁 vs 悲观锁
-
悲观锁 :悲观锁认为并发操作一定会导致冲突,因此它总是加锁,确保数据安全。
synchronized和ReentrantLock都属于悲观锁。 -
乐观锁 :乐观锁认为并发操作不会冲突,因此它不会加锁,而是在执行操作时进行检查,如果数据没有被修改,则更新;如果被修改了,则重新尝试。常见的实现方式是 CAS(Compare-And-Swap) ,Java 中的原子类(如
AtomicInteger)就是使用 CAS 来实现的。
举例:
- 悲观锁:在多线程操作共享资源时,所有线程都需要获取锁来确保安全,保证数据一致性。
- 乐观锁:线程不加锁,直接执行操作,如果发现数据被修改则重新执行操作,而不是等待锁释放。
6. 分段锁
分段锁是将锁分为多个段,每个段可以独立地加锁,从而提高并发性能。ConcurrentHashMap 就是通过分段锁来实现高效的并发操作,每个段内独立加锁,这样多个线程可以同时操作不同的段。
举例:
- 假设
ConcurrentHashMap中有 16 个段,每个段都可以独立加锁,这样多个线程可以并发地访问不同的段,提高了并发性。
7. 偏向锁、轻量级锁、重量级锁
这些锁是针对 synchronized 的优化,JVM 会根据线程的竞争情况进行锁的升级:
- 偏向锁:如果一个线程频繁访问同步代码块,它会获得偏向锁,这样可以减少锁竞争的开销。
- 轻量级锁:当偏向锁被其他线程竞争时,JVM 会将偏向锁升级为轻量级锁。此时,其他线程尝试通过自旋获取锁。
- 重量级锁:当自旋锁竞争激烈时,锁会升级为重量级锁,其他线程会被阻塞,性能会下降。
synchronized的理解
Synchronized 是 Java 中用于实现同步的一种机制。它确保同一时刻只有一个线程能够访问被修饰的代码块或方法,防止多个线程同时访问共享资源导致数据不一致。
1. 工作原理
当一个线程执行某个被 synchronized 修饰的方法或代码块时,它会获取到锁(称为对象锁或类锁)。其他线程在访问该方法或代码块时,如果没有获得锁,则会被阻塞,直到当前线程释放锁。
- 修饰实例方法 :锁住的是实例对象(
this)。 - 修饰静态方法:锁住的是类的 Class 对象。
- 修饰代码块:锁住的是指定对象。
2. 锁的种类
- 方法锁 :当
synchronized修饰方法时,整个方法都会被锁住。 - 代码块锁 :当
synchronized修饰代码块时,只有代码块内的部分被锁住。
3. 性能优化
从 Java 5 开始,JVM 对 synchronized 进行了优化,引入了偏向锁、轻量级锁、重量级锁等机制。锁的升级有助于减少不必要的锁竞争,从而提高性能。
4. 死锁的风险
Synchronized 锁可能导致死锁,特别是在多个线程相互等待对方释放锁时。例如:
java
class A {
synchronized void method1(B b) {
b.last();
}
synchronized void last() {}
}
class B {
synchronized void method2(A a) {
a.last();
}
synchronized void last() {}
}
如果线程 T1 在 A.method1() 中持有 A 锁,且需要 B 锁,线程 T2 在 B.method2() 中持有 B 锁,且需要 A 锁,那么就会发生死锁。
总结:
Synchronized是用于线程同步的关键字,保证在同一时刻只有一个线程能够访问临界区代码。- 锁机制有不同种类,适用于不同的场景,如 公平锁/非公平锁 、可重入锁 、悲观锁/乐观锁 等。