底层是⼀个数组,当这个数组满了之后,他就会⾃动进⾏扩容,变成⼀个更⼤的数组,让你在⾥⾯可以去放更多的元素
2倍扩容
[16位的数组,<> -> <> -> <>]
[32位的数组,<> -> <>, <>]
数组长度=16
n - 1 0000 0000 0000 0000 0000 0000 0000 1111
hash1 1111 1111 1111 1111 0000 1111 0000 0101
&结果 0000 0000 0000 0000 0000 0000 0000 0101 = 5(index = 5的位置)
n - 1 0000 0000 0000 0000 0000 0000 0000 1111
hash2 1111 1111 1111 1111 0000 1111 0001 0101
&结果 0000 0000 0000 0000 0000 0000 00000101 = 5(index = 5的位置)
在数组长度为16的时候,他们两个hash值的位置是⼀样的,⽤链表来处理,出现⼀个hash冲突的问题
如果数组的长度扩容之后= 32,重新对每个hash值进⾏寻址,也就是⽤每个hash值跟新数组的length - 1进⾏与操作
n-1 0000 0000 0000 0000 0000 0000 00011111
hash1 1111 1111 1111 1111 0000 1111 0000 0101
&结果 0000 0000 0000 0000 0000 0000 0000 0101 = 5(index = 5的位置)
n-1 0000 0000 0000 0000 0000 0000 00011111
hash2 1111 1111 1111 1111 0000 1111 0001 0101
&结果 0000 0000 0000 0000 0000 0000 00010101 = 21(index = 21的位置)
** 判断⼆进制结果中是否多出⼀个bit的1,如果没多,那么就是原来的index,如果多了出来,那么就是index + oldCap,通过这个⽅式,**就避免了rehash的时候,⽤每个hash对新数组.length取模,取模性能不⾼,位运算的性能⽐较⾼。
synchronized实现原理、CAS⽆锁化的原理、AQS是什么、Lock锁、ConcurrentHashMap的分段加锁的原理、线程池的原理、java内存模型、volatile说⼀下吗、对java并发包有什么了解?⼀连串的问题。
其实synchronized底层的原理,是跟jvm指令和monitor有关系的
你如果⽤到了synchronized关键字,在底层编译后的jvm指令中,会有monitorenter和monitorexit两个指令monitorenter
//代码对应的指令
monitorexit
那么monitorenter指令执⾏的时候会⼲什么呢?
每个对象都有⼀个关联的monitor,⽐如⼀个对象实例就有⼀个monitor,⼀个类的Class对象也有⼀个monitor,如果要对这个对象加锁,那么必须获取这个对象关联的monitor的lock锁。
**原理:**monitor⾥⾯有⼀个计数器,从0开始的。如果⼀个线程要获取monitor的锁,就看看他的计数器是不 是0,如果是0的话,那么说明没⼈获取锁,他就可以获取锁了,然后对计数器加1。
这个monitor的锁是⽀持重⼊加锁的,什么意思呢,好⽐下⾯的代码⽚段
// 线程1
synchronized(myObject) { -> 类的class对象来⾛的
// ⼀⼤堆的代码
synchronized(myObject) {
// ⼀⼤堆的代码
}
}
加锁,⼀般来说都是必须对⼀个对象进⾏加锁。
如果⼀个线程第⼀次synchronized那⾥,获取到了myObject对象的monitor的锁,计数器加1,然后第⼆次synchronized那⾥,会再次获取myObject对象的monitor的锁,这个就是重⼊加锁了,然后计数器会再次加1,变成2。
这个时候,其他的线程在第⼀次synchronized那⾥,会发现说myObject对象的monitor锁的计数器是⼤于0的,意味着被别⼈加锁了,然后此时线程就会进⼊block阻塞状态,什么都⼲不了,就是等着获取锁 。
接着如果出了synchronized修饰的代码⽚段的范围,就会有⼀个monitorexit的指令,在底层。此时获取锁的线程就会对那个对象的monitor的计数器减1,如果有多次重⼊加锁就会对应多次减1,直到最后,计数器是0 。
然后后⾯block住阻塞的线程,会再次尝试获取锁,但是只有⼀个线程可以获取到锁。
多个线程他们可能要访问同⼀个数据 HashMap map = new HashMap();
此时有多个线程要同时读写类似上⾯的这种内存⾥的数据,此时必然出现多线程的并发安全问题。
synchronized(map) {
// 对map⾥的数据进⾏复杂的读写处理
}
此时,synchronized意思就是针对当前执⾏这个⽅法的myObject对象进⾏加锁,只有⼀个线程可以成功的对myObject加锁,可以对他关联的monitor的计数器去加1,加锁,⼀旦多个线程并发的去进⾏synchronized加锁,串⾏化,效率并不是太⾼,很多线程,都需要排队去执⾏。
CAS在底层的硬件级别给你保证⼀定是原⼦的,同⼀时间只有⼀个线程可以执⾏CAS,先⽐较再设置,其他的线程的CAS同时间去执⾏此时会失败。
线程1和线程分别读取0到本地内存。线程1货渠到0后+1,然后比较本地值0和内存值0相等,则更新内存值为1。线程2比较本地值0和内存值1不等则更新失败。则尝试设置为2成功。
多个线程要访问同⼀个数据,synchronized加锁,CAS去进⾏安全的累加,去实现多线程场景下的安全的更新⼀个数据的效果,⽐较多的⼀个场景下,可能就是多个线程同时读写⼀个HashMap。使用synchronized,也没这个必要。
HashMap的⼀个底层的原理,本⾝是⼀个⼤的⼀个数组,[有很多的元素] Map map = new Map();
多个线程过来,线程1要put的位置是数组[5],线程2要put的位置是数组[21]
map.put(xxxxx,xxx);
明显不好,数组⾥有很多的元素,插入不同位置时可以并发插入的。所以除⾮是对同⼀个元素执⾏put操作,此时呢需要多线程是需要进⾏同步的。
JDK并发包⾥推出了⼀个ConcurrentHashMap,他默认实现了线程安全性。
** 在JDK 1.7及之前的版本⾥,**分段 即将原数组拆分为多个数组,每个数组分别加锁: [数组1] , [数组2],[数组3] ->每个数组都对应⼀个锁分段加锁,多个线程过来,线程1要put的位置是数组1[5],线程2要put的位置是数组2[21]。此时对于不同线程在不同数组中插入数据没有冲突。
** JDK 1.8以及之后**,做了⼀些优化和改进,锁粒度的细化 。[⼀个⼤的数组],数组⾥每个元素进⾏put操作,都是有⼀个不同的锁,刚开始进⾏put的时候,如果两个线程都是在数组[5]这个位置进⾏put,这个时候,对数组[5]这个位置进⾏put的时候,采取的是CAS的策略。
同⼀个时间,只有⼀个线程能成功执⾏这个CAS,就是说他刚开始先获取⼀下数组[5]这个位置的值,null,然后执⾏CAS,线程1⽐较⼀下put进去我的这条数据,同时间其他的线程执⾏CAS,都会失败。
分段加锁,通过对数组每个元素执⾏CAS的策略,如果是很多线程对数组⾥不同的元素执⾏put,⼤家是没有关系的,如果其他⼈失败了,其他⼈此时会发现说,数组[5]这位置,已经给刚才⼜⼈放进去值了。就需要在这个位置基于链表+红⿊树来进⾏处理,synchronized(数组[5]),加锁,基于链表或者是红⿊树在这个位置插进去⾃⼰的数据。
如果你是对数组⾥同⼀个位置的元素进⾏操作,才会加锁串⾏化处理;如果是对数组不同位置的元素操作,此时⼤家可以并发执⾏的。
首先对整个数组加锁 ------》 对数组分段加锁 ------》 对每个元素加锁。
多线程同时访问⼀个共享数据,可以使用sychronized,CAS,ConcurrentHashMap(并发安全的数据结构可以来⽤),Lock等方式处理。
synchronized就有点不⼀样了,底层基于 AQS,Abstract Queue Synchronizer,抽象队列同步器 。以及Semaphore、其他⼀些的并发包下的都是基于AQS。
ReentrantLock lock = new ReentrantLock(true); => ⾮公平锁
//多个线程过来,都尝试
lock.lock();
lock.unlock();
**原理分析:**AQS内部的重要变量state=0。此时有多个线程都执行lock.lock()进行加锁。
**非公平锁:**当上图线程2被线程1唤醒后,此时线程3直接先进行CAS将state=1和加锁线程=线程3。然后线程2进行CAS失败继续进入等待队列。故此时线程2时不公平的。
**公平锁:**当上图线程2被线程1唤醒后,此时线程3首先会判断队列:发现队列有线程2,线程3直接进入等待队列排队。
state变量 -> CAS ->失败后进⼊队列等待->释放锁后唤醒 公平与非公平
系统是不可能说让他⽆限制的创建很多很多的线程的,会构建⼀个线程池,有⼀定数量的线程,让他们执⾏各种各样的任务,线程执⾏完任务之后,不要销毁掉⾃⼰,继续去等待执⾏下⼀个任务。
ExecutorService threadPool = Executors.newFixedThreadPool(3) -> 3: corePoolSize
threadPool.submit(new Callable() {
public void run() {}
});
提交任务,先看⼀下线程池⾥的线程数量是否⼩于corePoolSize,也就是3,如果⼩于,直接创建⼀个线程出来执⾏你的任务。
如果执⾏完你的任务之后,这个线程是不会死掉,他会尝试从⼀个⽆界LinkedBlockingQueue⾥获取新的任务,如果没有新的任务,此时就会阻塞住,等待新的任务到来。
你持续提交任务,上述流程反复执⾏,只要线程池的线程数量⼩于corePoolSize,都会直接创建新线程来执⾏这个任务,执⾏完了就尝试从⽆界队列⾥获取任务,直到线程池⾥有corePoolSize个线程。
接着再次提交任务,会发现线程数量已经跟corePoolSize⼀样⼤了,此时就直接把任务放⼊队列中就可以了,线程会争抢获取任务执⾏的,如果所有的线程此时都在执⾏任务,那么⽆界队列⾥的任务就可能会越来越多。
newFixedThreadPool的队列是LinkedBlockingQueue,一个⽆界阻塞队列。
代表线程池的类是ThreadPoolExecutor。
创建⼀个线程池参数corePoolSize,maximumPoolSize,keepAliveTimequeue,如果你不⽤fixed之类的线程池,⾃⼰完全可以通过这个构造函数就创建⾃⼰的线程池。corePoolSize:3
maximumPoolSize:Integer.MAX_VALUE
keepAliveTime:60s
new ArrayBlockingQueue<Runnable>(200)
如果说你把queue做成有界队列,⽐如说new ArrayBlockingQueue<Runnable>(200),那么假设corePoolSize个线程都在繁忙的⼯作,⼤量任务进⼊有界队列,队列满了,此时怎么办?
这个时候假设你的maximumPoolSize是⽐corePoolSize⼤的,此时会继续创建额外的线程放
⼊线程池⾥,来处理这些任务,然后超过corePoolSize数量的线程如果处理完了⼀个任务也会尝试
从队列⾥去获取任务来执⾏。
如果额外线程都创建完了去处理任务,队列还是满的,此时还有新的任务来怎么办?
只能reject掉,他有⼏种reject策略,可以传⼊RejectedExecutionHandler。(1)AbortPolicy
(2)DiscardPolicy
(3)DiscardOldestPolicy
(4)CallerRunsPolicy
(5)⾃定义
如果后续慢慢的队列⾥没任务了,线程空闲了,超过corePoolSize的线程会⾃动释放掉,在keepAliveTime之后就会释放。
根据上述原理去定制⾃⼰的线程池,考虑到corePoolSize的数量,队列类型,最⼤线程数量,拒绝策略,线程释放时间
⼀般⽐较常⽤的是:fixed线程。
⾯试题:在远程服务异常的情况下,使⽤⽆界阻塞队列,是否会导致内存异常飙升?
当线程调用远程服务调⽤超时,导致任务处理很慢,而任务进来会很快导致 。队列变得越来越⼤,此时会导致内存飙升起来,⽽且还可能会导致你会OOM,内存溢出。
有界队列,可以避免内存溢出。corePoolSize: 10
maximumPoolSize : 200
ArrayBlockingQueue(200)
⾃定义⼀个reject策略,如果线程池⽆法执⾏更多的任务了,此时建议你可以把这个任务信息持久化写⼊磁盘⾥去,后台专门启动⼀个线程,后续等待你的线程池的⼯作负载降低了,他可以慢慢的从磁盘⾥读取之前持久化的任务,重新提交到线程池⾥去执⾏。
你可以⽆限制的不停的创建额外的线程出来,⼀台机器上,有⼏千个线程,甚⾄是⼏万个线程,每个线程都有⾃⼰的栈内存,占⽤⼀定的内存资源,会导致内存资源耗尽,系统也会崩溃掉
即使内存没有崩溃,会导致你的机器的cpu load,负载,特别的⾼。
必然会导致线程池⾥的积压的任务实际上来说都是会丢失的。如果说你要提交⼀个任务到线程池⾥去,在提交之前,⿇烦你先在数据库⾥插⼊这个任务的信息,更新他的状态:未提交、已提交、已完成。提交成功之后,更新他的状态是已提交状态。
系统重启,后台线程去扫描数据库⾥的未提交和已提交状态的任务,可以把任务的信息读取出来,重新提交到线程池⾥去,继续进⾏执⾏。
无界队列不断产生任务:导致内存飙升,内存溢出,OOM
有界队列不断创建线程:线程栈导致内存资源耗尽,cpu负载高。
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/mingyuli/article/details/120755205
内容来源于网络,如有侵权,请联系作者删除!