『深入学习Java』(四) Java锁基础

281 阅读2分钟

『深入学习Java』(四) Java锁基础.md

前言

前面记录了多线程的一些基本操作,这一小节来学习 Java 中锁相关知识。

什么叫锁?

锁是一种控制多线程访问共享资源的工具。 通常,锁提供对共享资源的独占访问:一次只有一个线程可以获取锁,并且对共享资源的所有访问都需要首先获取锁synchronized提供了对与每个对象关联的隐式监视器锁的访问。

以上是官方解释。

通俗来讲,锁可以理解成"坑位",也即一个坑位同时只能有一个人蹲,一个人蹲着其他人就得等着。

为什么需要锁?

一个模拟并发扣减库存的示例

int stockNum = 10;
    @Test
    public void test01() throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                Thread thread11 = new Thread(() -> {
//                    synchronized (this) {
                        if (stockNum <= 0) {
                            return;
                        }
                        int newStockNum = stockNum - 1;

                        // 模拟一些更新DB数据操作
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                        stockNum = newStockNum;
                        log.info("{} 购买成功。,剩余库存:{}。", Thread.currentThread().getName(), stockNum);
//                    }
                });
                thread11.start();
            }
        });

        thread1.start();
        Thread.sleep(5000);
        log.info("库存剩余:{}", stockNum);
    }

执行结果

16:30:37.902 [Thread-322] INFO ThreadLockTest - Thread-322 购买成功。,剩余库存:9。
16:30:37.902 [Thread-3574] INFO ThreadLockTest - Thread-3574 购买成功。,剩余库存:2。
16:30:37.902 [Thread-3589] INFO ThreadLockTest - Thread-3589 购买成功。,剩余库存:2。
16:30:37.901 [Thread-3598] INFO ThreadLockTest - Thread-3598 购买成功。,剩余库存:2。
16:30:37.904 [Thread-2919] INFO ThreadLockTest - Thread-2919 购买成功。,剩余库存:4。
16:30:37.902 [Thread-3602] INFO ThreadLockTest - Thread-3602 购买成功。,剩余库存:2。
16:30:37.901 [Thread-2923] INFO ThreadLockTest - Thread-2923 购买成功。,剩余库存:4。
16:30:37.902 [Thread-3578] INFO ThreadLockTest - Thread-3578 购买成功。,剩余库存:2。
16:30:37.902 [Thread-3577] INFO ThreadLockTest - Thread-3577 购买成功。,剩余库存:2。
16:30:37.902 [Thread-3585] INFO ThreadLockTest - Thread-3585 购买成功。,剩余库存:2。
16:30:37.901 [Thread-417] INFO ThreadLockTest - Thread-417 购买成功。,剩余库存:9。
16:30:37.901 [Thread-3597] INFO ThreadLockTest - Thread-3597 购买成功。,剩余库存:2。
16:30:37.902 [Thread-3573] INFO ThreadLockTest - Thread-3573 购买成功。,剩余库存:2。
16:30:37.902 [Thread-3590] INFO ThreadLockTest - Thread-3590 购买成功。,剩余库存:2。
........省略

我们可以看到……库存已经卖超了。如果真实业务系统,发生大规模库存超卖情况,后果是不堪设想的。

我们将注释的 synchronized (this)的放开,然后重新执行:

16:37:37.037 [Thread-2] INFO ThreadLockTest - Thread-2 购买成功。,剩余库存:916:37:37.158 [Thread-1966] INFO ThreadLockTest - Thread-1966 购买成功。,剩余库存:816:37:37.268 [Thread-1963] INFO ThreadLockTest - Thread-1963 购买成功。,剩余库存:716:37:37.380 [Thread-1964] INFO ThreadLockTest - Thread-1964 购买成功。,剩余库存:616:37:37.490 [Thread-1962] INFO ThreadLockTest - Thread-1962 购买成功。,剩余库存:516:37:37.601 [Thread-1961] INFO ThreadLockTest - Thread-1961 购买成功。,剩余库存:416:37:37.711 [Thread-1960] INFO ThreadLockTest - Thread-1960 购买成功。,剩余库存:316:37:37.821 [Thread-1959] INFO ThreadLockTest - Thread-1959 购买成功。,剩余库存:216:37:37.937 [Thread-1958] INFO ThreadLockTest - Thread-1958 购买成功。,剩余库存:116:37:38.057 [Thread-1957] INFO ThreadLockTest - Thread-1957 购买成功。,剩余库存:016:37:41.943 [main] INFO ThreadLockTest - 库存剩余:0

这就完美符合了我们的需要,当库存卖到零的时候,就不继续进行售卖了。

synchronized 的使用姿势

synchronized 使用在方法上或代码块上。

  1. 使用在方法上
    • 普通方法,锁为对象实例。
    • 静态方法,锁为对象Class。
