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 死锁的产生条件

要发生死锁,以下四个条件必须同时满足:

  1. 互斥条件:资源不能被多个线程同时使用。
  2. 请求和保持条件:线程已经保持至少一个资源,并且正在等待获取额外的资源。
  3. 不可剥夺条件:资源只能由获得它的线程主动释放。
  4. 循环等待条件:存在一种线程资源的循环等待链。

5.2 死锁的预防与解决策略

  • 预防策略:

    1. 破坏互斥条件:尽可能将资源设置为可共享。
    2. 破坏请求和保持条件:一次性申请所有资源。
    3. 破坏不可剥夺条件:如果请求的资源被占用,释放已获得的资源。
    4. 破坏循环等待条件:对资源采取有序分配策略,避免循环等待。
  • 解决策略:

    1. 资源顺序申请:线程按照一定的顺序请求资源,这样就可以避免循环等待。
    2. 锁超时解决:使用tryLock()方法进行超时等待,可在不能获取所有所需资源时回滚至初态。
    3. 死锁检测和恢复:通过工具或代码定期检测死锁,一旦检测到死锁,主动中断或回滚部分线程,释放资源。

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提供了更细粒度的控制,并能在某些场景下提高性能。 在选择合适的同步工具时,应考虑任务的性质、线程间的工作模式以及性能需求。通常,使用高级同步工具可以带来更好的性能和更高的灵活度,但同时也应当注意它们可能带来的复杂性。