目录
1.Callable接口
相关面试题
2.ReentrantLock
相关面试题
3.信号量Semaphore
4.CountDownLatch
5.多线程环境使用ArrayList
热加载
1.Callable接口
Callable是一个接口,把线程封装了一个"返回值",方便程序员借助多线程的方式计算结果.
类似于Runnable,是用来描述一个任务,区别就是Runnable描述的任务没有返回值,而Callable描述的任务有返回值
看这个例子
创建线程计算 1 + 2 + 3 + ... + 1000, 不使用 Callable 版本
public class Test {
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);
}
}
}
使用Runnable来描述任务,没有返回值,这里使用了一个辅助类result,还需要使用一些加锁和wait,notify操作,比较复杂易出错
我们使用Callable来解决这个问题
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;
}
};
创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行 FutureTask 内部的 Callable 的 call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结
果.
Integer result = futureTask.get();
System.out.println(result);
}
call方法就相当于Runnable的run方法,只不过call方法会返回一个泛型返回值,run返回void
创建好任务后,需要一个线程来执行,但是这里不能把callable直接传入Thread的构造方法中,需要给它套上一个辅助类
FutureTask实现了RunnableFuture<V>接口,RunnableFuture<V>接口继承了Runnable, Future<V>
public class FutureTask<V> implements RunnableFuture<V>{
...
}
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
FutureTask既是Runnable对象,也是Future对象。Future是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果、设置结果操作,get方法会阻塞,直到任务返回结果
get方法就是获取结果.get会发生阻塞,直到callable执行完毕,get才阻塞完成,获取到结果
Callable也是创建线程的一种方式
相关面试题
介绍下 Callable 是什么
Callable是一个接口,相当于把线程封装了一个"返回值",方便程序员借助多线程方式计算结果
Callable和Runnable相对,都描述一个任务,Callable描述的是带有返回值的任务,Runnable描述的是不带返回值的任务
Callable通常搭配FutureTask使用,FutureTask用来保存Callable的返回结果,因为Callable往往是在另一个线程中执行的,不知道具体执行完的时间,FutureTask就是负责等待结果出来并保存的
JUC的常见类:
2.ReentrantLock
ReentrantLock是标准库提供的另一种锁,是可重入锁
synchronized是基于代码块的方式加锁解锁的
ReentrantLock比较传统,使用了lock和unlock方式加锁解锁
ReentrantLock 的用法:
lock(): 加锁, 如果获取不到锁就死等.
trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
unlock(): 解锁
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();
reentrantLock.unlock();
}
在lock和unlock之间就是加锁的部分,但是这样写有可能unlock执行不到,加锁的代码块中间如果存在return或者异常都可能导致unlock不能顺利进行
或者在return之前加上unlock,但是如果这种条件语句很多,那么就会很麻烦,此时用finally来解决
上述是ReentrantLock的劣势,但是它也是有优势的
1.ReentrantLock提供了公平锁版本的实现
2.synchronized提供的加锁操作是"死等",只要获取不到锁就一直等待,ReentrantLock提供了更灵活的等待方式:tryLock
无参数版本:能加锁就加,加不上锁就放弃
有参数版本:指定了超时时间,加不上锁就等待,如果超时了还没获取到锁就放弃
3.ReentrantLock提供了一个更强大,更方便的等待通知机制.
synchronized搭配的是waitnotify,notify的时候是随机唤醒一个wait的线程
ReentrantLock搭配一个Condition类,进行唤醒的时候可以唤醒指定的线程
虽然ReentrantLock有一定的优势,但是实际开发常用的是synchronized
相关面试题
为什么有了 synchronized 还需要 juc 下的 lock?
synchronized使用时不需要手动释放锁,ReentrantLock 使用时需要手动释放. 使用起来更 灵活
synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启公平锁模式
synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的 线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程
3.信号量Semaphore
操作系统中的信号量和此处的信号量是相同的,只不过此处的信号量是Java把操作系统原生的信号量封装了一下
信号量本质是一个"计数器",用于描述可用资源的个数
就相当于停车场的入口的显示牌,有多少个车位可用!如果车位满了,那么牌子就是显示可用为0.那么再想要停车,就只能换地方,或者等待.
围绕这个计数器有两个操作:
P操作:申请一个可用资源,计数器就-1
V操作:释放一个可用资源,计数器就+1
P操作如果要是计数器为0了,继续P操作,就会阻塞等待
P操作使用acquire申请
V操作使用release释放
考虑一个计数初始值为1的信号量,针对这个值,只能有1和0两个取值,不会是负值.
如果执行P操作,1->0
如果执行V操作,0->1
如果进行一次P操作了,再进行一次P操作,就会阻塞等待
那么根据这个特性我们很容易联想到锁(锁可以视为计数器为1的信号量,有两个取值,称为二元信号量)
锁是信号量的一种特殊情况,信号量是锁的一般表达
实际开发中信号量也会用到,比如说,图书馆中有某本书有20本,那么就可以用初始值为20的信号量.借书就P操作,还书就V操作.如果书为0了,再P操作就会阻塞等待!
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(3);
semaphore.acquire();
System.out.println("第一次P操作");
semaphore.acquire();
System.out.println("第二次P操作");
semaphore.acquire();
System.out.println("第三次P操作");
semaphore.acquire();
System.out.println("第四次P操作");
}
申请了3个可用资源,当申请第四个就会阻塞等待,等到别的线程调用release释放可用资源,才能停止阻塞
无参版本是申请一个资源,带参数版本可以指定申请的资源数
代码中也可以使用Semaphore来实现类似于锁的功能,保证线程安全
4.CountDownLatch
同时等待 N 个任务执行结束
类似于跑步比赛,多个选手都就位,哨声响才同时出发,所有选手都通过终点,才公布成绩
跑步比赛开始时间都是相同的,但是结束时间是不明确的,为了衡量这个时间引入了CountDownLatch,主要提供了两个方法
1.await 方法,主线程来调用该方法,很多"a"为前缀的术语都表示异步.
同步和异步是相对的,同步:发送请求方自己主动等待响应结果
异步:发送请求方请求完就不管了,等有结果了,对方主动将结果推送出来
2.countDown表示冲过了终点线.
CountDown在构造的时候,指定一个计数器(选手的个数).例如,指定了四个选手进行比赛,初始情况下,调用await,就会阻塞,每个选手冲过终点后都会调用countDown方法,前三次调用countDown,await没有任何影响,第四次调用countDown,await就会被唤醒,返回,解除阻塞,此时认为整个比赛结束!
在开发中CountDownLatch也有很多应用场景,比如下载一个大的文件.使用多线程下载,把一个大的文件切分成多个小块儿的文件,安排多个线程分别下载,多线程下载,不是充分利用了多核CPU,而是充分利用了带宽(下载是IO操作,与CPU关系不大),此处就可以用CountDownLatch来区分是不是整体都下载完了
5.多线程环境使用ArrayList
Java标准库中的大部分集合都是"线程不安全"的,多个线程使用同一个集合类对象,很可能会出现问题
Vector, Stack, HashTable, 这几个集合是线程安全的,关键方法带有synchronized
多线程环境使用ArrayList:
1.自己加锁,使用synchronized或者ReentrantLock
2.Collections.synchronizedList,可以使用这个方法把集合类套一层,这里会提供一些Array List相关的方法,同时是带锁的
3.CopyOnWriteArrayList
简称为"COW",即"写时拷贝",如果针对这个ArrayList进行读操作,不做任何额外的工作,如果进行写操作,则拷贝一份新的ArrayList,针对新的进行修改,修改过程中如果有读操作,就继续读旧的这份数据,当修改完毕了,使用新的替换旧的(本质上是一个引用之间的赋值,是原子的)
这种方案的优势很明显,就是不用加锁了,缺点也很明显,就是这个Array List不能太大了,只适用于这种数组比较小的情况下
热加载
我们知道服务器程序的配置与维护是需要修改配置文件的,修改配置后就需要重启才能生效,但是重启操作成本比较高,重启过程中服务就中断了,用户发送的请求就没有响应了,产生的后果是很不好的,因此,很多服务器提供了"热加载"(reload)这样的功能
"热加载"功能可以不重启服务器,实现服务器程序的配置的更新,热加载的实现就是用写时拷贝的思想,新的配置放到新的对象中,加载过程里,请求仍然基于旧的配置进行工作,当新的对象加载完毕,使用新的对象代替旧的对象(替换完成之后,旧的对象就可以释放了)