目录
一、ReentrantLock
ReentrantLock也是可重入互斥锁。和synchronized定位类似,都是用来实现互斥效果,保证线程安全。
它的用法如下:
- lock():加锁,如果获取不到锁就死等。
- trylock(超时时间):加锁,如果获取不到锁,等待一定的时间之后就放弃加锁。
- unlock():解锁。
import java.util.concurrent.locks.ReentrantLock;
public class Demo1 {
static class Counter {
public int count = 0;
void increase() {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
ReentrantLock locker = new ReentrantLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<10000;i++){
locker.lock();
counter.increase();
locker.unlock();
}
}
});
Thread t2 = new Thread(){
@Override
public void run(){
for(int i=0;i<10000;i++){
locker.lock();
counter.increase();
locker.unlock();
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
上述是一个非常简单的代码,实现了两个线程各对count加10000。
ReentrantLock和synchronized的区别:
- synchronized是一个关键字,是JVM内部实现的(大概率是基于 C++ 实现);ReentrantLock是标准库的一个类,是在JVM外实现的(基于Java实现)。
- synchronized使用时不需要手动释放锁,ReentrantLock使用时需要手动释放,使用起来更灵活,但是也容易遗漏unlock。
- synchronized在申请锁失败时,会死等。ReentrantLock可以通过trylock的方式等待一段时间就放弃。synchronized是非公平锁,ReentrantLock默认是非公平锁,可以通过构造方法传入一个true开启公平锁模式。
// ReentrantLock 的构造方法
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
- synchronized是通过Object的wait/notify实现等待-唤醒,每次唤醒的是一个随机等待的线程。而ReentranLock搭配Condition类实现等待-唤醒,可以更精确控制唤醒某个指定的线程。
那么我们如何选择使用哪个锁呢?
- 锁竞争不激烈的时候,使用synchronized,效率更高,自动释放更方便。
- 锁竞争激烈的时候,使用ReentrantLock,搭配trylock更灵活控制加锁的行为,而不是死等。
- 如果需要使用公平锁,使用ReentrantLock。
二、信号量Semaphore
信号量,用来表示"可用资源的个数"。本质上就是一个计数器。我们可以把信号量想象成是停车场的展示牌,当前有车位100个,表示有100个可用资源。
当有车开进去的时候,就相当于申请一个可用资源,可用车位就-1(这个称为信号量的P操作);当有车开出来的时候,就相当于释放一个可用资源,可用车位就+1(这个称为信号量的V操作)。
如果计数器的值已经为0了,还尝试申请资源,就会阻塞等待,直到有其他线程释放资源。
Semaphore的PV操作中的加减计数器操作都是原子的,可以在多线程环境下直接使用。
我们创建代码来演示一下:
- 创建Semaphore示例,初始化为4,表示有4个可用资源。
- acquire方法表示申请资源(P操作),release方法表示释放资源(V操作)。
- 创建20个线程,每个线程都尝试申请资源,sleep1秒之后,释放资源。观察程序的执行效果。
import java.util.concurrent.Semaphore;
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
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实例,初始化10表示有10个任务需要完成。
- 每个任务执行完毕,都调用latch.countDown()。在CountDownLatch内部的计数器同时自减。
- 主线程中使用latch.await();阻塞等待所有任务执行完毕。相当于计数器为0了。
这个东西主要适用于多个线程来完成一系列任务的时候,用来衡量任务的进度是否完成。比如需要把一个大的任务,拆分成多个小的任务,让这些任务并发的去执行,就可以使用countDownLatch来判定说当前这些任务是否全都完成了。
CountDownLatch主要有两个方法:
- await:调用的时候就会阻塞,就会等待其他的线程完成任务,所有的线程都完成了任务之后,此时这个await才会返回,才会继续往下走。
- countDown:告诉countDownLatch,我当前这一个子任务已经完成了。
我们来看下代码呢:
import java.util.concurrent.CountDownLatch;
public class Demo1 {
public static void main(String[] args) throws Exception {
CountDownLatch latch = new CountDownLatch(10);
Runnable r = new Runnable() {
@Override
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等都可以用于线程同步。
为什么有了synchronized还需要juc下的 lock?
以juc的ReentrantLock为例:
- synchronized使用时不需要手动释放锁。ReentrantLock使用时需要手动释放。使用起来更灵活。
- synchronized在申请锁失败时,会死等。ReentrantLock可以通过trylock的方式等待一段时间就放弃。
- synchronized是非公平锁,ReentrantLock默认是非公平锁。可以通过构造方法传入一个true开启公平锁模式。
- synchronized是通过Object的wait/notify实现等待-唤醒。每次唤醒的是一个随机等待的线程。ReentrantLock搭配Condition类实现等待-唤醒,可以更精确控制唤醒某个指定的线程。
AtomicInteger的实现原理是什么?
基于CAS机制。伪代码如下:
class AtomicInteger { private int value; public int getAndIncrement() { int oldValue = value; while ( CAS(value, oldValue, oldValue+1) != true) { oldValue = value; } return oldValue; } }
信号量听说过么?之前都用在过哪些场景下?
信号量,用来表示"可用资源的个数"。本质上就是一个计数器。
使用信号量可以实现"共享锁",比如某个资源允许3个线程同时使用,那么就可以使用P操作作为加锁,V操作作为解锁,前三个线程的P操作都能顺利返回,后续线程再进行P操作就会阻塞等待,直到前面的线程执行了V操作。
四、线程安全的集合类
数据结构中大部分的集合类都是线程不安全的,针对这些线程不安全的集合类,要想在多线程环境下使用,就要考虑好线程安全问题了。
1、多线程环境使用ArrayList
标准库给我们提供了一些搭配的组件来保证线程安全:
Collections.synchronizedList(new ArrayList);
synchronizedList是标准库提供的一个基于synchronized进行线程同步的List。synchronizedList的关键操作上都带有synchronized。
这个东西会返回一个新的对象,这个新的对象就相当于给ArrayList套了一层壳,这层壳就是在方法上直接使用synchronized的。
2、使用CopyOnWriteArrayList
CopyOnWrite容器即写时复制的容器。当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
这个其实是与写时拷贝有关。比如,两个线程使用同一个ArrayList可能会读,也可能会修改。如果要是两个线程读,就直接读就好了。如果某个线程需要进行修改,就把ArrayList,复制出一份副本。修改线程,就修改这个副本,与此同时,另一个线程仍然可以读取数据(从原来的数据上进行读取)。一旦这边修改完毕,就会使用修改好的这份数据,替代掉原来的数据(往往就是一个引用赋值)。上述这个过程进行修改就不需要加锁了。
优点:
- 在读多写少的场景下,性能很高,不需要加锁竞争。
缺点:
- 占用内存较多。
- 新写的数据不能被第一时间读取到。
- 当前操作的ArrayList不能太大(拷贝成本不能太高)。
- 更适合于一个线程去修改,而不能多个线程同时修改(多个线程读,一个线程修改)。
3、多线程环境使用哈希表
1)Hashtable
Hashtable保证线程安全,主要就是给关键方法加上synchronized(相当于给this加锁)。
只要两个线程,在操作同一个Hashtable就会出现锁冲突。但是对于哈希表来说,锁不一定非得这么加,有些情况,其实是不涉及到线程安全问题的。
按照上述这样的方式来操作,并且在不考虑触发扩容的前提下操作不同的链表的时候就是线程安全的。相比之下,如果两个线程,操作的是同一个链表,才比较容易出现问题;如果两个线程,操作的是不同的链表,就根本不用加锁。只有说操作的同一个链表才需要加锁。
2)ConcurrentHashMap
相比于Hashtable,ConcurrentHashMap最核心的改进就是把一个全局的大锁,改进成了每个链表独立的一把小锁。这样做,大幅度降低了锁冲突的概率。一个hash表,有很多这样的链表,两个线程恰好同时访问一个链表的情况本身就比较少。其实ConcurrentHashMap就是把每个链表的头结点作为锁对象。
ConcurrentHashMap优化了扩容方式:化整为零
- 发现需要扩容的线程,只需要创建一个新的数组,同时只搬几个元素过去。
- 扩容期间,新老数组同时存在。
- 后续每个来操作ConcurrentHashMap的线程,都会参与搬家的过程。每个操作负责搬运一小部分元素。
- 搬完最后一个元素再把老数组删掉。这个期间,插入只往新数组加。
- 这个期间,查找需要同时查新数组和老数组。
3)相关面试题
- ConcurrentHashMap的读是否要加锁,为什么?
读操作没有加锁。目的是为了进一步降低锁冲突的概率,为了保证读到刚修改的数据,搭配了volatile关键字。
- 介绍下ConcurrentHashMap的锁分段技术?
这个是Java1.7中采取的技术。Java1.8中已经不再使用了。简单的说就是把若干个哈希桶分成一个"段"(Segment),针对每个段分别加锁。目的也是为了降低锁竞争的概率。当两个线程访问的数据恰好在同一个段上的时候,才触发锁竞争。
- ConcurrentHashMap在jdk1.8做了哪些优化?
取消了分段锁,直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头结点对象作为锁对象)。将原来数组+链表的实现方式改进成数组+链表/红黑树的方式。当链表较长的时候(大于等于8个元素)就转换成红黑树。
- Hashtable和HashMap、ConcurrentHashMap之间的区别?
- HashMap:线程不安全。key允许为null
- Hashtable:线程安全。使用synchronized锁Hashtable对象,效率较低。key不允许为null。
- ConcurrentHashMap:线程安全,使用synchronized锁每个链表头结点,锁冲突概率低,充分利用CAS机制。优化了扩容方式。key不允许为null。