回顾JAVA中的锁机制
Java中的锁机制
在Java中,锁机制是多线程编程里保障数据一致性与线程安全的关键技术。
1. 内置锁:synchronized关键字
synchronized
是Java的内置锁机制,能够保证在同一时刻,只有一个线程可以执行被其修饰的代码块或方法。
用法示例
public class SynchronizedExample {private int count = 0;// 同步方法public synchronized void increment() {count++;}// 同步代码块public void decrement() {synchronized(this) {count--;}}
}
实现原理
synchronized
是基于对象头中的Mark Word来实现的。当一个线程访问同步代码块时,会先查看对象的Mark Word。如果Mark Word显示该对象没有被锁定,那么这个线程就会将Mark Word设置为锁定状态,然后开始执行同步代码块。在这个线程执行同步代码块期间,如果其他线程也想访问这个同步代码块,它们会发现对象的Mark Word已经被设置为锁定状态,于是这些线程就会被阻塞,进入等待队列。
2. 显示锁:Lock接口
Lock
接口是Java 5引入的,它提供了比synchronized
更灵活、更强大的锁控制能力。
核心方法
lock()
:获取锁,如果锁不可用,则线程会被阻塞。unlock()
:释放锁,必须在finally
块中调用,以确保锁一定会被释放。tryLock()
:尝试获取锁,如果锁可用,则获取锁并返回true
;如果锁不可用,则立即返回false
,不会阻塞线程。
用法示例
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class LockExample {private final Lock lock = new ReentrantLock();private int count = 0;public void increment() {lock.lock();try {count++;} finally {lock.unlock();}}
}
3. ReentrantLock(可重入锁)
ReentrantLock
是Lock
接口的一个重要实现类,它支持可重入锁的特性。所谓可重入锁,就是指同一个线程可以多次获取同一把锁,而不会出现死锁的情况。
特性
- 公平性:
ReentrantLock
可以设置为公平锁或非公平锁。公平锁会按照线程请求锁的顺序来分配锁,而非公平锁则不保证这一点,有可能后请求的线程先获得锁。 - 可重入性:同一个线程可以多次获取同一把锁,每获取一次,锁的计数器就会加1,每释放一次,锁的计数器就会减1,当计数器为0时,锁才会被真正释放。
公平锁示例
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class FairLockExample {private final Lock fairLock = new ReentrantLock(true); // true表示公平锁public void performTask() {fairLock.lock();try {// 执行任务} finally {fairLock.unlock();}}
}
4. ReentrantReadWriteLock(读写锁)
ReentrantReadWriteLock
提供了读写分离的锁机制,它维护了一对锁,一个读锁和一个写锁。
特性
- 读锁:允许多个线程同时获取读锁,用于并发读取共享资源。
- 写锁:写锁是排他锁,同一时刻只允许一个线程获取写锁,用于修改共享资源。
- 读写互斥:读锁和写锁不能同时被获取,即当有线程获取了读锁时,其他线程不能获取写锁;当有线程获取了写锁时,其他线程不能获取读锁和写锁。
用法示例
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;public class Cache {private final ReadWriteLock rwLock = new ReentrantReadWriteLock();private Object data;public Object read() {rwLock.readLock().lock();try {return data;} finally {rwLock.readLock().unlock();}}public void write(Object newData) {rwLock.writeLock().lock();try {data = newData;} finally {rwLock.writeLock().unlock();}}
}
5. StampedLock(邮戳锁)
StampedLock
是Java 8引入的一种新的锁机制,它提供了比ReentrantReadWriteLock
更细粒度的锁控制。可以有效应对A-B-A问题。
特性
- 乐观读锁:乐观读锁是一种无锁机制,它允许在没有获取锁的情况下读取共享资源。读取完成后,需要验证资源是否在读取期间被修改过,如果没有被修改过,则读取有效;如果被修改过,则需要重新读取。
- 悲观读锁和写锁:与
ReentrantReadWriteLock
的读锁和写锁类似,但StampedLock
的悲观读锁和写锁是通过返回一个邮戳(stamp)来控制的。
用法示例
import java.util.concurrent.locks.StampedLock;public class Point {private double x, y;private final StampedLock sl = new StampedLock();public double distanceFromOrigin() {long stamp = sl.tryOptimisticRead(); // 尝试乐观读double currentX = x, currentY = y;if (!sl.validate(stamp)) { // 验证是否有写操作发生stamp = sl.readLock(); // 获取悲观读锁try {currentX = x;currentY = y;} finally {sl.unlockRead(stamp);}}return Math.sqrt(currentX * currentX + currentY * currentY);}
}
6. 锁的优化
锁粗化(Lock Coarsening)
锁粗化是指将多次连续的加锁和解锁操作合并为一次加锁和解锁操作,以减少锁的获取和释放带来的性能开销。
锁消除(Lock Elimination)
锁消除是指在编译时,Java编译器会对一些代码进行分析,如果发现某些锁是不必要的,就会将这些锁消除掉,从而提高代码的执行效率。
偏向锁(Biased Locking)
偏向锁是一种针对单线程环境的锁优化机制。当一个线程第一次获取锁时,锁会被标记为偏向锁,并记录该线程的ID。当该线程再次获取锁时,无需进行任何同步操作,直接获取锁,从而提高了单线程环境下的性能。
轻量级锁(Lightweight Locking)
轻量级锁是一种在多线程环境下,但线程竞争不激烈时的锁优化机制。当线程竞争不激烈时,轻量级锁可以避免线程的阻塞和唤醒操作,从而提高了性能。
自旋锁(Spin Lock)
自旋锁是指当一个线程获取锁失败时,它不会立即被阻塞,而是会在原地循环等待,直到锁被释放。自旋锁适用于锁的持有时间较短的场景,可以减少线程的阻塞和唤醒带来的性能开销。
7. 选择合适的锁
- synchronized:适用于简单的同步场景,代码简洁,由JVM自动管理锁的获取和释放。
- ReentrantLock:适用于需要更灵活的锁控制的场景,如可中断锁、公平锁等。
- ReentrantReadWriteLock:适用于读多写少的场景,可以提高并发读取的性能。
- StampedLock:适用于读多写少且读操作性能要求较高的场景,提供了乐观读锁机制,进一步提高了读操作的性能。
通过合理使用这些锁机制,你可以在多线程编程中实现高效且安全的并发控制。
重入锁和内置锁的选用
在Java里,重入锁(ReentrantLock)和内置锁(synchronized)都可用于实现线程同步,不过它们的适用场景存在差异。下面为你介绍选择使用重入锁还是内置锁的依据:
优先考虑内置锁的情况
- 语法简洁:内置锁是通过
synchronized
关键字来实现的,无需手动释放锁,JVM会自动处理锁的获取和释放,这样能降低因忘记释放锁而导致死锁的风险。public synchronized void method() {// 同步代码块 }
- 对性能要求不高:在JDK 1.6之后,Java对
synchronized
进行了一系列优化,像偏向锁、轻量级锁等,使得它的性能和ReentrantLock
相差不大。 - 锁的使用场景简单:若只是需要对方法或代码块进行简单的同步,内置锁完全可以满足需求。
优先考虑重入锁的情况
- 需要公平锁:重入锁可以通过构造函数指定使用公平锁(
new ReentrantLock(true)
),公平锁会按照线程请求锁的顺序来分配锁,能避免某些线程长时间等待锁的情况。而内置锁只能是非公平锁。 - 需要灵活的锁控制:重入锁提供了一些高级功能,如可中断锁、尝试锁(
tryLock()
)、带超时的锁获取等。Lock lock = new ReentrantLock(); try {// 尝试获取锁,若锁被其他线程持有,则当前线程可被中断lock.lockInterruptibly();// 执行同步操作 } catch (InterruptedException e) {Thread.currentThread().interrupt(); } finally {lock.unlock(); }
- 需要实现条件变量(Condition):重入锁可以和
Condition
接口配合使用,实现更灵活的线程等待和唤醒机制,比如实现生产者 - 消费者模式。Lock lock = new ReentrantLock(); Condition condition = lock.newCondition();// 等待条件 lock.lock(); try {while (!conditionMet()) {condition.await();} } finally {lock.unlock(); }// 唤醒等待的线程 lock.lock(); try {condition.signalAll(); } finally {lock.unlock(); }
性能方面的考量
- 低竞争场景:在竞争不激烈的情况下,内置锁和重入锁的性能差距不大。
- 高竞争场景:如果线程之间对锁的竞争非常激烈,重入锁的性能可能会略优于内置锁,因为重入锁提供了更多的锁优化选项,例如使用非公平锁可以减少线程的上下文切换。
总结
- 推荐优先使用内置锁:因为它的语法简洁,由JVM自动管理锁的获取和释放,降低了出错的概率。
- 在需要高级特性时使用重入锁:例如公平锁、可中断锁、条件变量等。
示例对比
内置锁示例
public class SynchronizedExample {private int count = 0;public synchronized void increment() {count++;}
}
重入锁示例
import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockExample {private final ReentrantLock lock = new ReentrantLock();private int count = 0;public void increment() {lock.lock();try {count++;} finally {lock.unlock();}}
}
通过对比可以看出,内置锁的代码更加简洁,而重入锁则提供了更灵活的控制方式。你可以根据具体的需求来选择合适的锁机制。