侧边栏壁纸
博主头像
翻斗

开始一件事最好是昨天,其次是现在

  • 累计撰写 44 篇文章
  • 累计创建 42 个标签
  • 累计收到 2 条评论

Java中的锁对象介绍

翻斗
2020-03-01 / 0 评论 / 0 点赞 / 646 阅读 / 4,277 字

Baeldung 翻译系列之Java并发基础:

Java中的concurrent包
Java中的Synchronized关键字
Future介绍
ThreadLocal介绍
Java线程的生命周期
如何杀掉一个Java线程
Java中的线程池介绍
实现Runnable接口还是继承Thread类
Java中的wait和notify方法
Runnable vs Callable
wait和sleep的区别
Thread.join方法介绍
Java中使用锁对象
ThreadPoolTaskExecutor中的corePoolSize和maxPoolSize
Java中的异步编程

1. 概览

本教程中,我们会看到在Java中实现 互斥锁(mutex) 的不同方法

2. 互斥锁(Mutex)

在多线程应用程序中,两个或多个线程可能需要同时访问共享资源,从而导致意外行为。这些共享资源的示例包括数据结构、输入输出设备、文件和网络连接。

我们将这种情况称为竞态条件race condition)。程序中访问共享资源的部分被称为临界区critical section)。为了避免竞态条件,我们需要对对临界区的访问进行同步。

互斥锁(mutex,或称为互斥量)是最简单的同步器类型,它确保一次只有一个线程可以执行计算机程序的临界区。

要访问临界区,线程先获取互斥锁,然后访问临界区,最后释放互斥锁。在此期间,所有其他线程都会阻塞,直到互斥锁被释放。一旦一个线程退出临界区,另一个线程就可以进入临界区。

3. 为何使用互斥锁?

首先,让我们以一个SequenceGenerator类的示例开始,该类通过每次将currentValue增加一来生成下一个序列:

public class SequenceGenerator {
    
    private int currentValue = 0;

    public int getNextSequence() {
        currentValue = currentValue + 1;
        return currentValue;
    }

}

现在,让我们创建一个测试用例,看看当多个线程同时尝试访问它时,这个方法的行为如何:

@Test
public void givenUnsafeSequenceGenerator_whenRaceCondition_thenUnexpectedBehavior() throws Exception {
    int count = 1000;
    Set<Integer> uniqueSequences = getUniqueSequences(new SequenceGenerator(), count);
    Assert.assertEquals(count, uniqueSequences.size());
}

private Set<Integer> getUniqueSequences(SequenceGenerator generator, int count) throws Exception {
    ExecutorService executor = Executors.newFixedThreadPool(3);
    Set<Integer> uniqueSequences = new LinkedHashSet<>();
    List<Future<Integer>> futures = new ArrayList<>();

    for (int i = 0; i < count; i++) {
        futures.add(executor.submit(generator::getNextSequence));
    }

    for (Future<Integer> future : futures) {
        uniqueSequences.add(future.get());
    }

    executor.awaitTermination(1, TimeUnit.SECONDS);
    executor.shutdown();

    return uniqueSequences;
}

执行这个测试用例后,我们可以看到大部分时间测试用例会失败,失败的原因类似于:

java.lang.AssertionError: expected:<1000> but was:<989>
  at org.junit.Assert.fail(Assert.java:88)
  at org.junit.Assert.failNotEquals(Assert.java:834)
  at org.junit.Assert.assertEquals(Assert.java:645)

uniqueSequences的大小应该等于我们在测试用例中执行getNextSequence方法的次数。然而,由于竞态条件,实际情况并非如此。显然,我们不希望出现这种行为。

因此,为了避免这种竞态条件,我们需要确保只有一个线程可以同时执行getNextSequence方法。在这种情况下,我们可以使用互斥锁来同步线程。

在Java中,有多种实现互斥锁的方式。接下来,我们将看到为SequenceGenerator类实现互斥锁的不同方法。

4. 使用synchronized关键字

首先,我们将讨论synchronized关键字,这是在Java中实现互斥锁最简单的方法。

Java中的每个对象都有一个与之关联的内在锁(intrinsic lock)。synchronized方法和synchronized块使用这个内在锁来限制对临界区的访问,确保一次只有一个线程可以访问。

因此,当一个线程调用synchronized方法或进入synchronized块时,它会自动获取锁。锁会在方法或块执行完成,或者从它们中抛出异常时释放。

让我们通过添加synchronized关键字来将getNextSequence方法改造为带有互斥锁的方法:

public class SequenceGeneratorUsingSynchronizedMethod extends SequenceGenerator {
    
    @Override
    public synchronized int getNextSequence() {
        return super.getNextSequence();
    }
}

synchronized块类似于synchronized方法,但对临界区和用于锁定的对象有更多的控制。

现在,让我们看看如何使用synchronized块来在自定义互斥锁对象上进行同步:

public class SequenceGeneratorUsingSynchronizedBlock extends SequenceGenerator {
    
    private Object mutex = new Object();

    @Override
    public int getNextSequence() {
        synchronized (mutex) {
            return super.getNextSequence();
        }
    }
}

5. 使用ReentrantLock

ReentrantLock类在Java 1.5中引入,它提供比synchronized关键字更灵活和可控的方式。

让我们看看如何使用ReentrantLock实现互斥锁:

public class SequenceGeneratorUsingReentrantLock extends SequenceGenerator {
    
    private ReentrantLock mutex = new ReentrantLock();

    @Override
    public int getNextSequence() {
        try {
            mutex.lock();
            return super.getNextSequence();
        } finally {
            mutex.unlock();
        }
    }
}

6. 使用信号量(Semaphore)

ReentrantLock类似,Semaphore类也是在Java 1.5中引入的。

在互斥锁的情况下,只有一个线程可以访问临界区,而Semaphore允许一定数量的线程访问临界区。因此,我们也可以通过将Semaphore的允许线程数设置为1来实现互斥锁。

现在,让我们使用Semaphore创建另一个线程安全版本的SequenceGenerator

public class SequenceGeneratorUsingSemaphore extends SequenceGenerator {
    
    private Semaphore mutex = new Semaphore(1);

    @Override
    public int getNextSequence() {
        try {
            mutex.acquire();
            return super.getNextSequence();
        } catch (InterruptedException e) {
            // 异常处理代码
        } finally {
            mutex.release();
        }
    }
}

7. 使用Guava的Monitor类

到目前为止,我们已经看到了使用Java提供的特性来实现互斥锁的选项。

然而,Google的Guava库中的Monitor类是ReentrantLock类的更好替代方案。根据其文档,使用Monitor的代码比使用ReentrantLock的代码更易读,更不容易出错。

首先,我们将添加GuavaMaven依赖:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.0.1-jre</version>
</dependency>

现在,我们将使用Monitor类编写另一个SequenceGenerator的子类:

public class SequenceGeneratorUsingMonitor extends SequenceGenerator {
    
    private Monitor mutex = new Monitor();

    @Override
    public int getNextSequence() {
        mutex.enter();
        try {
            return super.getNextSequence();
        } finally {
            mutex.leave();
        }
    }
}

8. 总结

在本教程中,我们介绍了互斥锁的概念,并看到了在Java中实现互斥锁的不同方式。

与往常一样,本教程中使用的示例代码的完整源代码可以在GitHub 或者 Gitee上找到。

0

评论区