-
Java 集合
-
ArrayList 和 LinkedList
-
ArrayList :底层基于数组,根据下标随机访问数组元素效率高。但是在删除和添加时因为需要移动数组元素所以效率比较低。
-
LinkList : 底层基于链表实现,查询慢,增加和删除快,因为删除和增加只需要移动指针即可,但是查询时需要遍历链表。
-
-
HashMap 和 ConcurrentHashMap HashTable
-
HashMap :HashMap 由数组和单向链表组成的数据结构,数组中存储key value也就是Java中的Entry(Java 8中是node),当碰到hash冲突时就需要用到链表了。Java 8 之前插入链表都是头插法,在Java8之后都改成尾插法了。
-
头插法:就是每次新来值时都会在链表头部插入数据,原来的值则被往后放一位。是因为当初设计者认为新来的值更被查找的可能性更大,放在头部可以提升效率。
-
为什么改成尾插法:因为头插法在多线程下resize操作时会改变链表上原本的顺序可能会导致链表出现环,这样在get操作时就会产生死循环。而改成尾插法,则不会改变链表原来的顺序,解决了resize时死循环的问题。
-
HashMap在什么时候resize:1.当往Map里面放第一个值时会初始化map,调用resize方法,默认map大小为16。2.两个因素Capacity:HashMap当前的长度,LoadFactor:负载因子,默认值0.75,当map中的size>Capacity*LoadFactor时进行resize。扩容创建一个新的Entry2大小为原来数组长度的两倍,然后遍历Entry并且reshah到新的Entry2里面。
-
为什么resize时要rehash:因为HashMap的 hash规则跟数据的大小绑定这,所以扩容后hash规则变了所以需rehash。
-
java 8中当链表长度大于8时,将链表转为红黑树。红黑树引入保证在大量hash冲突时保证了查询效率由O(n)提高到哦(logn)。
-
为什么默认长度选择16呢,为什么长度建议是2的幂次方呢?因为在Entry放入数组中时需要计算Entry的索引,计算公式为 hashcode(key)&(length-1)。这里有一个逻辑与的运算。在使用2的 幂的数字运算时length-1的值的所有二进制位全部为1。这种情况下index的值等于HashCode后几位的值。所以只要输入hashcode的值是均匀的那么hash算法就是均匀的。总的来说就是为了减少hash碰撞。
-
threshold 记录hashmap最大容量
-
modCount 记录hashmap内部结构变化次数
-
HashMap put 过程 1.对keyhash 2.判断table数组是否为空,为空则resize 3.按位与运算,找到key对应的数组元素并判断是否为空。为空则直接插入。不为空则判断该位置第一个key是否是当前key,是直接覆盖不是的话判断该位置是否是红黑树,是的话则以红黑树的形式插入并且记录节点。如果不是的话则以链表的形式插入并且记录节点。当链表的数量超过8并且tab长度大于64则会则会转为红黑树。如果链表长度超过8tab长度小于64则会进行扩容。
-
HashMap resize过程 当tab为空或者链表长度大于8并且tab小于64或者存储的数量大于容量3/4时进行扩容。
-
1.判断tab数组长度是否大于0,大于0则判断是否大于map最大值是的话直接返回map,如果不是的话创建一个位原来tab长度二倍的新数组并返回。如果tab长度不大于0,则判断旧的threshold是否大于0,若大于0这直接新建一个threshold长度的tab。若threshold小于0,是第一次初始化map则创建一个默认长度的tab数组threshold=新数组长度乘以负载因子。如果新的threshold等于0则给定一个值。接下来给threshold一个新的值,并且创建一个新的tab数组并且赋值给table。如果旧的table为空直接返回新的table。不为空则遍历元素,如果元素为空则直接进行hash放在table的对应位置。如果不为空判断是否是红黑树,是红黑树则按红黑树的处理方式进行处理。不是红黑树则循环链表处理每一节点数据。
-
-
-
ConcurrentHashMap
-
ConcurrentHashMap1.7 使用数组加链表实现,ConcurrentHashMap1.7采用了分段锁实现,其中 segment继承了 ReentrantLock。不会像hashtable不管put还是get都需要同步处理。理论上ConcurrentHashMap支持segment个数个的并发。每个线程锁住segment时不会影响其他segment。
-
put 方法 1.首先通过key的hash值定位到segment。2.尝试获取锁,失败则证明其他线程占用着segment,所以使用自旋获取锁。如果达到自旋获取的次数后还没获取到则改为阻塞获取,保证最终能获取成功。3.获取锁成功后将segment中的table通过key的hashcode定位到hashentry。4.遍历hashentry ,如果不为空则判断当前key和遍历的key是否相等,如果相等就覆盖。判断node是否为空,为空这创建一个hashEntry,并且判断是否需要扩容。5最后释放锁。
-
get 方法 将key先hash定位到segment然后在hash定位到具体元素上
-
size 先不加锁,连续计算两次(统计每个segment的元素数量)如果想等,说明计算记过准确。如果不相等说明计算过程中发生了增删操作,于是给每个segment加锁进行计算。
-
-
ConcurrentHashMap1.8 抛弃了1.7中的分段锁技术,采用了cas 合 synchronized保证并发安全性。
-
put 方法 1.根据key计算出hashcode。2.判断是否需要进行初始化。3定位出当前key对应的位置,如果为空表示可以写入,利用cas尝试写入,失败了则自旋保证成功。4.如果当前位置hashcode == moved ==-1 则表示要扩容。如果不满足2,3,4则利用synchronized锁写入。6链表数据量大于8则转为红黑树。
-
get 计算hashcode 如果在数组上则直接取值。如果有链表就按照链表的取值方式取值。如果是红黑树则按照树取数据。
-
size :basecount+CounterCell[]的值
-
-
-
-
-
Java ThreadLocal
-
ThreadLocal 类用来存放线程的局部变量,每个线程都有自己的局部变量。
-
threadlocal是怎么实现线程隔离的 每个线程中都有一个ThreadLocalMap的数据结构也就是threadLocals变量。该线程的所有ThreadLocal值都会放在该变量中。ThreadLocalMap内部是一个Entry数据,此数组保存k,v,k的值永远是ThreadLocal对象。值的注意的是ThreadLocal中没有链表,所以在hash冲突时,不会利用链表,而是找到另外一个空位置,在get时根据hash值先定位到Entry的位置,然后判断该位置的Entry的key值是否和当前key值一致,如果不一致就循环去取Entry,直到取到对应key为止。
-
使用threadloca时需要注意什么?为什么呢? 需要注意在用完threadlocal对象后记得调用remove方法,否则容易出现内存泄漏。因为ThreadLocalMap的key使用的是弱引用,那么在gc时如果找个key没有外部强引用,则会被回收,这就导致在ThreadLocalMap中对应的key值为null了,这样就没办法访问改key对应的Entry,如果当前线程一直不被回收的话(例如线程池)就会导致一直存一条这样的强引用。最终导致内存泄漏。
-
Threadlocal使用场景 存用户的登陆信息。spring中的事务管理器应该也是使用这个的。
-
-
-
Java 创建线程池
-
Excutors 类创建线程池 Excutors创建的线程池都是根据ThreadPoolExecutor类创建的,只不过参数不同,对应的线程池功能也有差别
-
Excutors.newCachedThreadPool(); 核心线程数为0,创建的线程都是非核心线程,并且不做数量大小限制,完全由操作系统去控制,当执行的任务数大于当前线程池的线程数量时会新创建线程,当有空闲线程时会回收线程。因为没有对线程数量做限制所以在任务量突增的情况下可能导致内存不足。创建线程池时使用的是SynchronousQueue,改队列的特点是内部只能包含一个元素,插入元素到队列的线程被阻塞,直到另一个线程从队列中获取元素后释放,同样如果线程去获取元素时队列为空那么同样会被阻塞,直到有线程往队列中插入元素。
-
Excutors.newFixedThreadPool(int nThreads) 创建一个指定大小的线程池,参数即为线程池的大小。每次提交任务就创建一个线程,若达到线程池的最大数目则后来任务就必须等待,若其中某个线程异常了,则会补充一个线程。改方法创建的线程永远不会被回收知道线程池关闭。底层队列使用的时LinkedBlockingQueue而这个队列如果不设置大小的话就是默认int的最大值,也就是说时无界阻塞队列,如果说从队列里面取任务的速度没有放入任务的速度快时,可能会导致内存溢出。所以在使用newFixedThreadPool时需要注意。
-
Excutors.newSingleThreadExecutor 创建一个只有一个线程的线程池,也就说单线程执行任务,如果将多个任务交给此线程池处理的话,那么该线程池会按照放入队列的顺序去取数据。如果线程发生异常则会重新创建一个线程。底层队列使用的是LinkedBlockingQueue
-
Excutors.newSingleThreadScheduledExecutor 创建一个支持定时执行的线程池。该线程池只有一个线程。
-
Excutors.newScheduledThreadPool(int corePoolSize)实现方式跟Excutors.newSingleThreadScheduledExecutor一样之不过返回的是参数大小个的线程池。
-
-
ThreadPoolExecutor创建线程池 ThreadPoolExecutor是jdk提供的一个创建线程池的类,可以根据参数灵活的创建不同需求的线程池。
-
ThreadPoolExecutor 构造方法总共提供了7个参数
-
corePoolSize 核心线程数大小 不会被回收的线程除非创建线程时调用了此方法allowsCoreThreadTimeOut那么核心线程数也会被回收
-
maximumPoolSize 最大线程数
-
keepAliveTime 线程空闲多长时间后被回收 当线程空闲时maximumPoolSize>corePoolSize时将回收部分线程
-
unit keepAliveTime的单位
-
workQueue 工作队列 经常使用的有LinkedBlockingQueue无界阻塞队列,有内存溢出的风险。 ArrayBlockingQueue,有界阻塞队列。SynchronousQueue 特殊队列只能存放一个元素,元素未取出和元素未拿到都会阻塞线程。
-
threadFactory 创建线程的工厂
-
handler 拒绝策略。
-
AbortPolice 默认拒绝策略,如果线程池队列存满了则直接抛出异常
-
CallerRunsPolice 如果队列存慢了则用主线程执行任务,直到程序关闭,最后的丢弃任务。
-
DiscardOlderstPolice 如果队列存慢则丢掉最旧的任务,并且尝试新接受新的任务
-
DiscardPolice 队列存慢直接丢弃任务,并且没有异常信息
-
-
-
ThreadPoolExecutor 方法解读
-
excute() 提交任务,没有返回值
-
submit() 提交任务,有返回值,利用Future框架实现,最终提交任务也是使用excute()方法
-
shutdown 关闭线程池并且会执行关闭前提交的任务,但是不会接受新的任务
-
shutdownNow 关闭线程池并且立即停止所有的任务,但是这些任务并不一定能停止成功,但是会返回未执行的任务。
-
isShutdown 判断线程池是running的状态。
-
isTerminating 判断线程是TIDYING状态或者stop状态
-
isTerminated 判断线程池是否是TERMINATED
-
prestartCoreThread()提前启动核心线程池,如果不执行此方法就是只有新任务进来时才启动核心线程。
-
remove()从队列中取消任务
-
allowsCoreThreadTimeOut(Boolean b) 设置是否回收核心线程
-
getActiveCount 返回调用此方式时正在执行任务的线程数
-
getLargestPoolSize 返回线程池中存在过的最大线程数量
-
getTaskCount 返回任务总数
-
getcompletedTaskCount 返回已经完成的总数
-
-
线程池的状态
-
running 此状态可接受任务,并且会处理已经在队列中的任务
-
SHUTDOWN 不接受新任务,但是会处理在队列中的任务。
-
STOP 不接受新任务,不会处理在队列中的任务
-
TIDYING 不接受新任务,所有任务都会被终止
-
Terminated不可接受新任务
-
-
线程池使用AtomicInteger的高低位来记录线程池的状态和线程池的数量高三位记录状态,低29位记录线程数量。
-
值的注意的是线程池的线程执行任务都是在ThreadPoolExecutor类的内部类worker类中的run方法执行
-
-
-
CountDownLatch CountDownLatch能使一个线程等待其他线程执行完毕后在继续执行。在每一个线程执行完后都需要调用countDown()方法,否则CountDownLatch计数器会永远减不到0。当使用CountDownLatch 时任何地方调用CountDownLatch.await()都会阻塞,直至计数器为0或者等待超时。
-
AQS AbstractQueuedSynchronizer aqs的核心思想就是如果当前请求的资源处于空闲状态,那么就将当前线程设置为工作线程,并锁定状态,如果当前资源被占用,那么线程需要被阻塞,在阻塞的过程中会一直尝试自旋获取共享资源的使用权。
-
数据结构时双向链表+锁状态 底层利用cas
-
共享锁 countdown 初始化为n 执行完一个就减一直至到0为止
-
独占锁 reentrantlock 初始化state 为0 获取改为1,释放改为0
-
-
JMM Java 内存模型 保证了线程之间对公共变量操作的可见性
-
volatile
-
保证了线程之间的可见性
-
禁止指令重排序 破坏有序性会导致代码执行出错。
-
对单次读写的原子性
-
-
-
Java 锁
-
乐观锁 乐观锁认为在同步数据时不会有其他线程修改资源,在更新数据时判断有没有线程更新过此数据,如果没有直接更新。
-
乐观锁通常使用cas实现
-
cas容易出现aba问题 可以通过 AtomicStampedReference 解决
-
-
悲观锁 悲观锁认为在同步数据时一定有其他数据修改共享资源 synchronized 合lock都是悲观锁
-
Synchronized 实现同步主要通过Java 对象头实现
-
无锁 无锁就是对共享资源没有做限制,任何线程都能修改,但是同一时间只有一个线程能修改成功。
-
偏向锁 是指一段同步代码一直被一个线程访问,那么该线程就会自动获取锁,降低获取锁的代价
-
轻量级锁 轻量级锁指的是锁是偏向锁时,被另外的线程访问,偏向锁就会升级为轻量级锁。其他线程会通过自旋的形式获取,不会阻塞。
-
重量级锁 此时等待锁的线程都会进入阻塞状态。
-
偏向锁是通过对比mark word 解决加锁问题,避免自旋,轻量级锁是通过cas自旋解决加锁的问题,避免线程阻塞。重量级锁是将其它等待线程全部阻塞。
-
-
公平锁和非公平锁
-
公平锁 获取锁的顺序根据按照申请锁的顺序来
-
非公平锁 指的是申请锁时直接尝试获取,获取不到才排队,假若在申请时刚好锁释放了那么就会获取到锁
-
reentrantlock 可以是公平锁也可以是非公平锁 公平锁里面用队列实现的,如果去获取锁判断自己是不是第一个如果是第一个获取锁,如果不是就排队
-
-
可重入锁 和非可重入锁
-
可重入锁 指在同一线程外层方法获取到锁时,内层方法会自动获取到锁,而不用因为外层方法锁未释放而等待reentrantlock 和Synchronized 都是可重入锁
-
非可重入锁 每一步都会重新获取锁 NonReentrantLock
-
-
独享锁 和共享锁
-
独享锁 又叫排他锁 就说该锁同时只能由一个线程持有Synchronized 和lock都是排他锁
-
共享锁 是指该锁可能被多个线程持有 ReentrantReadWriteLock 里面的readlock 是共享锁,Writelock是排他锁
-
-
自旋锁 MCS也是基于链表公平锁 CLH基于链表实现自旋的,公平锁
-
-