目录
1. Spring、Spring Boot、Spring Cloud区别
2.1 CopyOnWriteArrayList和CopyOnWriteArraySet
2.5 ConcurrentSkipListMap和ConcurrentSkipListSet
1. Spring、Spring Boot、Spring Cloud区别
- Spring是核心,提供了基础功能;全称应是 Spring Framework 。它提供了多个模块,Spring IoC、Spring AOP、Spring MVC 等等。
- Spring Boot 是为简化Spring配置的快速开发整合包;
- Spring Cloud是构建在Spring Boot之上的服务治理框架。提供了一整套的解决方案——服务注册与发现,服务消费,服务保护与熔断,网关,分布式调用追踪,分布式配置管理等。
springboot主要优点如下:
- 简化Maven配置:Spring 提供了一系列的 starter pom 来简化 Maven 的依赖加载。不用考虑版本兼容性了。
- 简化部署:Spring Boot可以内嵌Servlet容器,这样我们无需以war包的形式部署项目。直接通过java -jar xx.jar启动。
- 简化监控:Spring Boot提供一系列端点可以监控服务及应用,做健康检测。
- 自动配置:Spring Boot能根据当前类路径下的类、jar包来自动配置bean,如添加一个spring-boot-starter-web启动器就能拥有web的功能,无需其他配置。
2. Java并发之容器
Java原始的大部分容器是线程不安全的,而线程安全的容器由于加入了synchronized导致并发能力低下,因此,Java1.5之后推出了并发容器,以得到更高的性能。
2.1 CopyOnWriteArrayList和CopyOnWriteArraySet
2.1.1 CopyOnWriteArrayList
CopyOnWriteArrayList这个容器的名字特征比较明显,顾名思义,在执行写操作的时候会将共享变量重新复制一份出来,这样的好处是读操作是完全无锁的。源码如下:
public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } } public E get(int index) { return get(getArray(), index); } private E get(Object[] a, int index) { return (E) a[index]; } |
首先,对add操作加锁,阻止其他线程继续写入,然后获取数组,再将数组复制一份,但是注意复制的数组的长度比原始大1,这是为了让写操作在复制的数组上进行,在执行写操作的同时可以进行读操作。通过源码可以看出读操作是完全无锁的,当复制的数组写入数据完毕,再将原始数组直接替换成修改后的数组,最后再解除锁。
应用场景:仅适用于写操作非常少的场景,而且能够容忍读写的短暂不一致,读写并行造成不一致是很正常的。
2.2.2 CopyOnWriteArraySet
CopyOnWriteArraySet和CopyOnWriteArrayList是很相似的,实际上是完全依赖CopyOnWriteArrayList实现的,CopyOnWriteArraySet所有的操作都是通过CopyOnWriteArrayList完成的。
CopyOnWriteArraySet插入一个新元素的时候,首先会检查元素是否存在,如果已经存在则直接返回;若不存在,则进行写入。写入操作时同样会先锁住,然后校验锁住的时候数组有没有产生变化,如果产生变化需要校验待插入元素和当前数组是否有相同的元素,一旦有则不能插入直接退出,如果不存在则可以插入,剩下过程和CopyOnWriteArrayList相同。
2.2 BlockingQueue和BlockDeque
Java中两种阻塞式队列,单端队列的实现有若干种方式:
- ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue:由链表结构组成的有界阻塞队列。
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
- SynchronousQueue:不存储元素的阻塞队列。
- DelayQueue:使用优先级队列实现的无界阻塞队列。
2.3 ConcurrentLinkedQueue
这是一种非阻塞队列,透过源码可以看出,使用了CAS操作无锁进行,ConcurrentLinkedQueue具有以下特点:
- 不允许null入列;
- 在入队的最后一个元素的next为null;
- 队列中所有未删除的节点的item都不能为null且都能从head节点遍历到;
- 删除节点是将item设置为null, 队列迭代时跳过item为null节点;
- head节点跟tail不一定指向头节点或尾节点,可能存在滞后性。
由于是采用链表结构,所以也可以无限增长,使用时注意OOM问题。
2.4 ConcurrentHashMap
- put方法的流程:
(1)根据key 计算出 hashcode 。
(2)判断是否需要进行初始化。
(3)即为当前 key 定位出的Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
(4)如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
(5)如果都不行,则利用synchronized 锁写入数据。
(6)如果数量大于TREEIFY_THRESHOLD 则要转换为红黑树。
- get方法的流程:
(1)根据 hash值计算位置。
(2)查找到指定位置,如果头节点就是要找的,直接返回它的 value.
(3)如果头节点 hash 值小于 0 ,说明正在扩容或者是红黑树,查找之。
(4)如果是链表,遍历查找之。
可以看出对于插入一个新的key,使用CAS操作即可,而对于已经存在的节点,使用synchronized进行覆盖或添加操作。值得一提的是,ConcurrentHashMap1.7是使用分段锁的方式,以上是JDK1.8中的实现,使用的是CAS和synchronized混合,另外采用的数据结构Node里也变成了链表+红黑树。
2.5 ConcurrentSkipListMap和ConcurrentSkipListSet
ConcurrentHashMap的key是无序的,而ConcurrentSkipListMap的key是有序的。ConcurrentSkipListMap里面的SkipList(跳表)本身是一种数据结构,跳表执行插入、删除和查询操作的平均复杂度为O(log n)。适用于需要排序的场景。
ConcurrentSkipListSet的所有方法均通过ConcurrentSkipListMap完成,两者底层是一样的。
3. Java并发之线程
3.1 线程的状态和生命周期
- 新建状态:使用new关键字创建一个线程后,该线程就处于创建状态了,这个过程中调用start方法就进入下一个状态。
- 就绪状态:调用start方法后线程就处于就绪状态,就绪状态的线程需要加入就绪队列中等待JVM的调度。
- 运行状态:当线程获取到CPU资源开始执行run方法后就进入运行状态。
- 死亡状态:线程执行完或者被条件终止时,线程就进入死亡状态。
- 阻塞状态:如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
- 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
- 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
- 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当 sleep() 状态超时, join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
线程的状态转换图如下:
3.2 线程池
3.2.1 线程池的好处
对大量使用线程的系统,一般不会每次在使用到线程时才创建,使用完成后又销毁,这样效率和性能都很差。这种场景一般会使用线程池,使用线程池的好处有:
- 提高了响应速度,在有线程需求的时候,直接从线程池中获取线程立即执行。
- 降低资源消耗,虽然线程创建在线程池中会占用一定的内存,但是它避免了频繁的创建、销毁工作。
- 便于管理线程,手动创建和销毁线程,有不确定性,线程管理不善就会导致线程大量创建却没有释放资源,最后导致资源耗尽。
3.2.2 使用方法
3.2.2.1 调用Executors
Java中创建线程池很简单,只需要调用Executors中相应的便捷方法即可,Executors中有四种线程池的创建方式:
- newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
- newFixedThreadPool :创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
- newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
- newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行。
3.2.2.2 调用ThreadPoolExcutor
这是目前主流的推荐线程池使用方式,是所有线程池实现的最基本方法,上述所有的线程池的底层实现都是用这种方式,但是其参数较多,需要进行手动设置,参数如下:
- corePoolSize:0-Integer.MAX_VALUE,保持活动状态的最小工作线程数(不允许超时的情况下)
- maximumPoolSize:1-Integer.MAX_VALUE,最大线程池数,其值被CAPACITY限制,CAPACITY默认值是2^29-1,大概是500000000(50亿)。
- keepAliveTime:0-Integer.MAX_VALUE,等待工作的空闲线程的超时时间(以纳秒为单位)。当线程池中线程数量大于corePoolSize(核心线程数量)或设置了allowCoreThreadTimeOut(是否允许空闲核心线程超时)时,线程会根据keepAliveTime的值进行活性检查,一旦超时便销毁线程。
- unit:时间单位
- workQueue:用于保存任务和移交给工作线程的队列,使用的是BlockingQueue。
- handler:拒绝策略,没有时执行默认的拒绝策略defaultHandler,假设线程池无法执行该任务则可抛出异常或者做相应的处理。
- threadFactory:当执线程池创建一个新的线程时使用的工场,默认使用Executors.defaultThreadFactory()。
4. Java并发之锁
4.1 悲观锁和乐观锁
悲观锁和乐观锁的实现: Java 中的 Synchronized 和 ReentrantLock 等都是悲观锁,java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
4.2 自旋锁
自旋锁是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。使用自旋锁是为了规避cpu每次切换线程引起的保护现场和恢复现场的开销。
自旋锁显然是要消耗CPU资源的,虽然,自旋锁避免了频繁的线程状态的转换,但是对于长时间得不到释放的资源,自旋锁带来的开销代价就显得大了,所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。
自旋锁的实现:简单自旋锁、TicketLock、CLHLock、MCSLock
4.3 轻量级锁、偏向锁、重量级锁
这些都是和Synchronized 相关的概念,这几种锁的级别都是对象的状态位,在对象的头部中有对应的标志位,对象头的结构如下:
锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
- 无锁:对象头里面存储当前对象的hashcode,即原来的Markword组成是:001(锁标志位+偏向锁标志位)+hashcode+分代年龄。
- 偏向锁:其实就是偏向一个用户,适用场景,只有几个线程,其中某个线程会经常访问,他就会往对象头里面添加线程id,就像在门上贴个纸条一样,占用当前线程,只要纸条存在,就可以一直用
- 轻量级锁:比如你贴个纸条,一直使用,但是其他人不乐意了,要和你抢,只要发生抢占,synchronized就会升级变成轻量级锁,也就是不同的线程通过CAS方式抢占当前对象的指针,如果抢占成功,则把刚才的线程id改成自己栈中锁记录的指针LR(LockRecord),因为是通过CAS的方式,所以也叫自旋锁。
- 重量级锁:线程非常多,比如有的线程超过10次自旋,或者-XX:PreBlockSpin设置,或者自旋次数超过CPU核数的一半,就会升级成重量级锁,Java1.6之后加入了自适应自旋锁,JVM自己控制自旋次数。
4.4 公平锁和非公平锁
公平锁和非公平锁可以类比打饭,大家有序排队打饭就是公平锁,不排队同时往上挤就是非公平锁,公平锁会根据排队队列的先后顺序来获取锁,但是会导致阻塞,其他人都只能等你打完饭才能继续,非公平锁是竞争获取锁,靠近窗口的谁最接近窗口谁就开始打饭,而远离窗口的就只能等待前面人减少,也就处于被阻塞状态,这样可能会导致有些人一直打不到饭,也就是饿死。
4.5 可重入锁
可重入锁,也叫做递归锁,指一个线程可以多次抢占同一个锁。例如,线程 A 在进入外层函数抢占了一个 Lock 显式锁之后,当线程 A 继续进入内层函数时,如果遇到有抢占同一个 Lock显式锁的代码,线程 A 依然可以抢到该 Lock 显式锁,不会因为之前已经获取过还没释放而阻塞,一定程度上可以避免死锁。
不可重入锁与可重入锁相反,指的一个线程只能抢占一次同一个锁。例如,线程 A 在进入外层函数抢占了一个 Lock显式锁之后,当线程 A 继续进入内层函数时,如果遇到有抢占同一个 Lock显式锁的代码,线程 A 不可以抢到该 Lock 显式锁。除非,线程 A 提前释放了该 Lock 显式锁,才能第二次抢占该锁。
Synchronized 和JUC 的 ReentrantLock 类是可重入锁的一个标准实现类。
4.6 共享锁和排它锁
这是业务场景比较常见的概念,共享锁可以同时被多个线程持有,而排他锁同时只能被一个线程所有,通常的应用场景读写锁,读取数据是我们可以多个线程同时进行,这是一种共享锁,但是,写数据时不同线程执行顺序不同结果是不同的,所以多个线程不能同时进行,因此读锁是排他的。
ReentrantReadWriteLock中有关于读写锁的实现,分别是ReadLock和WriteLock。