集合
ArrayList
按照成员变量、构造函数、关键方法来进行解析
属性
- 维护了一个数组,用于存储元素
- size定义了 ArrayList大小
- 初始容量,大小为10
- 两个空数组
构造方法
共有三种:带初始容量、默认、输入为一个collection集合的
ArrayList的底层实现原理?
- 底层是一个动态数组实现的
- 初始容量为0时,第一次添加元素时才会初始化容量为10。
- 扩容是1.5倍
- 添加&扩容
添加&扩容
执行add操作时,首先去确定ArrayList的内部容量,调用确定内部容量函数时首先要计算出ArrayList所需的容量,并根据所需的容量和ArrayList已有的大小进行对比,如果所需容量大于已有容量,说明需要扩容。在扩容的过程中,是由位操作来保证扩容之后的大小是原来的1.5倍,此外还要确保ArrayList的大小不超过最大值,最后使用Arrays的copytOf方法实现数组拷贝,实现扩容。以上步骤完成后将新元素放在数组中size的位置上。
如何实现ArrayList和数组的转换?
两个静态方法:asList和toArray
那么如果对数组(asList)改变,ArrayList的内容受影响吗?
受影响,因为只是新生成了一个对象指向原来的数组。新生成的ArrayList是一个内部类ArrayList,将其中的a直接指向了原数组。
那么如果对List(toArray)改变,数组的内容受影响吗?
不受影响,是使用copyOf函数将值拷贝到一个新的数组上
两种copy的区别
ArrayList和LinedList的区别
- 底层数据结构
动态数组VS双向链表 - 效率
下标查询上的区别
未知下标查询的时间复杂度
增删时间复杂度 - 空间
内存占用:连续空间节省;额外维护两个指针 - 线程安全
都不是线程安全的
实现线程安全有两种方式:
1.在方法内使用,作为局部变量,可以保证线程安全
2.使用Collection类中的synchronizedList方法进行包装
HashMap
常见数据结构
二叉搜索树
左小右大
- 插入、查找、删除的时间复杂度:O(logn)
红黑树
自平衡的二叉搜索树
- 特点
1.节点要么是红色的,要么是黑色的。
2.根节点为黑色
3.叶子节点都是黑色的空节点
4.红色节点的子节点都是黑色的
5.从任意节点到叶子节点所需经过的黑色节点数目是相同的
在添加或删除节点时,若不满足以上性质,则会发生旋转
查找添加删除的时间复杂度都是O(logn)
HashMap实现原理
数据结构:hash表的数据结构,即数组+链表/红黑树
存储时:出现hash值相同的key:
1.如key相同,则覆盖
2.如key不同,则将当前的k-v对放入链表中
查找时:根据key的hash值寻找其在散列表中的未知,并遍历链表最终找到相同的key
数组大小大于64且链表长度大于8才会发生树化
HashMap的put方法流程
先对HashMap的代码进行分析
-
属性
-
构造
HashMap时惰性加载,在调用无参构造器的时候只初始化了装填因子 -
添加流程
HashMap扩容机制
1.首先看原来是否为空,若原数组为空则正常进行初始化。其中若是使用了指定了默认容量的构造器的话则newCap = oldThr(?)。
2.如果原来的Map不为空,则将其容量和阈值都扩大为原来的二倍。并在此基础上创建一个符合新的容量的数组用来维护索引表。
3.对原数组进行遍历,如果在原索引表上没有进行拉链操作产生hash冲突,则直接放入新的索引表中。
4.如果产生了hash冲突,且后续的是红黑树节点的话,则调用红黑树的添加方式
5.如果后续是链表方式的话,则遍历链表,并使用(e.hash & oldCap) == 0将链表中的元素分为low和high两个链表,以模16扩为模32为例,其中1属于lo,17属于high,然后将分好的链表分别插入对应的位置。
HashMap的寻址算法
- HashMap的寻址算法
1.计算hashcode
2.使用扰动算法是hash分布更均匀
3.最终通过与运算得到索引 - 为什么HashMap的数组长度一定是2^n
效率更高:计算索引&扩容时重新计算索引
1.7下HashMap多线程死循环问题
因为在数据迁移时采用的是头插法,发生数据迁移后链表的顺序将会发生更改,比如原来是A,B的顺序,在线程二中迁移后为B,A。此时线程一也要进行更新,由于B.next已经指向了A,因此在A中实现了A.next = B,B.next = A,在访问时会构成死循环。使用尾插法避免。
多线程
线程基础知识
线程与进程的区别
(首先对进程和线程分别是什么做以简单介绍)
程序由指令和数据构成,想要完成指令的运行需要将指令加载至CPU终,数据的读写则需要将数据加载至内存里。此外指令运行的过程中还需要用到磁盘网络等设备。这是运行一个程序的过程。程序运行时会开启一个进程,进程会用来加载指令,管理内存,管理IO的。 是资源分配的基本单位(多实例进程、单实例进程)
线程是进程的一个实体,是一个指令流,是CPU调度分派的基本单位。一个线程可以创建或者撤销另一个线程。
区别:
- 尺度
线程更小,所以多线程的程序并发性更高。进程至少包含了一个线程,每个线程执行不同的任务。 - 资源
进程是资源分配的基本单位,同一进程内多个线程共享其资源。 - 空间
进程有独立的地址空间,同一进程内多个线程共享其资源。 - 调度
线程是处理器调度的基本单位。 - 执行
线程不能单独执行,要组合成进程才能执行。一个进程至少有一个主线程。 - 切换
线程更加轻量,上下文切换成本比进程的上下文切换成本更低
并行和并发的区别
并发指的是同一时间内处理不同事物的能力
并行指的是同一时间内执行不同事物的能力
举几个例子:
1.在单核CPU中,同时执行线程123。则CPU将在不同的时间片执行不同的线程。宏观并行,围观串行。将这种线程轮流使用CPU的方式叫做并发
2.在多核CPU中每个核心都可以运行不同的线程,此时线程可以是并行的
创建线程的方式有哪些
-
继承Thread类
-
实现Runnable接口
-
实现Callable接口
-
线程池创建
Runnable和Callable的区别
- 返回值不同:Runnable没有返回值,Callable可以规定返回值
- 异常处理不同:Runnable不可抛出异常,只能在方法内部解决,Callable可以在方法外抛出异常。
run()和start()的区别
- run()方法封装了线程执行的代码,可多次调用。
- start()方法用来启动线程,通过该线程调用run()方法,只能调用一次
线程包括哪些状态,状态之间是如何变化的?
- 线程包括哪些状态
在enum类State中,一共有六种
new,runnable,blocked,waiting,timed_waiting,terminated - 线程的状态是如何变化的
首先可以大致分为三个:new,runnable,terminated
一个线程被创建后可以通过start()进入可执行状态,在执行结束后进入terminated状态。
在可执行状态中在一定条件下会进入另外三种状态:
没有获取锁(synchronized,lock)blocked;
调用wait()方法waiting(notify()唤醒后切换为可执行状态);
调用sleep()方法timed_waiting(时间结束后可执行))。
如何保证线程按顺序执行
使用join方法
notify和notifyAll的区别
唤醒一个VS唤醒所有
wait和sleep的异同
- 共同点
都可以是线程放弃处理器使用权,进入阻塞状态 - 不同点
方法归属不同
wait()是Object方法,每个对象都有
sleep是Thread的静态方法
醒来时机不同
wait(long)和wait()可以被notify()唤醒
wait()在不被唤醒的情况下会一直等待
wait(long)和sleep()会在时间结束后自动醒来
锁特性不同
wait()调用时必须先获得wait对象的锁,而sleep没有这个要求
wait()执行后会释放对象锁,允许其他进程获取该锁
sleep()方法在sychronized代码块中执行不会释放对象锁。
如何停止一个正在运行的线程
三种方式:
1.使用退出标志使线程正常退出
2.使用stop方法
3.使用interput方法中断线程
使用interput方法中断线程时,如果打断被阻塞的线程,会抛出异常;打断正常的线程,可以根据打断状态来标记是否退出线程
线程中并发安全
synchronized关键字的底层原理
- synchronized采用互斥的方式保证同一时刻至多只有一个线程可以获得对象锁;
- 底层是由monitor实现的,是jvm级别对象(?),线程获得锁需要将对象与monitor进行关联
- monitor中有三个属性:owner、EnrtyList、waitSet
- 其中owner是获得关联的锁的线程,EnrtyList关联的是处于阻塞状态的线程,waitSet关联的是处于wait状态的线程。
锁升级相关知识
synchronized是重量级锁,里面涉及用户态和内核态的切换、进程之间上下文切换,成本较高,性能较低。
偏向锁和轻量级锁被引进用于没有多线程竞争的场景下因为传统锁机制带来的性能开销问题。
- monitor是如何与对象锁进行关联的?
首先可以看到HotSpot虚拟机中的内存结构如下
其中关联操作主要是对MarkWord进行CAS修改,可以看到不同锁的头如图所示
(CAS,Compare And Swap)
- 那么对象是怎么关联上锁的呢?
重量级锁:使用synchronized给对象上锁后,该对象头的MarkWord中指向monitor对象指针
轻量级锁:
加锁过程:
1.在线程中创建一个LockRecord,将其obj字段指向锁对象
2.通过CAS指令在LockRecord的地址存储在对象头中的MarkWord,若对象处于无锁状态则修改成,代表已经获取了轻量锁
3.如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁
4.如果当前线程已经持有锁了,代表室锁重入,设置LockRecord的第一部分为null
解锁过程:
1.遍历线程栈,找到所有obj reference指向锁对象的LockRecord
2.如果LockRecord的MarkWord为null,代表是重入,将obj reference设置为null后continue
3.如果LockRecord的MarkWord不为null,则利用CAS指令将对象头的MarkWord恢复成无锁状态
偏向锁:
只在第一次进入偏向锁时进行了CAS操作,后面的只是单纯判断obj中的markword中的线程id是否是自己的