文章目录
- 前言
- ReentrantLock
- 原子类
- 线程池
- 信号量
- CountDownLatch
- 相关面试题
前言
经过前面文章的学习我们大致了解了如何实现多线程编程和解决多线程编程中遇到的线程不安全问题,java.util.concurrent
是我们多线程编程的一个常用包,那么今天我将为大家分享 java.util.concurrent
包下的其他几种常见的类。
ReentrantLock
ReentrantLock 是可重入互斥锁,跟 synchronized 定位是类似的,都是用来保证线程安全的,但是 ReentrantLock 在某些方面相较于 synchronized 有突出的优势。
ReentrantLock 的加锁方式分为两种:
- lock():如果获取不到锁,线程就会进入阻塞等待状态
- tryLock():如果获取不到锁,就会放弃加锁,而不是进入阻塞等待状态
使用 ReentrantLock 的时候需要手动 unLock() 解锁,如果忘记了手动解锁这个操作,将会带来比较严重的后果。
所以为了解决有些时候忘记手动解锁的情况,往往需要借助 try-finally 来进行解锁操作。
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
//working
}finally {
lock.unlock();
}
ReentranLock 与 synchronized 相比的优势
-
ReentrantLock 在加锁的时候,有两种方式,lock() 和tryLock()。使用 lock() 的时候,如果未获取到锁就会进入阻塞等待状态,而使用 tryLock() 的话,如果没有获取到锁就会放弃获取,而不是进入阻塞等待
-
ReentrantLock 提供了公平锁的实现(默认情况下是非公平锁)
-
ReentranLock 提供了更强大的等待通知机制,搭配了Condition类,实现等待通知,可以指定唤醒某个线程
如何选择使用哪个锁?
- 锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便.
- 锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等.
- 如果需要使用公平锁, 使用 ReentrantLock.
通常情况下还是建议使用 synchronized,虽然 ReentrantLock 在某些方面具有优势,但是使用起来较麻烦,并且Java程序员在 synchronized 里面做了很多的优化。
原子类
原子类内部都是使用 CAS 操作实现的,因为 CAS 操作时原子性的,不需要进行加锁操作,所以性能要比加锁好很多。
- AtomicBoolean
- AtomicInteger
- AtomicIntegerArray
- AtomicLong
- AtomicReference
- AtomicStampedReference
原子类常见方法:
- addAndGet(int delta); —— i += delta;
- decrementAndGet(); —— --i;
- getAndDecrement(); —— i–;
- incrementAndGet(); —— ++i;
- getAndIncrement(); —— i++;
CAS 操作我在前面详细讲解过,大家想要了解的话可以去看看。CAS(Compare And Swap)操作
线程池
线程池是为了解决因线程频繁创建和销毁而造成的资源浪费问题。
使用线程池需要使用到 ExecutorService
和 Executors
两个类。
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
- ExecutorService 表示一个线程池实例.
- Executors 是一个工厂类, 能够创建出几种不同风格的线程池.
- ExecutorService 的 submit 方法能够向线程池中提交若干个任务
Executors 创建线程池的几种方式
- newFixedThreadPool: 创建固定线程数的线程池
- newCachedThreadPool: 创建线程数目动态增长的线程池.
- newSingleThreadExecutor: 创建只包含单个线程的线程池.
- newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer。
这里面也涉及到了工厂模式,大家可以去看看这篇文章了解一下【JavaEE】多线程案例-线程池
信号量
信号量(Semaphore)是一种实现任务间通信的机制,可以用于实现任务之间的同步或临界资源的互斥访问。它通常被用于协助一组相互竞争的任务来访问临界资源。
信号量是一个非负整数,获取信号量的任务会将该整数减1。当信号量为0时,所有试图获取该信号量的任务都将处于阻塞状态。信号量的值代表积累下来的释放信号量操作的次数。
申请资源的操作被称为 P 操作,释放资源的操作被称为 V 操作。
信号量可以分为二值信号量和计数信号量两种。二值信号量只有一个消息队列,队列有两种状态:空或满。而计数信号量可以看做长度大于1的消息队列,用于计数。信号量的计数值表示还有多少个事件未被处理。当某个事件发生时,任务或中断会释放一个信号量(将信号量计数值加1);当需要处理某个事件时,任务或中断会取走一个信号量(将信号量计数值减1)。
加锁和解锁的操作就可以看成是二值信号量的操作,当加锁的时候信号量就为0,释放锁的时候信号量就为1.
在Java代码中,信号量相关的操作被封装在 Semaphore
类中,acquire()
方法表示申请资源,release()
方法表示释放资源。
public class Test2 {
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(4);
semaphore.acquire();
System.out.println("获取资源");
semaphore.acquire();
System.out.println("获取资源");
semaphore.acquire();
System.out.println("获取资源");
semaphore.acquire();
System.out.println("获取资源");
semaphore.acquire();
System.out.println("获取资源");
semaphore.release();
System.out.println("释放资源");
}
}
当申请的资源量大于总的资源量的时候,线程就会进入阻塞等待状态,直到其他线程释放掉部分信号量。
CountDownLatch
CountDownLatch是Java中的一个同步工具类,用来协调多个线程之间的同步。它允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。
CountDownLatch通过一个计数器来实现这个功能,计数器的初始值通常设置为需要等待完成的线程数量。每当一个线程完成了自己的任务,计数器的值就会减1。当计数器的值达到0时,表示所有线程都已经完成任务,此时在CountDownLatch上等待的线程就可以恢复执行。
CountDownLatch可以用来确保某些活动在其他活动完成之前不会继续执行。例如,可以确保某个计算在其需要的所有资源都被初始化之后才继续执行,或者确保某个服务在其依赖的所有其他服务都已经启动之后才启动。
在Java中,可以使用CountDownLatch的countDown()方法来对计数器做减操作,就是告诉CountDownLatch我这个当前的任务完成了,使用await()方法等待计数器达到0。所有调用await()方法的线程都会被阻塞,直到计数器达到0或者等待线程被中断或者超时。
public class Demo2 {
private static int count;
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(10);
for(int i = 0; i < 10; i++) {
Thread t = new Thread(() -> {
try {
Thread.sleep((long)(Math.random() * 4000));
System.out.println(++count + "号完成比赛");
countDownLatch.countDown();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t.start();
}
countDownLatch.await();
System.out.println("结束比赛");
}
}
只有调用了10次 countDown()
方法之后,await()
方法才会结束等待,继续执行后面的代码。
需要注意的是,CountDownLatch是一次性的,一旦计数器的值达到0,就不能再次使用。如果需要多次使用类似的功能,可以考虑使用CyclicBarrier等其他同步工具类。
相关面试题
1) 线程同步的方式有哪些?
synchronized, ReentrantLock, Semaphore 等都可以用于线程同步.
2) 为什么有了 synchronized 还需要 juc 下的 lock?
以 juc 的 ReentrantLock 为例,
- synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活,
- synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.
- synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启公平锁模式.
- synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.
3) AtomicInteger 的实现原理是什么?
基于 CAS 机制. 伪代码如下:
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}
4) 信号量听说过么?之前都用在过哪些场景下?
信号量, 用来表示 “可用资源的个数”. 本质上就是一个计数器.
使用信号量可以实现 “共享锁”, 比如某个资源允许 3 个线程同时使用, 那么就可以使用 P 操作作为加锁, V 操作作为解锁, 前三个线程的 P 操作都能顺利返回, 后续线程再进行 P 操作就会阻塞等待,直到前面的线程执行了 V 操作