共计 7283 个字符,预计需要花费 19 分钟才能阅读完成。
1、HashMap
解决 hash 抵触,链表法,红黑树和链表互相切换
key 是能够容许为 null 的,在 Node 节点下标为 0 处
替换的原理 :
两个 hash 值必须要相当,而后判断 (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
jdk8 之后的亮点:
1、hash 抵触,应用高 16 和低 16 异或,使得 hash 数据分布更加平均,而后再与 length- 1 相 &,jdk1.7 是间接获得 hash & length -1
2、2 的 n 次方,就能够保障说,(n – 1) & length,能够保障就是 hash % 数组.length 取模的一样的成果
3、扩容:2 倍容量进行扩容,jdk8 不会像 jdk7 那样齐全 hash 一遍,jdk8 扩容结束只能在原来 index 处,或者 index + length 处
jdk1.7 扩容死循环
都是头插入方式惹的祸,比方 : k1 -> k2 -> k3,线程 1 和线程 2 同时要扩容,线程 1 一下子做完了,k3 -> k2 -> k1,线程 2 苏醒过来,k1 -> k2(之前的),就造成环了
jdk1.8 尾插入法
2、ConcurrentHashMap
jdk1.7 ConcurrentHashMap 由 Segment 数组构造和 HashEntry 数组组成。Segment 是一种可重入锁,是一种数据和链表的构造,一个 Segment 中蕴含一个
HashEntry 数组,每个 HashEntry 又是一个链表构造
ConcurrentHashMap 的扩容是仅仅和每个 Segment 元素中 HashEntry 数组的长度无关,但须要扩容时,只扩容以后 Segment 中 HashEntry 数组即可。
也就是说 ConcurrentHashMap 中 Segment[]数组的长度是在初始化的时候就确定了,前面扩容不会扭转这个长度
所以说,针对 jdk1.7 来说,锁的并发度是不能扩容的
jdk1.8 勾销了 segment 数组,间接用 table 保留数据,锁的粒度更小,并发管制应用 synchronized + CAS 来操作(如果 Node<K,V>[] tab 上没有数据就通过 CAS 设置数据),如果有要进行数据插入或者更新,加 synchronized 操作
3、解决并发问题的办法有哪些?
无锁 :
局部变量(每个线程的工作内存中)、不可变对象、ThreadLocal(每个线程一个 Map,Map key 是以后实例对象,value 是本人设置的值)、CAS(内存地址 V,旧值预期值 A,要批改的值 B),当 V == A 的时候,V 才能够批改为 B,在 Java 中的实现则通常是指是以 Atomic 为前缀的一系列类,都采纳了 CAS
存在一个 Unsafe 实例,Unsafe 类体用硬件级别的原子操作,问题:ABA(AtomicStampedReference 记录版本)、循环工夫长开销大、只能保障一个共享变量的原子操作(AtomicReference)
有锁 :
Synchronized 和 ReentrantLock 都是采纳了乐观锁的策略。Synchronized 是通过语言层面来实现,ReentrantLock 是通过编程形式实现
4、共享数据操作
如果一个线程在读取,一个线程在写,有相似如下操作就会有问题 :
TaskInstance taskInstance = taskInstanceCache.get(taskInstanceId);
taskInstance.setState(ExecutionStatus.of(status));
taskInstance.setEndTime(endTime);
怎么办呢?
间接 taskInstanceCache.put(taskInstance); 即可,因为这操作是原子性的
5、CopyOnWriteArrayList 应用场景
读多写少的场景,写的时候就 copy 一份数据,镜像供读取,明确 ArrayList 是有线程平安问题的,如果那种动静注册,低频的,能够应用 CopyOnWrite 模式
依据 DriverManager 来做实例
1、应用了 SPI 定义了 Driver 接口,各个驱动来实现 Drvier 接口,比如说 com.mysql.jdbc.Driver,每个驱动 jar 都有 META-INF/services 只有以 java.sql.Driver 为文件名,value 是主动要实现的驱动,就能够在启动的时候加载驱动了,一旦实例化驱动就会向
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
java.sql.DriverManager 中的 registeredDrivers CopyOnWriteArrayList 进行驱动注册
2、在 DriverManager.getConnection() 的时候,会遍历所有驱动,看是不是合乎,其实就是各个驱动外面的 url 进行判断
6、ThreadLocal
线程和虚拟机栈
在 Java 虚拟机栈空间,每个线程都有本人栈空间,且互相独立,每次能够用不同的参数调用雷同的办法,且线程之间互不影响,每次执行办法的时候,变量都是存储在本人的栈空间,没有了共享,就不会呈现线程平安问题
栈帧是什么 : 是用于虚拟机执行办法调用和办法执行的数据结构,每个办法从调用到办法返回都对应着一个栈帧入栈和出栈的过程,栈帧包含局部变量、操作数栈、动静链接和办法返回地址等信息
ThreadLocal 其实说白了很简略,就是线程外面有一个 Map,Map 的 key 是弱援用,ThreadLocal 的实例,value 是存入的值
所以要设置其值就是往以后线程的 Map 中设置一个值,获取就是获取以后线程的 Map,而后再通过 ThreadLocal 的实例 key 获取你想要的 value
重点
1、key 为什么为弱援用?
其实很简略,按情理来说,ThreadLocal 的生命周期应该是和 Thread 的生命周期是一样的,这样 Thread 生命周期走完了,面对销毁,同样 Thread 中 ThreadLocal.ThreadLocalMap 中的数据也会回收
然而如果是线程池呢?线程池中的线程会回收到线程池中,真正并不销毁,意味着 Thread 还是对 ThreadLocal.ThreadLocalMap 有强援用,因为线程不销毁,ThreadLocal.ThreadLocalMap 还是销毁不了,所以这就很难堪,那我就不销毁了么?
2、所以就呈现了弱援用,弱援用其实说白了,简略了解,就是顺次 GC 过后,如果只有弱援用存在,那我就把你 ThreadLocal.ThreadLocalMap 干掉,然而如果强援用援用了我,好吧,还是须要保留的。所以就呈现了场景
对于这品种的动态变量,
private static ThreadLocal<String> threadLocal
这种状况下,是不会回收的,
比如说是单例。能够线程 1 到线程 n 进行拜访,而后设置值,这样线程 1 到线程 n Thread 中 ThreadLocal.ThreadLocalMap 中都会有 threadLocal 的援用。什么时候销毁呢?
3、针对这种动态变量,除非类销毁,类自身对 threadLocal 有强援用,所以 thread 即便对其实弱援用,也销毁不了。那怎么办呢?线程池中的线程曾经运行结束了,threadLocal 还在我线程中,不合理吧?
所以 ThreadLocal 设计了,倡议不必的,手动 remove。如果不 remove 肯能会造成线程泄露,解决不了。然而针对那种,外界没有援用的 threadLocal 中的 key,会对 Thread 中 ThreadLocal.ThreadLocalMap 中的 key 进行回收,
value 怎么办呢?每次 set,get 的时候会找如果 key 为 null,value 存在就要销毁
7、线程状态
NEW(new thread)、
RUNNABLE(start)、
WAITING(wait,LockSupoort.park)、
TIMED_WAITING(wait(time))、
BLOCKED(synchronized,reentrantlock)、T
TERMINATED(完结)
8、死锁
死锁产生的起因 :
互斥、占用且期待、不可抢占、循环期待
互斥是不能防止的
占用且期待 : 咱们能够一次性申请所有的资源
不可抢占 : 占用局部资源的线程申请其余资源时,如果申请不到,能够在肯定工夫后,被动开释它占用额资源
循环期待 : 依照程序申请资源
9、synchronized 原理
monitorenter 和 monitorexit
Monitor
锁池 : 比如说两个线程同时要加锁拜访数据,须要一一加锁,未加锁的要放入到 EntryList,其实就是一个队列
期待池 : 其实就是比如说条件不满足,是不是没有必要去获取锁去,那就放入到期待池中,条件成熟对你进行 notify 或者 singal,让期待池队列数据放入到锁池
这里必须应用 notifyAll,很简略,因为不晓得该让谁干活,都让你们去竞争锁去吧,如果条件不成立,持续进入期待池中,如果只是随机的 notify 一个,有可能会产生死锁
Owner 是哪个线程获取了锁
锁的分类 :
自旋锁 : 不是锁,是一种机制或者策略,说白了就是在获取不到锁的状况下,自旋一下,不立马开释 CPU 工夫片,因为 CPU 状态切换也很耗时
偏差锁 : 大多数状况下,锁总是由同一线程屡次获取,不存在多线程竞争,所以呈现了偏差锁
轻量级锁 : 偏差锁的时候,被另外的线程锁拜访,偏差锁就会降级为轻量级锁,其余线程会通过自旋的模式获取锁,不会阻塞,CAS
重量级锁 : 乐观锁
10、AQS
偏心锁和非偏心锁,默认是非偏心锁
外面外围组件 : 以后状态 state(加锁数量,可重入锁)、exclusiveOwnerThread(加锁线程)、
tryAcquire
1、没有锁,我间接获取锁,state=1,exclusiveOwnerThread 设置为以后线程,或者是我本人重入锁,持续 state+1
2、没有加锁胜利,怎么办?生成一个 Node.EXCLUSIVE,排他锁,addWaiter 生成一个队列,Node 列表队列,如果不为空,间接往后加,如果为空,初始化一个队列,一个节点 HEAD 节点,什么也不存,之后挂一个节点
3、之后进入一个 for 死循环,看是不是第一个节点啊,如果是第一个节点持续尝试获取锁,其实就是判断,addWaiter(Node.EXCLUSIVE) 前一个节点是不是头节点,如果是头节点,就尝试获取锁
4、获取锁大快人心,获取不到锁呢,设置前驱节点为 SINGAL,间接 LockSupport.park 住了
unlock
1、state – 1 并 exclusiveOwnerThread 设置为 null
2、从后往前遍历,获取第一节点是非 Cancelled 节点且不为 head 的节点 unpark
偏心锁 : 惟一区别在于获取锁的时候,如果有锁,要去队列中看,除非是本人能力加锁,否则请排队,不能插队获取锁
AQS 中有两个队列,竞争锁队列和条件队列,和 Synchronized 逻辑是一样的,不同的是条件队列能够是多个,能够互相 await 和 singal
11、LinkedListBlockQueue & ArrayBlockingQueue
ArrayBlockingQueue
一个锁 lock
两个 Condition notEmpty 和 notFull
同时只有一个线程进行读写
阻塞本人(期待他人唤醒)
LinkedBlockingQueue
阻塞队列
take : 如果为空,就不能 take;应用 takeLock,notEmpty Condition
put : 如果满了,就不能 put,应用 putLock,notFull Condition
两把锁
阻塞本人、唤醒本人和唤醒别人
12、能够 interrupt 的办法
wait()、sleep()、join()、take、put,能够间接 thread.interrupt
13、ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize : 外围线程
maximumPoolSize : 最大线程(最大线程 - 外围线程 = 能够超时的线程)
keepAliveTime : 超时线程工夫
TimeUnit unit : 超时线程工夫单位
workQueue : 罕用的 LinkedBlockingQueue 和 SynchronousQueue
threadFactory : 线程工厂,就是创立线程的工厂,能够应用 Executors.defaultThreadFactory() 或者 自定义
handler : 回绝策略
AbortPolicy : 默认策略,间接抛 RejectedExecutionException 异样,能够比如说在提交线程中进行异样解决,重试提交
DiscardPolicy : 间接扔掉,个别不会这么玩
DiscardOldestPolicy : 其实就是对头移除,队尾减少工作,个别也不必
CallerRunsPolicy : 只有线程还没有敞开,那么这个策略就会间接在提交工作的用户线程运行当前任务,说白了,就是线程池提交不了工作了,我要占用用户线程
比如说 main 线程来执行工作,可能会影响主线程工作的执行
execute(new Runnable()) 在工作提交的时候
1、小于外围线程创立外围线程
2、大于外围线程,向队列中压入
3、队列满,创立非核心线程
addWorker
1、应用 ReentrantLock 加锁,保障了创立线程的 HashSet<Worker> workers
线程平安,
2、Worker 是集成了 AQS,同时实现了 Runnable,阐明 Worker 是一个能够加锁的 Runnable 线程
3、将 Worker 线程启动,Worker 自身是 Runnable,然而外面蕴含一个线程 Thread,将 Runnable 放入到 Thread,而后启动 Worker
Worker 应用 AQS 的两个作用:
在 shutdown 的时候,能够让正在运行的工作运行结束,因为正在运行工作是获取了这个 worker 的锁,所以 shutdown 的时候,去获取锁获取不到,而后就不 interrupt
正在运行的工作能够运行结束之后推出,因为之前曾经设置了 SHUTDOWN 标记
一旦 Worker 启动之后,就开始有限循环的 getTask,如果获取一个工作,则加锁,执行该工作
getTask 很要害 :
如果执行了 shutdown
getTask
1、设置线程池以后的状态为 shutdown,将闲暇的 Worker interrupt 了,Worker run 办法中有两处能够响应 interrupt
第一处就是 task.run(),自身提交的工作是能够响应 interrupt 的,比如说 sleep,第二处是 getTask 的 workQueue.poll 和 workQueue.take 也能够响应中断请求
针对 shutdown 状况下,不会对 task.run 进行 interrupt,最多只会对 getTask 的 workQueue.poll 和 workQueue.take 闲暇线程进行 interrupt
2、如果以后线程状态为 shutdown 状态,且 workQueue.isEmpty()空了,那就间接 return null,或者上面走非核心线程超时
如果执行了 shutdownnow
不论是正在执行的工作还是外围线程阻塞或者非核心线程期待超时的线程,都会进行 interrupt,一起进行退出
SynchronousQueue,是针对 CachedThreadPool 的
CachedThreadPool 创立进去的都是非核心线程,外围原理 :
当 offer 时候,如果 poll 没有线程在获取,那就会失败,间接创立非核心线程
如果在 offer 的时候,正好有线程在 poll,那就会复用之前创立的非核心线程
put 和 take 相对来说是阻塞的,零容忍
14、FutureTask
submit(new Callable()),callable 以结构参数传入到 FutureTask 中
FutureTask 有 Runnable 和 Future 两种个性,其实说白了就是创立了一个返回值的援用对象
submit 其实比 execute 来说就多创立一个 FutureTask 对象,FutureTask 对象封装了 new Callable(),而后其余形式都是一样的
submit 和 execute 不同的是,execute 在 Worker run 办法间接调用 task.run,submit 其实也是调用的 FutureTask run 中的 callable.run 办法
callable.run 是有后果的,如果失常执行结束,通过 CAS 将以后 FutureTask NEW 设置为 COMPLETING,设置 outcome = v(计算结果),调用 finishCompletion
将通过 get park 住的线程进行 unpark(是一个栈,栈中的线程都进行 unpark)
get 的时候,如果实现了间接将值返回,如果未实现,相似 AQS 压栈 线程 LockSupport.park
15、unable to create new native thread(OOM)
如果是 JVM 内溢出,则会使 java.lang.OutOfMemoryError : Java Heap space,能创立多少线程是由一个计算公式的 : 可创立的线程数 = (进行的最大内存 – JVM 调配的内存 – 操作系统预留的内存) / 线程栈大小
如果不显示设置 -Xss 或 -XX:ThreadStackSize 参数的时候,Linux64 上 ThreadStackSize 的默认就是 1024k,也就是 1MB
就是说给 JVM 内存调配的内存越大,逻辑上能创立的线程数量越少
如感兴趣,点赞加关注哦!