目录
1. Spring、Spring Boot、Spring Cloud区别
2. Java并发之容器
2.1 CopyOnWriteArrayList和CopyOnWriteArraySet
2.2 BlockingQueue和BlockDeque
2.3 ConcurrentlinkedQueue
2.4 ConcurrentHashMap
2.5 ConcurrentSkipListMap和ConcurrentSkipListSet
3. Java并发之线程
3.1 线程的状态和生命周期
3.2 线程池
4. Java并发之锁
4.1 悲观锁和乐观锁
4.2 自旋锁
4.3 轻量级锁、偏向锁、重量级锁
4.4 公平锁和非公平锁
4.5 可重入锁
4.6 共享锁和排它锁
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的功能,无需其他配置。
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和BlockDequeJava中两种阻塞式队列,单端队列的实现有若干种方式:
- ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
- linkedBlockingQueue:由链表结构组成的有界阻塞队列。
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
- SynchronousQueue:不存储元素的阻塞队列。
- DelayQueue:使用优先级队列实现的无界阻塞队列。
这是一种非阻塞队列,透过源码可以看出,使用了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和ConcurrentSkipListSetConcurrentHashMap的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.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.5 可重入锁可重入锁,也叫做递归锁,指一个线程可以多次抢占同一个锁。例如,线程 A 在进入外层函数抢占了一个 Lock 显式锁之后,当线程 A 继续进入内层函数时,如果遇到有抢占同一个 Lock显式锁的代码,线程 A 依然可以抢到该 Lock 显式锁,不会因为之前已经获取过还没释放而阻塞,一定程度上可以避免死锁。
不可重入锁与可重入锁相反,指的一个线程只能抢占一次同一个锁。例如,线程 A 在进入外层函数抢占了一个 Lock显式锁之后,当线程 A 继续进入内层函数时,如果遇到有抢占同一个 Lock显式锁的代码,线程 A 不可以抢到该 Lock 显式锁。除非,线程 A 提前释放了该 Lock 显式锁,才能第二次抢占该锁。
Synchronized 和JUC 的 ReentrantLock 类是可重入锁的一个标准实现类。
4.6 共享锁和排它锁这是业务场景比较常见的概念,共享锁可以同时被多个线程持有,而排他锁同时只能被一个线程所有,通常的应用场景读写锁,读取数据是我们可以多个线程同时进行,这是一种共享锁,但是,写数据时不同线程执行顺序不同结果是不同的,所以多个线程不能同时进行,因此读锁是排他的。
ReentrantReadWriteLock中有关于读写锁的实现,分别是ReadLock和WriteLock。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)