** 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
都是可重入锁。比如,在一个方法中获取锁后,再调用另一个方法,若该方法也需要相同的锁,线程仍然可以继续执行。
代码示例:
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
锁可能导致死锁,特别是在多个线程相互等待对方释放锁时。例如:
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
是用于线程同步的关键字,保证在同一时刻只有一个线程能够访问临界区代码。- 锁机制有不同种类,适用于不同的场景,如 公平锁/非公平锁、可重入锁、悲观锁/乐观锁 等。