高并发基础
Concurrent包
是Java中一套用于进行高并发编程的包 — JDK1.5
BlockingQueue - 阻塞式队列
队列:FIFO(先进先出)
往往需要进行界限的限定
ArrayBlockingQueue - 阻塞式顺序表 - 基于数组来实现,在使用的时候也需要指定容量,而且容量指定好之后不可改变。
LinkedBlockingQueue - 阻塞式链表 - 基于节点来进行说数据的存储。在使用的时候可以指定容量也可以不指定。如果指定了容量,则容量不可变。如果不指定容量,则默认容量为Integer.MAX_VALUE,即231-1,所以一般认为这种、情况下,队列是没有界限的。
PriorityBlockingQueue - 优先级阻塞式队列 - 这个队列会对放入的元素进行自然排序,也就意味着队列中的元素对应的类要实现Comparable。但是在迭代的时候不保证排序。
SynchronousQueue - 同步队列 - 容量默认为1,而且只能为1。
ConcurrentMap - 并发映射
HashMap - 异步式线程不安全的映射
Hashtable - 同步式线程安全的映射。对外提供的方法大部分都是同步方法。如果同步方法是非静态的,那么锁对象是this;如果同步方法时静态方法,那么锁对象是当前类的字节码。所以Hashtable使用的时候是将整个映射进行锁定
ConcurrentHashMap — 异步式线程安全的映射。在JDK1.5采取了分段锁/分桶锁(引入了读写锁)。从JDK1.8开始,采取了CAS(Compare and Swap)算法+红黑树结构。
红黑树
基于了二叉查找树来建立的。对于二叉查找树而言,所有的左子叶一定是小于根节点,所有的右子叶一定是大于根节点,而且每一个节点的值都不一样。时间复杂度是O(logn)。
在极端的情况下,二叉查找树可能会出现不平衡的现象,从而导致某个子树成为了一个链表结构导致查询以及增删效率降低,引入了红黑树来解决。
红黑树的特点
- 每一个节点不是红色就是黑色
- 根节点一定是黑色
- 红色节点的子节点一定是黑色
- 从根节点到任意一个最底下的子节点的路径中包含的黑色节点个数是相同的
- 所有的最底层的子节点都是黑色的空节点
- 插入的节点一定是红色节点。
红黑树的修正
- 如果父节点和叔父节点均为红色,那么将父节点以及叔父节点都涂黑,将祖父节点涂红
- 如果父节点是红色且叔父节点是黑色,并且当前节点是右子叶,以当前节点为基准进行左旋
- 如果父节点是红色且叔父节点是黑色,并且当前节点是左子叶,以父节点为基准进行右旋
扩展:跳跃表
针对一个有序列表,随机挑选一些节点来构成一个新的列表,在操作列表的时候,先在新的列表中锁定范围在根据范围来操作原列表。新的列表就是跳跃表。跳跃表在提取的时候,最上层的跳跃表至少有2个节点。以空间换时间的产物。适用于读比写多的场景。
logab = logb/loga
log(ab) = loga + logb
跳跃表的时间复杂度是O(log(n))。
如果跳跃表已经构建好,在原列表中又插入新的数据,那么这个新数据对应的节点是否往上提取将根据抛硬币原则来决定。
线程池
为了减少线程的创建和销毁而只是服务器资源的浪费,所以定义线程池来存储和处理线程。
/*
* 特点:
* 1. 没有核心线程
* 2. 全部都是临时线程
* 3. 能够处理任意多的线程
* 4. 工作队列是同步队列
* 小队列大任务
* 场景:
* 1. 适用于高并发的短任务场景,不适合于长任务场景
* 2. 如果处理长任务就会导致服务器的资源被大量占用
*/
ExecutorService es = Executors.newCachedThreadPool();
/*
* 特点:
* 1. 只有核心线程
* 2. 没有临时线程
* 3. 工作队列是阻塞式链表,没有指定容量,意味着存储任意多的线程
* 小任务大队列
* 场景:
* 1. 适用于长任务场景,不适用于短任务
* 2. 如果处理短任务会导致效率降低
*/
ExecutorService es = Executors.newFixedThreadPool(10);
// 既可以有核心线程,又可以创建大量的临时线程
ExecutorService es = Executors.newScheduledThreadPool(5);
Callable和Runnable的区别:
- 返回值:Callable有返回值,Runnable没有返回值
- 异常:Runnable没有容错机制,意味着如果出现异常必须立即处理;Callable有容错机制,意味着出现异常之后可以向上抛出
- 启动方式:Runnable可以通过Thread来启动,也可以通过线程池的execute、submit来处理;Callable线程只能通过线程池的submit来处理
Lock
CountDownLatch - 闭锁/线程递减锁。适用于所有线程都在执行完成之后进行最后的统一处理。
CyclicBarrier - 栅栏。所有线程到达同一个点之后再继续往下执行。
Exchanger - 交换机。用于交换两个线程之间的数据。
Semaphore - 信号量。线程会抢占信号,每抢占一个信号计数就会-1,当信号量为0的时候其余线程就被阻塞,直到有线程释放信号,其余的线程才能抢占
synchronized — 重入锁,非公平锁。格式非常固定,而且不能跨方法或者跨类使用。
原子性操作
在对象的底层进行加锁,保证对象在同一时刻只能被一个线程操作。
回顾:内存
java将内存大致分为5块:栈内存、堆内存、方法区、本地方法栈、PC计数器(寄存器)。
栈内存:计算。所有的方法以及代码块的执行都是在栈内存中。每一个线程独享一个栈。
堆内存:存储对象。
方法区:存储类信息,常量(字面量、自定义常量)。
本地方法栈:计算。所有的本地方法(native)的执行都在本地方法栈。
PC计数器:程序的计数和线程的调度。
NIO
BIO - Blocking IO - 同步式阻塞式IO
BIO的缺点
- 如果有大量的请求访问服务器,那么就要产生大量的线程去应对这些请求,导致服务器资源过于紧张
- 在完成一次任务的过程中需要创建大量的输入或者输出流
- 数据通过流传输,无法实现定点操作
- 阻塞模式
NIO - New IO - 同步式非阻塞式IO:Buffer,Channel, Selector — Buffer是容器,用于存储数据;Channel用于传输;Selector进行过滤。
Buffer - 缓冲区
用于进行数据的存储,存储的时候都是用的数组。操作的都是基本类型。
数据在操作的时候是根据操作位position的位置决定。
限制位limit是决定操作位所能达到的最大值
标记位mark用于进行位置的标记,在出现错误的时候可以回到标记位进行重新操作,可以不用重复操作前边的数据
容量位capacity用于固定容量
标记位 <= 操作位 <= 限制位 <= 容量位
翻转缓冲区:将限制位挪到操作位上,将操作位归零,清除标记位
重置缓冲区:将操作位挪到标记位
重绕缓冲区:将操作位归零,清除标记位
清空缓冲区:并不是将缓冲区真正清空,只是将操作位归零,将限制位挪到容量位,将标记位清除
Channel - 通道
传输数据,基于缓冲区进行传输。可以进行双向传输。
默认是阻塞的,可以设置为非阻塞。但是往往需要进行手动阻塞
TCP : SocketChannel , ServerSocketChannel
UDP : DatagramChannel
File : FileChannel
Selector - 多路复用通道选择器
可以选择出有用的事件或者是连接,进行事件的处理。
客户端的事件:可连接、可读、可写
服务器端的事件:可接受、可读、可写
tomcat5:使用的是传统的BIO
tomcat6:使用NIO
AIO - Asynchronous IO - 异步式非阻塞式IO - JDK1.8 - 在NIO的基础上延伸出来的,所以称之为NIO.2