一、Java锁机制的起点:synchronized

在Java编程里,多线程操作是很常见的。可当多个线程同时访问共享资源时,就容易出问题,像数据不一致啥的。这时候,就需要锁机制来保证数据的安全。synchronized 就是 Java 里最基础的锁机制。

基本用法

synchronized 可以修饰方法或者代码块。咱先看修饰方法的例子:

// Java 技术栈
class SynchronizedExample {
    private int count = 0;

    // 同步方法,保证同一时间只有一个线程能执行该方法
    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

在这个例子里,increment 方法被 synchronized 修饰了,这就意味着同一时间只能有一个线程执行这个方法。要是有其他线程想执行这个方法,就得等着当前线程执行完。

再看看修饰代码块的例子:

// Java 技术栈
class SynchronizedBlockExample {
    private int count = 0;
    private final Object lock = new Object();

    public void increment() {
        // 同步代码块,使用 lock 对象作为锁
        synchronized (lock) {
            count++;
        }
    }

    public int getCount() {
        return count;
    }
}

这里用了一个 lock 对象作为锁,把要同步的代码放在 synchronized 代码块里。这样,同一时间也只有一个线程能执行这个代码块。

应用场景

synchronized 适用于对共享资源的简单同步操作。比如多个线程同时对一个计数器进行操作,就可以用 synchronized 来保证计数器的正确性。

优缺点

优点:使用简单,能有效防止多个线程同时访问共享资源,保证数据的一致性。 缺点:性能比较低,因为它是一种独占锁,同一时间只能有一个线程访问被锁住的资源。而且一旦线程持有了锁,其他线程就得一直等待,可能会造成线程阻塞。

注意事项

使用 synchronized 时,要注意锁的范围。如果锁的范围太大,会影响性能;如果锁的范围太小,可能无法保证数据的一致性。

二、ReentrantLock:更灵活的锁

ReentrantLock 是 Java 里另一种常用的锁机制,它比 synchronized 更灵活。

基本用法

// Java 技术栈
import java.util.concurrent.locks.ReentrantLock;

class ReentrantLockExample {
    private int count = 0;
    private final ReentrantLock lock = new ReentrantLock();

    public void increment() {
        // 获取锁
        lock.lock();
        try {
            count++;
        } finally {
            // 释放锁,确保在任何情况下锁都会被释放
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }
}

在这个例子里,使用 ReentrantLocklock 方法获取锁,用 unlock 方法释放锁。要注意的是,释放锁的操作要放在 finally 块里,这样能保证无论是否发生异常,锁都会被释放。

应用场景

ReentrantLock 适用于需要更灵活控制锁的场景。比如可以使用 tryLock 方法尝试获取锁,如果获取不到可以做其他操作,而不是一直等待。

// Java 技术栈
import java.util.concurrent.locks.ReentrantLock;

class TryLockExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void doSomething() {
        if (lock.tryLock()) {
            try {
                // 获取到锁,执行操作
                System.out.println("获取到锁,执行操作");
            } finally {
                lock.unlock();
            }
        } else {
            // 没获取到锁,做其他操作
            System.out.println("没获取到锁,做其他操作");
        }
    }
}

优缺点

优点:比 synchronized 更灵活,可以实现公平锁,还能使用 tryLock 方法尝试获取锁。 缺点:使用起来相对复杂,需要手动获取和释放锁,如果忘记释放锁,会导致死锁。

注意事项

使用 ReentrantLock 时,一定要确保在 finally 块里释放锁,避免死锁。

三、ReadWriteLock:读写分离的锁

在很多场景下,读操作是可以同时进行的,只有写操作才需要互斥。ReadWriteLock 就是为了这种场景设计的。

基本用法

// Java 技术栈
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class ReadWriteLockExample {
    private int data = 0;
    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    public void writeData(int newData) {
        // 获取写锁
        lock.writeLock().lock();
        try {
            data = newData;
        } finally {
            // 释放写锁
            lock.writeLock().unlock();
        }
    }

    public int readData() {
        // 获取读锁
        lock.readLock().lock();
        try {
            return data;
        } finally {
            // 释放读锁
            lock.readLock().unlock();
        }
    }
}

在这个例子里,使用 ReentrantReadWriteLock 创建了一个读写锁。写操作使用写锁,同一时间只能有一个线程进行写操作;读操作使用读锁,多个线程可以同时进行读操作。

应用场景

适用于读多写少的场景,比如缓存系统。读操作可以同时进行,提高了并发性能。

优缺点

优点:在读多写少的场景下,能显著提高并发性能。 缺点:如果写操作比较频繁,会导致读操作长时间等待,性能下降。

注意事项

使用 ReadWriteLock 时,要根据实际情况合理分配读写锁的使用,避免写操作饥饿。

四、StampedLock:新一代的锁机制

StampedLock 是 Java 8 引入的一种新的锁机制,它结合了读写锁和乐观锁的特点。

基本用法

// Java 技术栈
import java.util.concurrent.locks.StampedLock;

class StampedLockExample {
    private int data = 0;
    private final StampedLock lock = new StampedLock();

    public void writeData(int newData) {
        // 获取写锁
        long stamp = lock.writeLock();
        try {
            data = newData;
        } finally {
            // 释放写锁
            lock.unlockWrite(stamp);
        }
    }

    public int readData() {
        // 乐观读
        long stamp = lock.tryOptimisticRead();
        int currentData = data;
        // 检查在读取过程中是否有写操作
        if (!lock.validate(stamp)) {
            // 有写操作,升级为悲观读
            stamp = lock.readLock();
            try {
                currentData = data;
            } finally {
                // 释放读锁
                lock.unlockRead(stamp);
            }
        }
        return currentData;
    }
}

在这个例子里,使用 StampedLock 进行读写操作。读操作先尝试乐观读,如果在读取过程中没有写操作,就直接返回数据;如果有写操作,就升级为悲观读。

应用场景

适用于读多写少,且读操作不要求实时性的场景。比如一些缓存数据的读取。

优缺点

优点:性能比 ReadWriteLock 更高,尤其是在读多写少的场景下。 缺点:使用起来比较复杂,不适合初学者。

注意事项

使用 StampedLock 时,要注意乐观读和悲观读的转换,避免出现数据不一致的问题。

文章总结

从 synchronized 到 StampedLock,Java 的锁机制不断演进,越来越灵活,性能也越来越好。synchronized 是最基础的锁机制,使用简单,但性能较低;ReentrantLock 更灵活,可以实现公平锁和尝试获取锁;ReadWriteLock 适用于读多写少的场景,提高了并发性能;StampedLock 结合了读写锁和乐观锁的特点,性能更高。在实际开发中,要根据具体的应用场景选择合适的锁机制。