3

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)、
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

image.png

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内存分配的内存越大,逻辑上能创建的线程数量越少

如感兴趣,点赞加关注,谢谢


journey
32 声望22 粉丝

« 上一篇
JVM入门
下一篇 »
Zookeeper原理