- ⾸先CopyOnWriteArrayList内部也是⽤过数组来实现的,在向CopyOnWriteArrayList添加元素 时,会复制⼀个新的数组,写操作在新数组上进⾏,读操作在原数组上进⾏
- 并且,写操作会加锁,防⽌出现并发写⼊丢失数据的问题
- 写操作结束之后会把原数组指向新数组
- CopyOnWriteArrayList允许在写操作时来读取数据,⼤⼤提⾼了读的性能,因此适合读多写少的应 ⽤场景,但是CopyOnWriteArrayList会⽐较占内存,同时可能读到的数据不是实时最新的数据,所 以不适合实时性要求很⾼的场景
1.7版本
1. 先⽣成新数组
2. 遍历⽼数组中的每个位置上的链表上的每个元素
3. 取每个元素的key,并基于新数组⻓度,计算出每个元素在新数组中的下标
4. 将元素添加到新数组中去
5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
1.8版本
1. 先⽣成新数组
2. 遍历⽼数组中的每个位置上的链表或红⿊树
3. 如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
4. 如果是红⿊树,则先遍历红⿊树,先计算出红⿊树中每个元素对应在新数组中的下标位置
a. 统计每个下标位置的元素个数
b. 如果该位置下的元素个数超过了8,则⽣成⼀个新的红⿊树,并将根节点的添加到新数组的对 应位置
c. 如果该位置下的元素个数没有超过8,那么则⽣成⼀个链表,并将链表的头节点添加到新数组的对应位置
5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
1.7版本
- 1.7版本的ConcurrentHashMap是基于Segment分段实现的
- 每个Segment相对于⼀个⼩型的HashMap
- 每个Segment内部会进⾏扩容,和HashMap的扩容逻辑类似
- 先⽣成新的数组,然后转移元素到新数组中
- 扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值
1.8版本
- 1.8版本的ConcurrentHashMap不再基于Segment实现
- 当某个线程进⾏put时,如果发现ConcurrentHashMap正在进⾏扩容那么该线程⼀起进⾏扩容
- 如果某个线程put时,发现没有正在进⾏扩容,则将key-value添加到ConcurrentHashMap中,然 后判断是否超过阈值,超过了则进⾏扩容
- ConcurrentHashMap是⽀持多个线程同时扩容的
- 扩容之前也先⽣成⼀个新的数组
- 在转移元素时,先将原数组分组,将每组分给不同的线程来进⾏元素的转移,每个线程负责⼀组或 多组的元素转移⼯作
- ThreadLocal是Java中所提供的线程本地存储机制,可以利⽤该机制将数据缓存在某个线程内部, 该线程可以在任意时刻、任意⽅法中获取缓存的数据
- ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对 象)中都存在⼀个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值
- 如果在线程池中使⽤ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使⽤完之后,应该要 把设置的key,value,也就是Entry对象进⾏回收,但线程池中的线程不会回收,⽽线程对象是通过 强引⽤指向ThreadLocalMap,ThreadLocalMap也是通过强引⽤指向Entry对象,线程不被回收, Entry对象也就不会被回收,从⽽出现内存泄漏,解决办法是,在使⽤了ThreadLocal对象之后,⼿ 动调⽤ThreadLocal的remove⽅法,⼿动清除Entry对象
- ThreadLocal经典的应⽤场景就是连接管理(⼀个线程持有⼀个连接,该连接对象可以在不同的⽅ 法之间进⾏传递,线程之间不共享同⼀个连接)
在并发领域中,存在三⼤特性:原⼦性、有序性、可⻅性。
volatile关键字⽤来修饰对象的属性,在并发环境下可以保证这个属性的可⻅性,对于加了volatile关键字的属性,在对这个属性进⾏修改时,会直接 将CPU⾼级缓存中的数据写回到主内存,对这个变量的读取也会直接从主内存中读取,从⽽保证了可⻅ 性。
底层是通过操作系统的内存屏障来实现的,由于使⽤了内存屏障,所以会禁⽌指令重排,所以同时 也就保证了有序性,在很多并发场景下,如果⽤好volatile关键字可以很好的提⾼执⾏效率。
ReentrantLock中的公平锁和⾮公平锁的底层实现⾸先不管是公平锁和⾮公平锁,它们的底层实现都会使⽤AQS来进⾏排队。
它们的区别在于:线程在使⽤lock()⽅法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队, 则当前线程也进⾏排队,如果是⾮公平锁,则不会去检查是否有线程在排队,⽽是直接竞争锁。 不管是公平锁还是⾮公平锁,⼀旦没竞争到锁,都会进⾏排队,
当锁释放时,都是唤醒排在最前⾯的线 程,所以⾮公平锁只是体现在了线程加锁阶段,⽽没有体现在线程被唤醒阶段。
另外,ReentrantLock是可重⼊锁,不管是公平锁还是⾮公平锁都是可重⼊的。
ReentrantLock中tryLock()和lock()⽅法的区别- tryLock()表示尝试加锁,可能加到,也可能加不到,该⽅法不会阻塞线程,如果加到锁则返回 true,没有加到则返回false
- lock()表示阻塞加锁,线程会阻塞直到加到锁,⽅法也没有返回值
CountDownLatch表示计数器,可以给CountDownLatch设置⼀个数字,⼀个线程调⽤ CountDownLatch的await()将会阻塞,其他线程可以调⽤CountDownLatch的countDown()⽅法来对 CountDownLatch中的数字减⼀,当数字被减成0后,所有await的线程都将被唤醒。
对应的底层原理就是,调⽤await()⽅法的线程会利⽤AQS排队,⼀旦数字被减为0,则会将AQS中 排队的线程依次唤醒。
Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使⽤该信号量,通 过acquire()来获取许可,如果没有许可可⽤则线程阻塞,并通过AQS来排队,可以通过release()⽅法来释放许可,当某个线程释放了某个许可后,会从AQS中正在排队的第⼀个线程开始依次唤 醒,直到没有空闲许可。
Sychronized的偏向锁、轻量级锁、重量级锁- 偏向锁:在锁对象的对象头中记录⼀下当前获取到该锁的线程ID,该线程下次如果⼜来获取该锁就 可以直接获取到了
- 轻量级锁:由偏向锁升级⽽来,当⼀个线程获取到锁后,此时这把锁是偏向锁,此时如果有第⼆个 线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻 量级锁底层是通过⾃旋来实现的,并不会阻塞线程
- 如果⾃旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
- ⾃旋锁:⾃旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就⽆所谓唤醒线程,阻塞和唤醒 这两个步骤都是需要操作系统去进⾏的,⽐较消耗时间,⾃旋锁是线程通过CAS获取预期的⼀个标 记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程⼀直在运 ⾏中,相对⽽⾔没有使⽤太多的操作系统资源,⽐较轻量。
- sychronized是⼀个关键字,ReentrantLock是⼀个类
- sychronized会⾃动的加锁与释放锁,ReentrantLock需要程序员⼿动加锁与释放锁
- sychronized的底层是JVM层⾯的锁,ReentrantLock是API层⾯的锁
- sychronized是⾮公平锁,ReentrantLock可以选择公平锁或⾮公平锁
- sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识 来标识锁的状态
- sychronized底层有⼀个锁升级的过程
线程池内部是通过队列+线程实现的,当我们利⽤线程池执⾏任务时:
- 如果此时线程池中的线程数量⼩于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建 新的线程来处理被添加的任务。
- 如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放⼊ 缓冲队列。
- 如果此时线程池中的线程数量⼤于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量⼩于maximumPoolSize,建新的线程来处理被添加的任务。
- 如果此时线程池中的线程数量⼤于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等 于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
- 当线程池中的线程数量⼤于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被 终⽌。这样,线程池可以动态的调整池中的线程数
对于还在正常运⾏的系统:
- 可以使⽤jmap来查看JVM中各个区域的使⽤情况
- 可以通过jstack来查看线程的运⾏情况,⽐如哪些线程阻塞、是否出现了死锁 3. 可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc⽐较频繁,那么就得进⾏ 调优了
- 通过各个命令的结果,或者jvisualvm等⼯具来进⾏分析
- ⾸先,初步猜测频繁发送fullgc的原因,如果频繁发⽣fullgc但是⼜⼀直没有出现内存溢出,那么表示fullgc实际上是回收了很多对象了,所以这些对象最好能在younggc过程中就直接回收掉,避免 这些对象进⼊到⽼年代,对于这种情况,就要考虑这些存活时间不⻓的对象是不是⽐较⼤,导致年轻代放不下,直接进⼊到了⽼年代,尝试加⼤年轻代的⼤⼩,如果改完之后,fullgc减少,则证明 修改有效
- 同时,还可以找到占⽤CPU最多的线程,定位到具体的⽅法,优化这个⽅法的执⾏,看是否能避免 某些对象的创建,从⽽节省内存
对于已经发⽣了OOM的系统:
6. ⼀般⽣产系统中都会设置当系统发⽣了OOM时,⽣成当时的dump⽂件(- XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base) 2. 我们可以利⽤jsisualvm等⼯具来分析dump⽂件
7. 根据dump⽂件找到异常的实例对象,和异常的线程(占⽤CPU⾼),定位到具体的代码
8. 然后再进⾏详细的分析和调试 总之,调优不是⼀蹴⽽就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题
JVM中存在三个默认的类加载器:
- BootstrapClassLoader
- ExtClassLoader
- AppClassLoader
AppClassLoader的⽗加载器是ExtClassLoader,ExtClassLoader的⽗加载器是 BootstrapClassLoader。
JVM在加载⼀个类时,会调⽤AppClassLoader的loadClass⽅法来加载这个类,不过在这个⽅法中,会 先使⽤ExtClassLoader的loadClass⽅法来加载类,同样ExtClassLoader的loadClass⽅法中会先使⽤ BootstrapClassLoader来加载类,如果BootstrapClassLoader加载到了就直接成功,如果 BootstrapClassLoader没有加载到,那么ExtClassLoader就会⾃⼰尝试加载该类,如果没有加载到, 那么则会由AppClassLoader来加载这个类。 所以,双亲委派指得是,JVM在加载类时,会委派给Ext和Bootstrap进⾏加载,如果没加载到才由⾃⼰ 进⾏加载。
Tomcat中为什么要使⽤⾃定义类加载器⼀个Tomcat中可以部署多个应⽤,⽽每个应⽤中都存在很多类,并且各个应⽤中的类是独⽴的,全类名是可以相同的
⽐如⼀个订单系统中可能存在com.zhouyu.User类,⼀个库存系统中可能也存在 com.zhouyu.User类,⼀个Tomcat,不管内部部署了多少应⽤,Tomcat启动之后就是⼀个Java进程, 也就是⼀个JVM,所以如果Tomcat中只存在⼀个类加载器,⽐如默认的AppClassLoader,那么就只能 加载⼀个com.zhouyu.User类,这是有问题的,⽽在Tomcat中,会为部署的每个应⽤都⽣成⼀个类加载 器实例,名字叫做WebAppClassLoader,这样Tomcat中每个应⽤就可以使⽤⾃⼰的类加载器去加载⾃ ⼰的类,从⽽达到应⽤之间的类隔离,不出现冲突。另外Tomcat还利⽤⾃定义加载器实现了热加载功 能。
热加载功能:如果一个已经被加载的类重新修改之后,那么会重新生成一个WebAppClassLoader实例,之前那个WebAppClassLoader实例就会销毁。
Tomcat如何进⾏优化?对于Tomcat调优,可以从两个⽅⾯来进⾏调整:内存和线程。
- ⾸先启动Tomcat,实际上就是启动了⼀个JVM,所以可以按JVM调优的⽅式来进⾏调整,从⽽达到 Tomcat优化的⽬的。
- 另外Tomcat中设计了⼀些缓存区,⽐如appReadBufSize、bufferPoolSize等缓存区来提⾼吞吐量。
- 还可以调整Tomcat的线程,⽐如调整minSpareThreads参数来改变Tomcat空闲时的线程数,调整 maxThreads参数来设置Tomcat处理连接的最⼤线程数。 并且还可以调整IO模型,⽐如使⽤NIO、APR这种相⽐于BIO更加⾼效的IO模型
- 浏览器解析⽤户输⼊的URL,⽣成⼀个HTTP格式的请求
- 先根据URL域名从本地hosts⽂件查找是否有映射IP,如果没有就将域名发送给电脑所配置的DNS进 ⾏域名解析,得到IP地址
- 浏览器通过操作系统将请求通过四层⽹络协议发送出去
- 途中可能会经过各种路由器、交换机,最终到达服务器
- 服务器收到请求后,根据请求所指定的端⼝,将请求传递给绑定了该端⼝的应⽤程序,⽐如8080被 tomcat占⽤了
- tomcat接收到请求数据后,按照http协议的格式进⾏解析,解析得到所要访问的servlet
- 然后servlet来处理这个请求,如果是SpringMVC中的DispatcherServlet,那么则会找到对应的 Controller中的⽅法,并执⾏该⽅法得到结果
- Tomcat得到响应结果后封装成HTTP响应的格式,并再次通过⽹络发送给浏览器所在的服务器
- 浏览器所在的服务器拿到结果后再传递给浏览器,浏览器则负责解析并渲染



