使用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
可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全.
ReentrantLock
也是可重入锁. “Reentrant” 这个单词的原意就是 “可重入”
ReentrantLock
有着synchronized
没有的独特优势:
提供了trylock方法进行加锁.
public boolean tryLock()
: 加锁失败直接返回false
public boolean tryLock(long timeout, TimeUnit unit)
有两种模式, 可以工作在公平锁状态下, 也可以工作在非公平锁的状态下, 通过构造方法中参数的设定来决定(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
类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.标准库中提供了java.util.concurrent.atomic
包, 里面的类都是基于CAS来实现的原子类.
原子类的应用场景有哪些呢?
计数需求
例如, 播放量,点赞量,投币量,转发量,收藏量
一个视频, 有很多人同时播放,点赞,收藏…
统计效果
例如, 统计应用程序出错的数目, 可以使用原子类记录, 通过监控服务器, 获得线上服务器错误数量, 并以曲线图的方式会知道页面上
上文已经介绍过了, 这里就不再赘述了. 在这
在操作系统中经常出现, 是并发编程中的一个重要的概念/组件.
准确来讲, 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操作");
}
这是针对特定场景的一个组件, 同时等待 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");
}