之前在Java基础中,我们学习过了map和它的子类hashmap,hashtable。所以在学习concurrentMap之前我们需要复习一下,hashmap的原理。
底层是使用 数组+链表 ,数组查询快,链表增删快。所以hashmap查询快增删也快。默认初始容量是16,默认加载因子0.75
hashmap中我们知道,它是线程不安全的,通俗的说,就是一个线程进来是没有锁来锁这个map的,这就会导致线程不安全,而今天要学习的concurrentMap,就解决了高并发情况下的线程安全问题,相比于hashTable它的效率更高。
concurrent拥有两个子类:ConcurrentHashMap ,ConcurrentNavigableMap。
在学习之前,需要明白这个东西在什么地方使用,它的用法合hashmap一样,你只需要在使用hashmap的时候思考一下,我是不是还有个叫ConcurrentHashMap的东西可以使用。
底层原理:
它的底层合hashmap一样是 数组+链表。数组的默认容量是16个桶,默认加载因子0.75,扩容是增加一倍的桶数。
通过源码可以更好的看出,初始值右移1个单位(/2)然后+初始容量+1。把计算出来的值再 放入下面那个函数,就是找2的N次方。
案例:初始化值是48,48>>1=24,24+48+1=73,63<73<128,128=2的7次方,实际容量就是128.
最大容量:
桶也是有极限的,2的30次方
变化为红黑树:
当桶中的元素个数超过8个,且桶的个数多余64,就会把元素数量多余超过8个的链表扭成红黑树。
ConcurrentHashMap是一个异步线程安全的映射:
异步——可以让多个线程在一个时间段内一起访问
采用了分段/桶锁机制,并且在分段锁基础上引入了读写锁机制。
读锁:允许多个线程同时读,不允许线程写
写锁:只允许一个线程写,不允许线程读
实现方法:
假如是hashmap,比如线程a先去访问链表做查询操作,在查询出数据的前0.00001秒线程b抢到了CPU,进去把数据修改了,这个时候线程a读取出来的数据就不对了。再假如是hashtable,线程a先进去访问,这个时候它就会把整个桶都锁住了,要访问其他链表的线程也没有办法访问,这就造成了效率低。
这个时候,主角出现了就是我们的concurrentHashMap,它是怎么做的?它不锁整个桶,它锁的是桶里的列表,比如你线程a访问第一个链表做操作,它是不会影响线程c对第三个链表做操作的。而且多个线程访问同一个链表,还做了读锁和写锁;读锁,当有多个线程去读取同一个链表,这时就不允许有线程去修改该链表。写锁,当有一个线程去对链表做写操作,就不允许其他线程去读这个列表。
使用代码:
声明
ConcurrentHashMap<String, String> map =new ConcurrentHashMap<>(48);
至于它的方法,和hashmap基本一样,就不做解释了。
ConcurrentNavigableMap 是一个接口,我们要学习使用的是它的实现类 ConcurrentSkipListMap - 并发跳跃表映射。
底层:
ConcurrentSkipListMap 的底层就是跳跃链表。
跳跃链表: 链表一开始是存入0-4号元素,使用跳跃链表,链表中存入1和3两个元素。比如要寻找2号元素,先去跳远链表中看,2大于1,1小于3,那就去1的右边3的左边找。
跳跃列表的注意: 针对有序列表进行操作 ,适用于查询多而增删少的场景,最上层跳跃表的元素个数不能少于2个,如果在跳跃表中新添了元素,那么新添的元素是否要提取到上层的跳跃表中遵循"抛硬币"原则
截取map:
ConcurrentNavigableMap是可以像string list那样做skip的,这就是为什么它的内容是要有序的。
使用代码:
// 提供了用于截取子映射的方法
ConcurrentNavigableMap<String, Integer> map =
new ConcurrentSkipListMap<>();
map.put("d", 5);
map.put("e", 3);
map.put("a", 4);
map.put("y", 6);
map.put("u", 5);
map.put("o", 9);
map.put("m", 0);
System.out.println(map);
// 从头开始截取到指定的位置
System.out.println(map.headMap("e"));
// 从指定位置开始截取到末尾
System.out.println(map.tailMap("e"));
// 截取指定范围内的元素
System.out.println(map.subMap("e", "u"));
引入:
虽然名字很有迷惑性,但是它的本质就是一个线程池。要使用它我们就需要知道线程池里有什么内容,需要定义一些什么,是怎么工作的。
工作原理:
线程池刚创建的时候是空的
core thread(核心线程):每过来一个请求,就会在线程池中创建一个核心线程来处理这个请求。核心线程的数量在定义线程池的时候需要指定。核心线程用完之后不会被销毁而是等待下一个请求 。只要核心线程没有达到指定的数量,那么每一个请求都会触发创建一个新的核心线程处理 。
work queue(工作队列):如果核心线程被全部占用,那么新来的请求会放到工作队列中进行排队等待。工作队列本质上是一个阻塞式队列(blocking queue)
temporary thread(临时线程):如果工作队列被全部占用,那么新来的请求会交给临时线程来处理。临时线程的数量在定义线程池的时候需要指定 临时线程用完之后会存活一段时间,如果在这段时间内没有接收到新的任务那么就会被销毁。工作队列中的任务不会被临时线程执行:尽量缩短临时线程的存活时间,尽量提高核心线程的利用率 。
rejectedExecutionHandler(拒绝执行处理器):如果临时线程被全部占用,那么新来的请求会交给拒绝执行处理器来处理
代码:
声明线程池:
// corePoolSize - 核心线程数量
// maximumPoolSize - 总线程数量 = 核心线程数 + 临时线程数
// keepAliveTime - 临时线程的存活时间
// unit - 时间单位
// workQueue - 工作队列
// handler - 拒绝执行处理器,可以不指定
ExecutorService es = new ThreadPoolExecutor(
5, // 5个核心线程
12, // 7个临时线程
5, TimeUnit.SECONDS, // 临时线程用完之后能够存活5s
new ArrayBlockingQueue<Runnable>(5),
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 如果有拒绝流程,那么需要将拒绝流程写到这个方法中
// 例如:日志记录(请求、时间、IP等),跳转页面(努力加载中,失败了 - 秒杀等)等
System.out.println(r + "被拒绝了~~~");
}
}
);
声明线程类:
class ESThread implements Runnable {
@Override
public void run() {
try {
System.out.println("start");
Thread.sleep(3000);
System.out.println("finish");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
调用线程池中的线程:
// new Thread(new ESThread()).start();
// 核心 - 5 临时 - 7 队列 - 5
// 12个start,3个拒绝 -> 3s之后再有5个start
for (int i = 0; i < 20; i++) {
es.execute(new ESThread());
}
// 如果线程池用完,可以关闭线程池
// 实际开发中,线程池开启之后一般是不关的
es.shutdown();
executorService的两个代理线程池——newCachedThreadPool和newFixedThreadPool:
1.newCachedThreadPool:
特点:
- 没有核心线程全部都是临时线程。
- 临时线程的数量是Integer.MAX_VALUE,即2^31-1考虑到单台服务器所能承载的线程数量远远小于21亿,所以一般认为这个线程池能够处理无限多的请求。
- 临时线程用完之后最多存活60s。
- 工作队列是一个同步队列,实际生产过程中, 一般在测试阶段就会利用空请求将这个工作队列填充,此时可以认为这个线程池没有工作队列。
// 大池子小队列
// 适用于高并发的短任务的场景,例如即时通讯
// 不适用于长任务场景
ExecutorService es =Executors.newCachedThreadPool();
2.newFixedThreadPool:
特点:
- 没有临时线程全部都是核心线程
- 工作队列是LinkedBlockingQueue,默认是Integer.MAX_VALUE, 一般认为能够存储无限多的请求
给入参数是核心线程个数
// 小池子大队列
// 适用于并发低的长任务场景,例如网盘下载
// 不适用于高并发的短任务场景
ExecutorService es =Executors.newFixedThreadPool(5);
ScheduledExecutorService(定时调度执行器服务器):
使用这个线程池可以推迟每个线程执行的时间间隔。
线程类:
class ScheduleThread implements Runnable {
@Override
public void run() {
try {
System.out.println("hello~~~");
Thread.sleep(8000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
实现定时执行:
//核心线程5个
ScheduledExecutorService ses =Executors.newScheduledThreadPool(5);
// 从上一次启动,开始计算下一次的启动时间
// 每隔5s执行一次
// 间隔时间是取执行时间和指定时间的最大值
ses.scheduleAtFixedRate(new ScheduleThread(), 0,5, TimeUnit.SECONDS);
// 从上一次结束,开始计算下一次的启动时间
// 每隔5s执行一次
ses.scheduleWithFixedDelay(new ScheduleThread(), 0, 5, TimeUnit.SECONDS);
分叉合并池:
分叉——将一个大任务进行拆分,拆分成多个小任务分配给多个线程执行,这个过程称之为分叉
合并——将分叉出来的任务结果进行汇总,这个过程称之为合并
工作窃取——在分叉合并中,如果一个核将它身上所有的任务执行完成之后,并不空闲下来,而是会去随机扫描一个核,然后从这个核的任务队列尾端来偷取一个任务回来执行,这种方式称之为work-stealing(工作窃取)策略,因为工作窃取策略涉及到CPU核之间的线程调度问题,所以底层是用C语言实现的
案例:求1-100000000000L的和,使用分叉合并池,把计算分配到各个核上执行,然后再合并。
mian方法中的线程调用:
//声明分叉合并线程池
ForkJoinPool p = new ForkJoinPool();
//启动线程
ForkJoinTask<Long> f = p.submit(new Sum(1, 100000000000L));
//获取值
System.out.println(f.get());
p.shutdown();
线程类:
class Sum extends RecursiveTask<Long> {
private long start;
private long end;
public Sum(long start, long end) {
this.start = start;
this.end = end;
}
// 分叉合并的逻辑就是在这个方法中覆盖
@Override
protected Long compute() {
// 分叉:将大的拆成小的
// 拆的过程中得有限度
if (end - start <= 10000) {
// 如果范围内的数字个数不到10000个,此时认为数量已经比较少了
// 那么不能继续拆了,就要将这个范围内的数字来进行求和
long sum = 0;
for (long i = start; i <= end; i++) {
sum += i;
}
return sum;
} else {
// 说明范围内的数字依然比较多,那就继续分叉
long mid = (start + end) / 2;
Sum left = new Sum(start, mid);
Sum right = new Sum(mid + 1, end);
// 需要将拆分出来这两半各自定义成两个线程来执行
left.fork();
right.fork();
// 分叉完之后,最后需要将结果汇总
return left.join() + right.join();
}
}
使用Callable来实现线程类的创建:
之前学过使用继承thread类和实现runnable接口两种方法来实现线程的书写,但是这两个方法都没有办法在执行run方法的时候返回数据,使用callable就可以实现数据的返回。
线程类:
// 泛型表示的结果类型
class CDemo implements Callable<String> {
@Override
public String call() throws Exception {
return "Success~";
}
}
启动线程:
ExecutorService es =Executors.newCachedThreadPool();
// 将结果封装成了Future对象
Future<String> f = es.submit(new CDemo());
System.out.println(f.get());
es.shutdown();