文章目录
JUC(java.util.concurrent)的常见类
1. Callable interface
使用Callable也可以创建线程
-
Runnable能表示一个任务(run方法)
run方法返回的是void
-
Callable也能表示一个任务(call方法)
返回一个值, 类型可以用泛型参数来指定
如果关心多线程的执行过程, 使用Runnable即可.
如果是关心多线程的计算结果, 使用Callable更合适
代码示例: 创建线程计算1 + 2 + 3 + … + 1000
不使用Callable
-
创建一个类 Result , 包含一个 sum 表示最终结果, lock 表示线程同步使用的锁对象.
-
main 方法中先创建 Result 实例, 然后创建一个线程 t. 在线程内部计算 1 + 2 + 3 + … + 1000.
-
主线程同时使用 wait 等待线程 t 计算结束. (注意, 如果执行到 wait 之前, 线程 t 已经计算完了, 就不必等待了).
-
当线程 t 计算完毕后, 通过 notify 唤醒主线程, 主线程再打印结果
static class Result {
public int sum = 0;
public Object lock = new Object();
}
public static void main(String[] args) throws InterruptedException {
Result result = new Result();
Thread t = new Thread() {
@Override
public void run() {
int sum = 0;
for (int i = 1; i <= 1000; i++) {
sum += i;
}
synchronized (result.lock) {
result.sum = sum;
result.lock.notify();
}
}
};
t.start();
synchronized (result.lock) {
while (result.sum == 0) {
result.lock.wait();
}
System.out.println(result.sum);
}
}
可以看出, 上述代码比较复杂, 容易出错.
使用Callable
-
创建一个匿名内部类, 实现 Callable 接口. Callable 带有泛型参数. 泛型参数表示返回值的类型.
-
重写 Callable 的 call 方法, 完成累加的过程. 直接通过返回值返回计算结果.
-
把 callable 实例使用
FutureTask
包装一下. -
创建线程, 线程的构造方法传入
FutureTask
. 此时新线程就会执行FutureTask
内部的 Callable 的 call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中. -
在主线程中调用
futureTask.get()
能够阻塞等待新线程计算完毕. 并获取到FutureTask
中的结果.
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 1000; i++) {
sum += i;
}
return sum;
}
};
//这里的泛型和上面Callable的泛型保持一致
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
Integer result = futureTask.get();
System.out.println(result);
}
Callable
实例不能直接作为Thread类构造方法的参数, 需要借助一个辅助类FutureTask
2. ReentrantLock
可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全.
ReentrantLock
也是可重入锁. “Reentrant” 这个单词的原意就是 “可重入”
- lock(): 加锁, 如果获取不到锁就死等.
- unlock(): 解锁
ReentrantLock
有着synchronized
没有的独特优势:
-
提供了trylock方法进行加锁.
public boolean tryLock()
: 加锁失败直接返回falsepublic boolean tryLock(long timeout, TimeUnit unit)
- timeout: 等待锁的时间
- unit: timeout参数的时间单位
- 如果当前线程已经持有此锁,该方法返回true。
- 如果锁由另一个线程持有,那么当前线程将因线程调度目的而被禁用,并处于休眠状态,直到发生以下三种情况之一:
- 锁由当前线程获取: 则返回值true
- 其他线程中断当前线程: InterruptedException被抛出
- 指定的等待时间已过: 如果超过了指定的等待时间,则返回值false。如果时间小于或等于零,则该方法根本不会等待。
-
有两种模式, 可以工作在公平锁状态下, 也可以工作在非公平锁的状态下, 通过构造方法中参数的设定来决定(
synchronized
只能工作在非公平锁的状态下) -
ReentrantLock
也有等待通知的机制, 搭配Condition
类来完成. 比wait notify功能更强大. 比如notify()只能随即唤醒一个线程; 但是Condition
可以精确地唤醒其中某个线程
而劣势就是: unlock()容易漏, 所以要放在finally里面执行
ReentrantLock
和synchronized
其他区别:
synchronized
的锁对象可以是任意对象; 而ReentrantLock
的锁对象就是它本身synchronized
是一个关键字, 是 JVM 内部实现的.ReentrantLock
是标准库的一个类, 在 JVM 外实现的(基于 Java 实现).synchronized
使用时不需要手动释放锁.ReentrantLock
使用时需要手动释放. 使用起来更灵活, 但是也容易遗漏 unlock.synchronized
在申请锁失败时, 会死等.ReentrantLock
可以通过 trylock 的方式等待一段时间就放弃.synchronized
是非公平锁,ReentrantLock
默认是非公平锁. 可以通过构造方法传入一个 true 开启公平锁模式.- 更强大的唤醒机制.
synchronized
是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程.ReentrantLock
搭配Condition
类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.
3. 原子类
标准库中提供了java.util.concurrent.atomic
包, 里面的类都是基于CAS来实现的原子类.
原子类的应用场景有哪些呢?
-
计数需求
例如, 播放量,点赞量,投币量,转发量,收藏量
一个视频, 有很多人同时播放,点赞,收藏…
-
统计效果
例如, 统计应用程序出错的数目, 可以使用原子类记录, 通过监控服务器, 获得线上服务器错误数量, 并以曲线图的方式会知道页面上
4. 线程池
上文已经介绍过了, 这里就不再赘述了. 在这
5. 信号量 Semaphore
在操作系统中经常出现, 是并发编程中的一个重要的概念/组件.
准确来讲, semaphore是一个计数器(变量), 描述了"可用资源的个数". 这个"可用资源", 也叫"临界资源", 是指多个线程/进程等并发执行的实体可以公共使用到的资源(多个线程修改同一个变量, 这个变量就可以认为是临界资源)
理解信号量
可以把信号量想象成是停车场的展示牌: 当前有车位 100 个. 表示有 100 个可用资源.
当有车开进去的时候, 就相当于申请一个可用资源, 可用车位就 -1 (这个称为信号量的 P 操作)
当有车开出来的时候, 就相当于释放一个可用资源, 可用车位就 +1 (这个称为信号量的 V 操作)
如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源.
Semaphore 的 PV 操作中的加减计数器操作都是原子的, 可以在多线程环境下直接使用.
代码示例
-
创建 Semaphore 示例, 初始化为 4, 表示有 4 个可用资源.
-
acquire 方法表示申请资源(P操作), release 方法表示释放资源(V操作)
-
创建 20 个线程, 每个线程都尝试申请资源, sleep 1秒之后, 释放资源. 观察程序的执行效果.
public static void main(String[] args) throws InterruptedException {
//计数器初始值为4
Semaphore semaphore = new Semaphore(4);
semaphore.acquire();//计数器-1
System.out.println("执行P操作");
semaphore.acquire();//计数器-1
System.out.println("执行P操作");
semaphore.acquire();//计数器-1
System.out.println("执行P操作");
semaphore.acquire();//计数器-1
System.out.println("执行P操作");
semaphore.acquire();//计数器的值为0, 阻塞等待
System.out.println("执行P操作");
}
6. CountDownLatch
这是针对特定场景的一个组件, 同时等待 N 个任务执行结束.
比如下载一个较大的文件, 会比较慢. 但是有一些"多线程下载器", 把一个文件拆分成多个小部分, 使用多个线程分别下载一部分, 每个线程分别是网络连接, 会大幅度提高下载速度.
假设分成十个线程, 10个部分来下载, 得等到10各部分都下载完成, 整体才算下载完成
那么如何判定整体已经下载完了呢? 这就需要用到CountDownLatch了
代码案例
public static void main(String[] args) throws InterruptedException {
//构造方法中, 指定创建几个任务
CountDownLatch countDownLatch = new CountDownLatch(10);
//创建十个线程完成任务
for (int i = 0; i < 10; i++) {
int l = i;
Thread t = new Thread(() -> {
//涉及变量捕获, java的变量捕获要求被捕获的变量时final修饰的, 或者事实上是final的变量(值不变)
//所以这里不能使用i, 因为i会改变
///解决这个问题, 可以新创建一个变量, 将i的值赋给它
//这个新变量没有人改, 所以它就是事实上是final的变量
System.out.println("线程" + l + "正在工作");
try {
//代指某些耗时任务
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(l + "结束");
//任务结束后, 调用一下方法
countDownLatch.countDown();
});
t.start();
}
//调用await方法, 等待所有任务全部结束, 在未结束之前, 主线程会在这里阻塞等待
countDownLatch.await();
System.out.println("end");
}