// 普通方法。
public synchronized void test01() {
    ...
}
// 静态方法。
public static synchronized void test01() {
    ...
}
  1. 使用在代码中
public void test01() {
   synchronized(锁对象) {
        ...
    }
}

只有 synchronized?使用 Lock 锁

Java 中除了提供 synchronized 这种自动加解锁的特性外,还提供了可以手动加解锁的 Lock 对象。

大多数情况下,应该按照以下方式来使用:

Lock l = ...;
l.lock();
try {
  // access the resource protected by this lock
} finally {
  l.unlock();
}

这么做的主要目的是,防止 l.unlock(); 报错。

我们将文章开头的例子修改为 Lock 模式,也可以得到相同的结果。

Thread thread11 = new Thread(() -> {
    lock.lock();
    try {
        if (stockNum <= 0) {
            return;
        }
        int newStockNum = stockNum - 1;
        // 模拟一些更新DB数据操作
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        stockNum = newStockNum;
        log.info("{} 购买成功。,剩余库存:{}。", Thread.currentThread().getName(), stockNum);
    } finally {
        lock.unlock();
    }
});
thread11.start();

Java 的 Lock 体系

Lock 与 ReadWriteLock 接口

Lock

public interface Lock {

    void lock();

    void lockInterruptibly() throws InterruptedException;

    boolean tryLock();

    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    void unlock();

    Condition newCondition();
}
  • lock() - 获取锁。如果获取不到,则阻塞等待。
  • lockInterruptibly() - 获取锁,并支持阻塞等待中断。即如果这个线程正在等待锁,但是被另外一个线程中断了,这个方法会抛出InterruptedException以结束锁等待。
  • tryLock()/tryLock(long time, TimeUnit unit) - 尝试加锁。加锁成功返回 True,失败返回 Flase,不进行锁等待/或只进行指定时间的等待,超时后抛出InterruptedException
  • unlock() - 释放锁。
  • newCondition() - 为锁设定"wait-sets(等待池)"。该方法可以调用多次,即设定多个等待池。

ReadWriteLock

读写锁,针对读写行为分别加锁。不过多描述了。

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

ReentrantLock

ReentrantLock implements Lock 。可重入的互斥锁,语义基本和synchronized 一致。

基本特性:可重入、可中断、可设定锁等待超时时间、可设置为公平锁、支持多个条件变量。

前几个基本特性,已经在上一小节描述过了。这里想着重说一下条件变量。

条件变量,就是为线程设置一个"休息室"。

当不满足条件时,线程进入"休息室"休息;满足条件时,从把线程从"休息室"喊出来。

所以与notify()/wait() 的语义是相似的。

下面的示例,模拟了一个数据缓冲区,当数据列表大小为100,开始消费。消费结束后,开始接受数据。

    final Lock lock = new ReentrantLock();
    final Condition addCondition = lock.newCondition();
    final Condition getCondition = lock.newCondition();
    public List<String> data = new ArrayList<>();

    @Test
    public void conditionTest() throws InterruptedException {

        // 获取数据
        new Thread() {
            @Override
            public void run() {
                lock.lock();
                try {
                    // 如果缓冲区里没有数据,开始等待
                    while (data.size() == 0) {
                        getCondition.await();
                    }
                    log.info("开始取数据。");
                    data.clear();
                    addCondition.signal();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    lock.unlock();
                }
            }
        }.start();

        // 添加数据
        new Thread() {
            @Override
            public void run() {
                lock.lock();
                try {
                    while (data.size() == 100) {
                        addCondition.await();
                    }
                    log.info("开始添加数据。");
                    while (data.size() < 100) {
                        data.add("");
                    }
                    getCondition.signal();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    lock.unlock();
                }
            }
        }.start();
    }

ReentrantReadWriteLock

ReentrantReadWriteLock implements ReadWriteLock

可重入读写锁的基本特性与ReentrantLock 一致。也不多说了。

StampedLock

Java 8 中引入了 StampedLock 。它除了普通的锁,还支持读锁和写锁。加锁方法返回一个标志戳,用于释放锁或检查锁是否仍然有效。

  • 写锁 writeLock()会阻塞等待、独占访问。返回值可以在方法unlockWrite() 作为入参释放锁。持有写锁时,无法获得读锁,并且所有乐观读验证都会失败。
  • 读锁 readLock 会阻塞等待、非独占访问,返回值可以在方法 unlockRead() 作为入参释放锁。
  • 乐观读锁 当未持有写锁时,tryOptimisticRead 方法返回锁标志戳。如果获取锁标志戳后,又持有了写锁validate 方法会返回 flase。 在乐观模式下读取的字段可能非常不一致,因此仅当您对数据表示足够熟悉以检查一致性和/或重复调用方法validate()时才适用

小结

这一小节,主要对 Java 中锁的基本知识做了一些总结。

其中包含了 synchronized 、ReentrantLock、ReentrantReadWriteLock、StampedLock 的基本知识。