ReentrantLock是可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全.
我们从字面意思上就可以知道 “Reentrant” 这个单词的原意就是 "可重入”,
所以ReentrantLock 也是可重入锁.
lock(): 加锁, 如果获取不到锁就死等.
trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
unlock(): 解锁
代码示例如下:
ReentrantLock lock = new ReentrantLock();
-----------------------------------------
lock.lock();
try {
// working
} finally {
lock.unlock()
}
synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现).ReentrantLock 是标准库的一个类, 在 JVM 外实现的(基于 Java 实现).
synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活,但是也容易遗漏 unlock.
synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.
synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启公平锁模式.
下面是 ReentrantLock 的构造方法,我们从中也可以看出这是一个公平锁(先来后到为公平)
/ ReentrantLock 的构造方法
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
在实际应用中我们该如何选择锁呢?
锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便.
锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等.
如果需要使用公平锁, 使用 ReentrantLock
原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个
AtomicBoolean
AtomicInteger
AtomicIntegerArray
AtomicLong
AtomicReference
AtomicStampedReference
这里以 AtomicInteger 举例,常见方法有:
addAndGet(int delta); i += delta;
decrementAndGet(); --i;
getAndDecrement(); i--;
incrementAndGet(); ++i;
getAndIncrement(); i++;
关于线程池这儿的内容大家可以去博主下面的这篇文章进行了解
【JavaEE初阶】 线程池详解与实现
信号量, 用来表示 “可用资源的个数”. 本质上就是一个计数器
这里博主给大家举个例子,方便大家理解;
可以把信号量想象成是停车场的展示牌: 当前有车位 100 个. 表示有 100 个可用资源.
当有车开进去的时候, 就相当于申请一个可用资源, 可用车位就 -1 (这个称为信号量的 P 操作)
当有车开出来的时候, 就相当于释放一个可用资源, 可用车位就 +1 (这个称为信号量的 V 操作)
如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源
由于Semaphore 的 PV 操作中的加减计数器操作都是原子的, 可以在多线程环境下直接使用.
代码示例如下:
创建 Semaphore 示例, 初始化为 4, 表示有 4 个可用资源.
acquire 方法表示申请资源(P操作), release 方法表示释放资源(V操作)
创建 20 个线程, 每个线程都尝试申请资源, sleep 1秒之后, 释放资源. 观察程序的执行效果.
实现代码入下:
import java.util.concurrent.Semaphore;
public class ThreadDemo3 {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(4);
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
System.out.println("申请资源");
semaphore.acquire();
System.out.println("我获取到资源了");
Thread.sleep(1000);
System.out.println("我释放资源了");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 20; i++) {
Thread t = new Thread(runnable);
t.start();
}
}
}
CountDownLatch的作用为:同时等待 N 个任务执行结束
就好像跑步比赛,10个选手依次就位,哨声响才同时出发;
但是每个选手的实力我们不知道,不知道谁最后到达
而我们需要所有选手都通过终点,才能公布成绩
CountDownLatch就可以起到等待他们全部跑完的效果
接下来我们依旧用代码实例进行讲解:
构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成.
每个任务执行完毕, 都调用 latch.countDown() . 在 CountDownLatch 内部的计数器同时自减.
主线程中使用 latch.await(); 阻塞等待所有任务执行完毕. 相当于计数器为 0
代码示例如下:
import java.util.concurrent.CountDownLatch;
public class ThreadDemo4 {
public static void main(String[] args) throws Exception {
CountDownLatch latch = new CountDownLatch(10);
Runnable r = new Runnable() {
public void run() {
try {
Thread.sleep((long) (Math.random() * 10000));
latch.countDown();
} catch (Exception e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 10; i++) {
new Thread(r).start();
}
// 必须等到 10 人全部回来
latch.await();
System.out.println("比赛结束");
}
}
synchronized, ReentrantLock, Semaphore 等都可以用于线程同步.
以 juc 的 ReentrantLock 为例,
synchronized 使用时不需要手动释放锁. ReentrantLock使用时需要手动释放. 使用起来更 灵活,
synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时 间就放弃.
synchronized 是非公平锁, ReentrantLock 默认是非公平锁.可以通过构造方法传入一个 true 开启公平锁模式.
synchronized 是通过 Object 的 wait / notify实现等待-唤醒. 每次唤醒的是一个随机等待的 线程. ReentrantLock 搭配 Condition 类实现等待-唤醒,可以更精确控制唤醒某个指定的线程.
基于 CAS 机制. 伪代码如下:
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}
执行过程参考博主写的【JavaEE初阶】 CAS详解
信号量, 用来表示 “可用资源的个数”. 本质上就是一个计数器.
使用信号量可以实现 “共享锁”, 比如某个资源允许 3 个线程同时使用, 那么就可以使用 P 操作作为加锁, V 操作作为解锁, 前三个线程的 P 操作都能顺利返回, 后续线程再进行 P 操作就会阻塞等待,直到前面的线程执行了 V 操作.
具体实现可以去博主所写的【JavaEE初阶】 线程池详解与实现进行查看
关于《【JavaEE初阶】 JUC(java.util.concurrent) 的常见类》就讲解到这儿,感谢大家的支持,欢迎各位留言交流以及批评指正,如果文章对您有帮助或者觉得作者写的还不错可以点一下关注,点赞,收藏支持一下!