Java基础 集合 锁 线程 spring
集合
hashMap
hash函数设计
int h = (key == null) ? 0 :(h = key.hashcode)^(h>>>16)
int index = h & (length-1);
- h>>>16 被称为扰动函数 使用无符号右移16位后再与原hashcode异或 原因是 .hashcode 方法计算的结果为32位,但是length位数比较低,直接计算index高位不参与容易冲突,右移16再异或可以混合低位和高位增加随机性。从打降低hash冲突的风险。
- 为什么是 &(length-1): Java中对2的n次方取余等于减一的与运算,效率更高。
扩容步骤
resize()方法中若数组未初始化则初始化数组,若是已经初始化的数组,则数组扩容为之前的2倍。扩容过程如下
- 遍历数组若旧数组中不存在hash冲突的节点,直接移动到新的数组当中去。int index = (e.hashcode&(newLength -1))
- 若存在冲突的节点,树节点进行拆分,链表节点会保持原有顺序,依然是尾插法。
- 判断元素是否在原有位置
e.hashcode&(oldcap)==0
这是因为oldCap的高位和newCap-1的高位是一致的。 - 发现元素不是在原有位置,更新hitail和hiHead的指引关系。
- 将index未改变的复制到新的数组当中去。
- 将index发生改变的元素复制到新数组当中去。新的下标为oldindex+oldCap
为什么扩容为2
因为在扩容中判断元素是否在原位置使用的是与操作。都为1才能为1。假设数组从8扩容到16,决定为扩容后是否在原位置的为hashCode的高位值。1/0会被分流到不同的位置当中去,从而在扩容中可以让数组分布更加均匀。
old.length -1 = 7 0 0 1 1 1
e.hashCode = k x p x x x
==============================
0 0 y y y
扩容前index值由低三位决定,与操作让高位以上都为0
e.hashcode&(oldCap-1)
new.length -1 = 15 0 1 1 1 1
e.hashCode = k x p x x x
==============================
0 z y y y
扩容后唯一发生变化的是高位z。若z为0那么位置不变。
若z的位置为1 那么新的index等于oldCap+oldIndex
新的index的值为zyyy z000等于oldCap 0yyy等于oldIndex
e.hashcode&(newCap-1)
old.length = 8 0 1 0 0 0
e.hashCode = k x p x x x
==============================
0 z 0 0 0
此时e.hashcode & oldCap == 0 那么则z为0 说明位置不变。
put/get方法
get方法
- hash & (length -1)确定元素位置,如果没碰撞直接放到bucket里;
- 如果碰撞了,以链表的形式存在buckets后;
- 如果碰撞导致链表过长(就把链表转换成红黑树(JDK1.8中的改动 大于等于8);
- 如果节点已经存在就替换old value(保证key的唯一性)
- 如果bucket满了(超过load factor*current capacity),就要resize。
put方法
- hash & (length -1)确定元素位置
- 判断第一个元素是否是我们要找的位置
- 判断节点是否为树,若是在树节点查找
- 判断节点是否为链表,若是在链表查找
- 找到对应的元素返回,无则返回空值
死循环
在JDK1.7当中由于头插法在并发情况下会出现成环的情况。假设数组index[1]=1->5->9
- 当前A线程正在准备扩容 e=1 e.next=5让出时间片 B线程完成扩容
- 扩容后的newtabIndex[1]=9->5->1 由于头插法 顺序被置换
- 此时A线程继续执行 newtabIndex[1]会挂载到e=1上
- 然后执行 newtabIndex[1] = e.next 给出一个 e=1 指向e.next =5的引用导致成环
- 在当前位置getKey时候就会引起死循环问题。
ConcurrentHashMap
在JDK1.7版本中,ConcurrentHashMap维护了一个Segment数组,Segment这个类继承了重入锁ReentrantLock,并且该类里面维护了一个 HashEntry<K,V>[] table数组,在写操作put,remove,扩容的时候,会对Segment加锁,所以仅仅影响这个Segment,不同的Segment还是可以并发的,所以解决了线程的安全问题,同时又采用了分段锁也提升了并发的效率。在JDK1.8版本中,ConcurrentHashMap摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap。
集合数据结构
List
- ArrayList :内部是通过数组实现的,它允许对元素进行快速随机访问,通过getIndex得到,中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,插入删除效率相对较低。
- Vector: 线程安全的数组
- Linklist: 双向循环链表,线层不安全查询慢,插入删除快,只需要移动指针不需要移动数据。
Map
- hashMap
- hashTable: 线程安全的map
- linkedHashMap: 在hashMap的基础上维护了一个双链表,支持读取顺序访问,以及
Set
- hashSet: 底层是hashMap key存的是当前元素,vaule统一使用一个object数据.对比元素是否相同先比较hash值,若hash值相同再比较.equals方法。
- LinkedHashSet: 继承 HashSet,方法操作与 HashSet 相同,底层构造一个 LinkedHashMap 来实现。
hashSet和ThreadLocalMap
注意区分
- ThreadLocalMap的key固定为 静态的ThreadLocal
- hashSet的vaule为固定的 object
ThreadLocal 的作用
是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或 者组件之间一些公共变量的传递的复杂度。常见的操作是数据库连接、Session 管理等。
多线程
核心线程池参数
- corePoolSize:指定了线程池中的线程数量
- maximumPoolSize:指定了线程池中的最大线程数量
- keepAliveTime:线程池维护线程所允许的空闲时间
- unit: keepAliveTime 的单位。
- workQueue:任务队列,被提交但尚未被执行的任务。
- threadFactory:线程工厂,用于创建线程,一般用默认的即可。
- handler:拒绝策略。当任务太多来不及处理,如何拒绝任务。
四种线程池
newCachedThreadPool、newFixedThreadPool、newSingleThreadExecutor、newScheduleThreadPool
线程池的执行流程
- 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务
- 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列
- 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务
- 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常RejectExecutionException
线程池的拒绝策略
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
线程池的线程数量怎么确定
- 一般来说,如果是CPU密集型应用,则线程池大小设置为N+1。不太准确
- 一般来说,如果是IO密集型应用,则线程池大小设置为2N+1。不太准确
- 其他点地方对于线程池参数的预估。
阻塞队列
是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
任务缓冲模块是线程池能够管理任务的核心部分。线程池的本质是对任务和线程的管理,而做到这一点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务
锁
synchronized 和 ReentrantLock 的区别
相同:
- 都是用来协调多线程对共享对象、变量的访问
- 都是可重入锁,同一线程可以多次获得同一个锁
- 都保证了可见性和互斥性
不同:
- ReentrantLock 显示的获得、释放锁,synchronized 隐式获得释放锁
- ReentrantLock 可响应中断、可轮回,synchronized 是不可以响应中断的
- ReentrantLock 是 API 级别的,synchronized 是 JVM 级别的
- ReentrantLock 可以实现公平锁
- ReentrantLock 通过 Condition 可以绑定多个条件
- 底层实现不一样, synchronized 是同步阻塞,使用的是悲观并发策略,lock 是同步非阻
塞,采用的是乐观并发策略 - Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言
实现。 - synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;
而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,
因此使用 Lock 时需要在 finally 块中释放锁。 - Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,
- 等待的线程会一直等待下去,不能够响应中断。
- 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
Lock 可以提高多个线程进行读操作的效率,既就是实现读写锁等。
Java中的锁
- 乐观锁:乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为
别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数
据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),
如果失败则要重复读-比较-写的操作。
java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入
值是否一样,一样则更新,否则失败。 - 悲观锁:是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人
会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。
java 中的悲观锁就是 Synchronized,AQS 框架下的锁则是先尝试 cas 乐观锁去获取锁,获取不到,
才会转换为悲观锁,如 RetreenLock。 - 自旋转锁
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁
的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),
等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。- 线程自旋是需要消耗 cup 的,cup做无用功一般有自旋的最大时间。
- 如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
- 适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时 JVM 还针对当前 CPU 的负荷情况做了较多的优化,如果平均负载小于 CPUs 则一直自旋,如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞,如果正在自旋的线程发现 Owner 发生了变化则延迟自旋时间(自旋计数)或进入阻塞,如果 CPU 处于节电模式则停止自旋,自旋时间的最坏情况是 CPU的存储延迟(CPU A 存储了一个数据,到 CPU B 得知这个数据直接的时间差),自旋时会适当放弃线程优先级之间的差异。
- 同步锁 可重入锁
- 公平锁与非公平锁
- 公平锁:加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得
- 非公平锁: 加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待
- ReadWriteLock 读写锁 与数据库的读写锁对比
- 读锁: 可以多人读 不能多人写
- 写锁:写操作上锁
锁优化和膨胀过程
- 自旋锁:自旋锁其实就是在拿锁时发现已经有线程拿了锁,自己如果去拿会阻塞自己,这个时候会选择进行一次忙循环尝试。也就是不停循环看是否能等到上个线程自己释放锁。适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间。
- 锁粗化:虚拟机通过适当扩大加锁的范围以避免频繁的拿锁释放锁的过程。
- 锁消除:通过逃逸分析发现其实根本就没有别的线程产生竞争的可能(别的线程没有临界量的引用),或者同步块内进行的是原子操作,而“自作多情”地给自己加上了锁。有可能虚拟机会直接去掉这个锁。
- 偏向锁:在大多数的情况下,锁不仅不存在多线程的竞争,而且总是由同一个线程获得。因此为了让线程获得锁的代价更低引入了偏向锁的概念。偏向锁的意思是如果一个线程获得了一个偏向锁,如果在接下来的一段时间中没有其他线程来竞争锁,那么持有偏向锁的线程再次进入或者退出同一个同步代码块,不需要再次进行抢占锁和释放锁的操作。
- 轻量级锁:当存在超过一个线程在竞争同一个同步代码块时,会发生偏向锁的撤销。当前线程会尝试使用CAS来获取锁,当自旋超过指定次数(可以自定义)时仍然无法获得锁,此时锁会膨胀升级为重量级锁。
- 重量级锁:重量级锁依赖对象内部的monitor锁来实现,而monitor又依赖操作系统的MutexLock(互斥锁)。当系统检查到是重量级锁之后,会把等待想要获取锁的线程阻塞,被阻塞的线程不会消耗CPU,但是阻塞或者唤醒一个线程,都需要通过操作系统来实现。
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,
也就是说只能从低到高升级,不会出现锁的降级)。也就是锁的升级膨胀过程。
Spring
Bean的生命周期
- 实例化 Instantiation
- 属性赋值 Populate
- 初始化 Initialization
- 销毁 Destruction
三级缓存
第一级缓存:单例缓存池singletonObjects。
第二级缓存:早期提前暴露的对象缓存earlySingletonObjects。(属性还没有值对象也没有被初始化)
第三级缓存:singletonFactories单例对象工厂缓存。为了解决需要代理的情况,不需要代理的情况二级缓存就够用了。
A绑定到ObjectFactory 注册到工厂缓存singletonFactory中,
B在填充A时,先查成品缓存有没有,再查半成品缓存有没有,最后看工厂缓存有没有单例工厂类,有A的ObjectFactory。调用getObject ,执行扩展逻辑,可能返回的代理引用,也可能返回原始引用。
成功获取到A的早期引用,将A放入到半成品缓存中,B填充A引用完毕。
代理问题, 循环依赖问题都解决了。
BeanFactory和ApplicationContext的区别
BeanFactory是Spring里面最低层的接口,提供了最简单的容器的功能,只提供了实例化对象和拿对象的功能。
ApplicationContext应用上下文,继承BeanFactory接口,它是Spring的一各更高级的容器,提供了更多的有用的功能。如国际化,访问资源,载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,消息发送、响应机制,AOP等。
BeanFactory在启动的时候不会去实例化Bean,中有从容器中拿Bean的时候才会去实例化。ApplicationContext在启动的时候就把所有的Bean全部实例化了。它还可以为Bean配置lazy-init=true来让Bean延迟实例化
网络
IO模型
!(IO模型是指在计算机系统中,用于实现输入和输出操作的方式或模式。它描述了数据在应用程序和底层设备之间的传输方式和机制。以下是几种常见的IO模型)[https://zhuanlan.zhihu.com/p/115912936]
-
阻塞IO(Blocking IO):应用程序在执行IO操作时会被阻塞,直到数据传输完成。在阻塞IO模型中,应用程序需要等待IO操作完成后才能继续执行其他任务。
-
非阻塞IO(Non-blocking IO):应用程序可以立即返回并继续执行其他任务,而无需等待IO操作完成。通过使用轮询或选择器等机制,应用程序可以周期性地检查IO操作的状态,并在数据准备就绪时进行读取或写入。
-
IO多路复用(IO Multiplexing):通过使用select、poll或epoll等系统调用,应用程序可以同时监听多个IO事件。当任一IO事件就绪时,应用程序会被通知并进行相应的读取或写入操作。
-
信号驱动IO(Signal-driven IO):应用程序通过使用信号机制,将IO事件的通知绑定到特定的信号上。当IO事件就绪时,操作系统会向应用程序发送相应的信号,应用程序通过信号处理函数进行处理。
-
异步IO(Asynchronous IO):应用程序发起IO操作后,可以继续执行其他任务,而无需等待IO操作完成。当IO操作完成后,操作系统会通知应用程序,并将数据传输给应用程序。异步IO模型通常使用回调函数或事件驱动的方式进行处理。
每种IO模型都有自己的特点和适用场景。选择适合的IO模型取决于应用程序的需求、系统的性能和可伸缩性要求等因素。