1.Java同步机制的演进历程
在多线程编程中,同步机制是保证数据一致性和线程安全的关键。Java在早期版本中引入了synchronized关键字,它提供了一种简单的锁机制来控制对共享资源的并发访问。随着Java语言的发展,synchronized的性能得到了显著提升,但在某些场景下仍然显得力不从心。因此,在JDK 1.5中引入了java.util.concurrent.locks.Lock接口,它提供了更灵活的锁操作,允许尝试非阻塞获取锁、可中断的锁等候以及公平锁等高级功能。
public class SynchronizedVsLock {
private int counter = 0;
public synchronized void incrementSync() {
counter++;
}
private final Lock lock = new ReentrantLock();
public void incrementLock() {
lock.lock();
try {
counter++;
} finally {
lock.unlock();
}
}
}
2.synchronized的实现和限制
synchronized是基于监视器锁(Monitor Lock)的同步原语,它保证了同一时刻只有一个线程可以访问被同步的代码块或方法。synchronized锁定的是对象和类,当作用于实例方法时,锁定的是调用该方法的对象实例;当作用于静态方法时,锁定的是方法所在的Class对象。
尽管synchronized使用起来相对简单直观,但它存在一些局限性:
- 不可中断:一旦线程进入了阻塞状态,就没有办法中断它,只能等待其他线程释放锁。
- 不可尝试:synchronized没有提供尝试获取锁的机制,线程只能直接获取锁或者阻塞。
- 不公平:synchronized不保证等待的线程获取锁的顺序,可能会产生“饥饿”现象。
public class SynchronizedExample {
public synchronized void syncMethod() {
// Critical section
}
public void syncBlock() {
synchronized (this) {
// Critical section
}
}
}
3. 高级同步:Lock接口简介
为了解决synchronized的一些局限性,Java 5引入了java.util.concurrent.locks.Lock接口,它提供了比synchronized更丰富的锁操作。它允许更灵活的结构,可以有选择地获取锁,在等待锁的时候可以响应中断,还可以尝试非阻塞地获取锁,或者在尝试获取锁时等待一段时间。
Lock接口提供了以下几个重要方法:
- lock():如果锁不可用,则当前线程将被阻塞。
- lockInterruptibly():如果当前线程未被中断,则获取锁。
- tryLock():仅在调用时锁空闲时才获取锁。
- tryLock(long time, TimeUnit unit):如果锁在给定等待时间内没有被另一个线程持有,且当前线程未被中断,则获取锁。
- unlock():释放锁。
public class LockExample {
private final Lock lock = new ReentrantLock();
public void performTask() {
lock.lock();
try {
// Critical section code
} finally {
lock.unlock();
}
}
public void taskWithTimeout() throws InterruptedException {
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// Critical section code
} finally {
lock.unlock();
}
} else {
// Unable to acquire the lock
}
}
}
使用Lock接口可以极大提高多线程处理的灵活性和效率。但它也有自己的缺点,比如需要程序员手动释放锁,这增加了编程的复杂性。
4.synchronized与Lock的比较
虽然synchronized和Lock在某些情况下可以互换使用,但它们之间存在明显的差异。了解这些差异对于正确并有效地使用同步机制非常关键。
4.1 可操作性和灵活性
- synchronized是Java内置的关键字,与语言的同步机制紧密集成,使用简单但配置固定。
- Lock是一个接口,ReentrantLock是常用的实现类,提供了更多高级功能和灵活的控制。
4.2 功能差异
- Lock接口提供了条件变量(Condition),分离了对象监控器的锁定和等待通知的机制,允许更加细粒度的控制。
- Lock还可以设置尝试获取锁的超时时间,或者让获取锁的操作变得可中断。
4.3 性能对比
- 在资源竞争不是特别激烈的场景下,两者性能相差无几。
- 当出现高度竞争时,Lock通常会提供比synchronized更优的性能。
4.4 代码可读性与维护性
- synchronized由于其简洁性,在代码的可读性上有优势。
- Lock使用虽然提供更多的控制,但也导致了代码的复杂性,需要显式地进行锁的管理。
public class SynchronizedAndLockComparison {
private int count = 0;
private Lock lock = new ReentrantLock();
public void incrementWithSync() {
synchronized(this) {
count++;
}
}
public void incrementWithLock() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
5.死锁问题及预防策略
死锁是多线程同步时最常见的问题之一,它发生在当多个线程互相等待对方释放资源时,导致所有线程都无法继续执行。在Java中预防和避免死锁是编程时必须考虑的问题。
5.1 死锁的产生条件
要发生死锁,以下四个条件必须同时满足:
- 互斥条件:资源不能被多个线程同时使用。
- 请求和保持条件:线程已经保持至少一个资源,并且正在等待获取额外的资源。
- 不可剥夺条件:资源只能由获得它的线程主动释放。
- 循环等待条件:存在一种线程资源的循环等待链。
5.2 死锁的预防与解决策略
-
预防策略:
- 破坏互斥条件:尽可能将资源设置为可共享。
- 破坏请求和保持条件:一次性申请所有资源。
- 破坏不可剥夺条件:如果请求的资源被占用,释放已获得的资源。
- 破坏循环等待条件:对资源采取有序分配策略,避免循环等待。
-
解决策略:
- 资源顺序申请:线程按照一定的顺序请求资源,这样就可以避免循环等待。
- 锁超时解决:使用tryLock()方法进行超时等待,可在不能获取所有所需资源时回滚至初态。
- 死锁检测和恢复:通过工具或代码定期检测死锁,一旦检测到死锁,主动中断或回滚部分线程,释放资源。
5.3 示例代码分析
下面是如何使用ReentrantLock的tryLock()方法来避免死锁的一个例子:
public class TryLockExample {
private Lock lock1 = new ReentrantLock();
private Lock lock2 = new ReentrantLock();
public void task1() {
while (true) {
// 尝试获取两把锁
boolean gotLock1 = lock1.tryLock();
boolean gotLock2 = lock2.tryLock();
if(gotLock1 && gotLock2) {
// 获取了所有必要的锁,执行任务
try {
// 处理任务
} finally {
lock1.unlock();
lock2.unlock();
}
break;
} else {
// 如果未能获取所有锁,则释放并再试
if (gotLock1) {
lock1.unlock();
}
if (gotLock2) {
lock2.unlock();
}
}
// 等待一段时间再试,避免过度竞争
try { Thread.sleep(1); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}
}
在这个示例中,通过tryLock()避免了死锁的可能性,因为线程会在无法获取所有所需锁时释放已获得的锁,并重新尝试,这样规避了等待循环。
6.Lock接口的高级功能和使用案例
Lock接口不仅提供了基本的锁定机制,而且还拥有一些高级功能,使得它成为了实现复杂同步策略的理想选择。其中的ReentrantLock类是一个非常典型的实现,它提供了许多synchronized所不具备的灵活性。
6.1 ReentrantLock的高级特性
- 公平锁:通过设置公平锁(Fairness Policy),可以确保长时间等待的线程将优先获得锁。
- 可重入性:线程可以多次获得已经持有的锁,这减少了死锁发生的机会。
- 锁的状态查询:可以调用isHeldByCurrentThread和getHoldCount方法来查询当前线程对锁的持有情况。
- 条件变量(Condition):每个ReentrantLock对象可以有一个或多个与之关联的Condition对象,使得单个锁可以在不同的等待集上进行精确的线程控制。
6.2 使用案例分析
以下是一个使用ReentrantLock实现生产者-消费者模型的例子:
public class ProducerConsumerExample {
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private Object[] buffer = new Object[10];
private int addIndex, removeIndex, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while(count == buffer.length) {
// 等待notFull条件
notFull.await();
}
buffer[addIndex] = x;
if(++addIndex == buffer.length) {
addIndex = 0;
}
++count;
// 通知notEmpty条件的线程
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while(count == 0) {
// 等待notEmpty条件
notEmpty.await();
}
Object x = buffer[removeIndex];
if(++removeIndex == buffer.length) {
removeIndex = 0;
}
--count;
// 通知notFull条件的线程
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
在上述代码中,使用了Lock和Condition来进行精确的条件控制,这样就可以精确地控制线程的挂起和唤醒,更加有效地管理线程间的协作。
7. 其他同步工具:从ReentrantLock到StampedLock
除了synchronized和Lock接口,Java在并发包java.util.concurrent中提供了更多的同步工具,这些工具提供了不同的锁机制来满足不同场景的需求。了解这些工具的特性和使用场景对于构建高效、健壮的并发应用至关重要。
7.1 ReentrantReadWriteLock
ReentrantReadWriteLock允许同时有多个读线程访问资源,但是写操作是独占的。这种锁适用于读多写少的场景,可以提高并发性能。
public class ReadWriteLockExample {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private Map<String, String> dictionary = new HashMap<>();
public void put(String key, String value) {
rwLock.writeLock().lock();
try {
dictionary.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}
public String get(String key) {
rwLock.readLock().lock();
try {
return dictionary.get(key);
} finally {
rwLock.readLock().unlock();
}
}
}
7.2 StampedLock
Java 8引入了StampedLock,这个类在乐观读和悲观锁之间提供了一种平衡。通过使用方法返回的一个戳(stamp),可以在操作数据之后进行锁状态的检查,这可以在没有线程竞争时提高性能。
public class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
void move(double deltaX, double deltaY) {
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
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);
}
}
7.3 小结
每个同步工具都有其特定的使用场景。ReentrantLock提供了很多高级的同步功能,ReentrantReadWriteLock适用于读多写少的情况,而StampedLock提供了更细粒度的控制,并能在某些场景下提高性能。 在选择合适的同步工具时,应考虑任务的性质、线程间的工作模式以及性能需求。通常,使用高级同步工具可以带来更好的性能和更高的灵活度,但同时也应当注意它们可能带来的复杂性。