文章目录

可重入锁 :ReentrantLock
ReentrantLock(再入锁、可重入锁)是 Java 5 提供的锁实现,它的功能和 synchronized 基本相同。ReentrantLock 通过调用 lock 方法来获取锁,通过调用 unlock 来释放锁。
1. ReentrantLock 使用
ReentrantLock 基础使用 ,代码如下:
Lock lock = new ReentrantLock();
lock.lock(); // 加锁
// 业务代码...
lock.unlock(); // 解锁
使用 ReentrantLock 完善本文开头的非线程安全代码:
public class LockTest {
static int number = 0;
public static void main(String[] args)
throws InterruptedException {
// ReentrantLock 使用
Lock lock = new ReentrantLock();
Thread thread1 = new Thread(() -> {
try {
lock.lock();
addNumber();
} finally {
lock.unlock();
}
});
Thread thread2 = new Thread(() -> {
try {
lock.lock();
addNumber();
} finally {
lock.unlock();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("number:" + number);
}
public static void addNumber() {
for (int i = 0; i < 10000; i++) {
++number;
}
}
}
-
尝试获取锁
ReentrantLock 可以无阻塞尝试访问锁,使用 ReentrantLock#tryLock 方法。
Lock reentrantLock = new ReentrantLock(); // 线程一 new Thread(() -> { try { reentrantLock.lock(); Thread.sleep(2 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { reentrantLock.unlock(); } }).start(); // 线程二 new Thread(() -> { try { Thread.sleep(1 * 1000); System.out.println(reentrantLock.tryLock()); // false Thread.sleep(2 * 1000); System.out.println(reentrantLock.tryLock()); // true } catch (InterruptedException e) { e.printStackTrace(); } }).start();
-
尝试一段时间内获取锁
ReentrantLock#lock 默认是以非阻塞方式获取锁,如果获取不到,则立即以 false 返回。
ReentrantLock#tryLock 有一个重载方法 ReentrantLock#tryLock(long timeout, TimeUnit unit),用于尝试一段时间内获取锁(而非立即返回)。
注意,在此期间,ReentrantLock 是一直尝试,而非等待一段时间后再试。
Lock reentrantLock = new ReentrantLock(); // 线程一 new Thread(() -> { try { reentrantLock.lock(); System.out.println(LocalDateTime.now()); Thread.sleep(2 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { reentrantLock.unlock(); } }).start(); // 线程二 new Thread(() -> { try { Thread.sleep(1 * 1000); System.out.println(reentrantLock.tryLock(3, TimeUnit.SECONDS)); System.out.println(LocalDateTime.now()); } catch (InterruptedException e) { e.printStackTrace(); } }).start();
以上代码执行结果如下:
2019-07-05 19:53:51 true 2019-07-05 19:53:53
可以看出锁在休眠了 2 秒之后,就被线程二直接获取到了,所以说 tryLock(long timeout, TimeUnit unit) 方法内的 timeout 参数指的是获取锁的最大等待时间。
2. ReentrantLock 注意事项
-
使用 ReentrantLock 一定要记得释放锁,否则该锁会被永久占用。
-
lock - unlock 应该成对出现,即,lock 了多少次,就要有相应多少次的 unlock 。
如果有需要,你可以通过 ReentrantLock#getHoldCount 方法查询当前线程执行 lock的次数
3. ReentrantLock 和 synchronized 有什么区别?
synchronized 和 ReentrantLock 都是保证线程安全的,它们的区别如下:
- ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
- ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
- ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等;
- ReentrantLock 性能略高于 synchronized。
条件等待变量:Condition
Java 中条件变量都实现了 java.util.concurrent.locks.Condition 接口,条件变量的实例化是通过一个 Lock 对象上调用 newCondition()
方法来获取的,这样,条件就和一个锁对象绑定起来了。因此,Java 中的条件变量只能和锁配合使用,来控制并发程序访问竞争资源的安全。
在 Condition 中,用 await()
替换 wait()
,用 signal()
替换 notify()
,用 signalAll()
替换 notifyAll()
,传统线程的通信方式,Condition 都可以实现。
再次提醒
Condition 是被绑定到 Lock 上的,要创建一个 Lock 的 Condition 必须用 newCondition() 方法。 这样看来,Condition 和传统的线程通信没什么区别,Condition 的强大之处在于它可以为多个线程间建立不同的 Condition 。
信号量:Semaphore
Java 通过 Semaphore 类实现了经典的信号量。信号量通过计数器来控制对共享资源的访问。
- 如果计数器大于 0 , 则允许访问;
- 如果计数器为 0 ,则不允许访问。
计数器的计数逻辑上代表着当前共享资源的许可证的数量。
Semaphore 类具有如下所示的两个构造函数:
Semaphore(int num)
Semaphore(int num, boolean how)
其中,num
指定了初始的许可证计数大小。因此,num 指定了任意时刻可以访问共享资源的线程数量。如果,num 是 1,那么任意时刻只有一个线程能够访问资源。
默认情况下,等待获取许可证的线程以随机的方式「抢夺」许可证。通过将 how
设置为 true ,可以确保等待的线程以前后顺序获得许可证。即,是否是公平锁。
为了得到许可证,可以调用 Semaphore#acquire 方法,该方法具有以下 2 种形式:
// 获得 1 个许可证
void acquire() throws InterruptedException
// 获得 num 个许可证
void acquire(int num) throws InterruptedException
如果调用时无法获得许可证,就会挂起线程(阻塞),直到许可证可以获得为止。
为了释放许可证,可以调用 Semaphore#release 方法,该方法具有以下 2 种形式:
// 释放 1 个许可证
void release()
// 释放 num 个许可证
void relase(int num)
注意
为了使用信号量控制对资源的访问,在访问资源之前,希望使用资源的每个线程必须先调用 acquire 方法;当线程使用完资源时,必须调用 release 方法。
原子值:Atomic
原子值(atomic)也是 JDK 1.5 的 J.U.C 特性引入的知识点。
如果多个线程更新一个共享计数器,那么你就需要保证更新操作是以线程安全的方式进行的。因为 i++
、i--
这样的操作是非原子性的,它们是线程不安全的。
JDK(从 1.5 开始) 在 java.util.concurrent.atomic 包下面为我们准备了很多可以高效、简洁地「对 int、long 和 boolean 值、对象的引用和数组进行原子性操作」的类。
1. 简单使用
以 AtomicLong 为例。
AtomicLong 的 .incrementAndGet 方法可以将 AtomicLong 对象的值加 1 ,并返回增加之后的值。即,实现 ++i
的逻辑,只不过比 ++i
更高级的是整个操作不能被打断,即,它是原子性的。
AtomicLong i = new AtomicLong(0);
System.out.println( i.incrementAndGet() );
AtomicLong 的各个方法的功能都是显而易见的,此处就不一一展示。
不过,需要注意的是,如果你想先读后写 AtomicLong 的值,不要使用 .get 和 .set 方法,因为它两的组合不是原子性的。你要使用的一个 .updateAndGet 方法来替代它们两个。.updateAndGet 方法要求你传入一个 lambda 表达式,在表达式中它会将 AtomicLong 的原值传进来,你在 lambda 表达式中返回新值。
2. AtomicReference
AtomicReference 类提供了一个可以原子读写的对象引用变量。 原子意味着尝试更改相同 AtomicReference 的多个线程(例如,使用比较和交换操作)不会使 AtomicReference 最终达到不一致的状态。 AtomicReference 甚至有一个先进的 .compareAndSet 方法,它可以将引用与预期值(引用)进行比较,如果它们相等,则在 AtomicReference 对象内设置一个新的引用。