ReentrantLock 即可重入互斥锁
synchronized关键字是基于代码块的方式进行加锁和解锁~–>【JavaEE】Synchronized原理分析
而ReentrantLock则是提供了lock和unlock方法来进行加锁和解锁
在大部分情况下使用Synchronized就行了,但是ReentrantLock也是一个重要补充:
(1)Synchronized只是加锁和解锁,在加锁时发现锁被占用的话就只能阻塞等待,而ReentrantLock还提供了一个tryLock,如果加锁成功了不会有啥事,如果加锁失败了则不会阻塞,而是直接返回false,让程序员更加灵活的决定接下来咋做
(2)Synchronized是一个非公平锁(概率均等,不遵守先来后到),而ReentrantLock则提供了公平锁和非公平锁两种工作模式(在构造方法中传入true开启公平锁)
// ReentrantLock 的构造方法
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
(3)Synchronized搭配wait\notify进行等待唤醒,如果多个线程wait一个对象,notify的时候时随即唤醒一个。而ReentrantLock则是搭配Condition这个类,功能更强大
信号量(Semaphore), 用来表示 “可用资源的个数”. 本质上就是一个计数器
eg:
可以把信号量想象成是停车场的展示牌: 当前有车位 100 个. 表示有 100 个可用资源. 当有车开进去的时候, 就相当于申请一个可用资源, 可用车位就 -1 (这个称为信号量的 P 操作)当有车开出来的时候, 就相当于释放一个可用资源, 可用车位就 +1 (这个称为信号量的 V 操作)如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源.
注意!!!Semaphore 的 PV 操作中的加减计数器操作都是原子的, 可以在多线程环境下直接使用
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();
}
同时等待 N 个任务执行结束.
好像跑步比赛,多个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩
eg:
构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成.
每个任务执行完毕, 都调用 latch.countDown() . 在 CountDownLatch 内部的计数器同时自减.
主线程中使用 latch.await(); 阻塞等待所有任务执行完毕. 相当于计数器为 0 了.
public class Demo {
public static void main(String[] args) throws Exception {
CountDownLatch latch = new CountDownLatch(10);
Runnable r = new Runable() {
@Override
public void run() {
try {
Thread.sleep(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("比赛结束");
}
}
ArrayList、LinkedList、HashMap、PriorityQueue…等等都存在线程不安全的问题,如果在多线程场景下就会出现问题。
如果我们必须使用多线程怎么办?
(1)直接手动保证—>加锁
比如修改ArrayList中的值,我们可以给修改操作加锁
(2)标准库中提供了线程安全的集合类
(1)Collections.synchronizedList(new ArrayList);
synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List.
synchronizedList 的关键操作上都带有 synchronized
(2)CopyOnWriteArrayList
:支持“写时拷贝”的集合类
当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
CopyOnWriteArrayList
虽然不加锁,但是频繁拷贝开销很大。适合于修改不频繁,元素个数较少
在多线程环境下使用哈希表可以使用:
(1)Hashtable
(2)ConcurrentHashMap
HashTable
只是简单的在关键方法上加上了synchronized
图示:
HashTable是对整个HashMap加锁,任何的增删改查操作都会加锁,也就更容易引发锁竞争!!
而由于HashTable只有一把锁,所以如果是两个线程,这两个线程访问HashTable中的任何数据都会产生锁竞争
ConcurrentHashMap每个哈希桶(链表)都有一把锁,只有两个线程访问同一个哈希桶中的数据才会触发锁竞争
扩展:
相关面试题
1) ConcurrentHashMap的读是否要加锁,为什么?
读操作没有加锁. 目的是为了进一步降低锁冲突的概率. 为了保证读到刚修改的数据, 搭配了
volatile 关键字.
2) 介绍下 ConcurrentHashMap的锁分段技术?
这个是 Java1.7 中采取的技术. Java1.8 中已经不再使用了. 简单的说就是把若干个哈希桶分成一个
“段” (Segment), 针对每个段分别加锁.
目的也是为了降低锁竞争的概率. 当两个线程访问的数据恰好在同一个段上的时候, 才触发锁竞争.
3) ConcurrentHashMap在jdk1.8做了哪些优化?
取消了分段锁, 直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头结点对象作为锁对
象).
将原来 数组 + 链表 的实现方式改进成 数组 + 链表 / 红黑树 的方式. 当链表较长的时候(大于等于
8 个元素)就转换成红黑树.
4) Hashtable和HashMap、ConcurrentHashMap 之间的区别?
HashMap: 线程不安全. key 允许为 null
Hashtable: 线程安全. 使用 synchronized 锁 Hashtable 对象, 效率较低. key 不允许为 null.
ConcurrentHashMap: 线程安全. 使用 synchronized 锁每个链表头结点, 锁冲突概率低, 充分利用
CAS 机制. 优化了扩容方式. key 不允许为 null