-
JDK、JRE、JVM,以及三者的关系
1)JDK 指的是 java 开发工具包,它包括编译器、JAVA核心类库、JVM、开发辅助工具(jps、jinfo、jmap、jconsole、jvisualvm)
2)JRE 指的是 JAVA 程序运行环境,主要包括 JAVA 核心类库、JVM
3)JVM 是字节码执行引擎,java 程序运行在 java 虚拟机上,同时负责 java 程序的内存管理、垃圾回收
三者的关系:JDK 包含 JRE,JRE 包含 JVM -
String 和 StringBuffer、StringBuilder 的区别是什么?
主要从两个方面来比较,一个是可变性,一个是线程安全性
1)String 是不可变字符串;StringBuffer、StringBuilder 是可变字符串,可以通过 append() 方法来改变
2)线程安全性:String 因为是对象不可变的,因此它是线程安全的,StringBuffer 的方法度加了 synchronized,它是线程安全的,而 StringBuilder 的方法没有加锁,因此它是线程不安全的 -
String 为什么是不可变的
final 关键字修饰的数组保存字符串并不是 String 不可变的根本原因,因为这个数组保存的字符串是可变的
1)保存字符串的 char[] 数组被 final 修饰且为私有的,并且 String 类并没有提供修改这个字符串的方法
2)String 类被 final 修饰,因此其不能被继承,进而避免了子类破坏 String 的不可变性。如果 String 类没有被 final 修饰,那就可以通过继承 String 类,然后重写 String 的构造方法,从而导致 String 变成可变的 -
String 为什么要设计成不可变的
1)保证线程安全,因为 String 是一个比较特殊的对象,假如一个字符串在多个地方被使用,如果 String 是可变的,那么一个地方修改就会引起其它地方的同步改动,这样的话会给程序带来很严重的不可靠性
2)字符串常量池的需求,字符串常量池需要 String 不可变。当创建一个 String 对象时,若此字符串已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。如果字符串变量允许改变,会导致各种逻辑错误,如改变一个对象会影响到另一个独立对象
3)支撑哈希表(HashMap/HashSet)的正常工作:当 String 作为 HashMap 的 Key 时,若后续修改了 String 的字符序列,其 hashCode 会随之改变。此时,原 Key 所在的哈希桶位置与新 hashCode 对应的桶位置不一致,导致后续无法通过 Key 找到对应的 Value,哈希表彻底失效
4)保障 Java 核心 API 的安全性:文件路径(File file = new File(String path)):若 path 可变,创建 File 后若 path 被篡改,可能导致程序访问错误的文件(甚至敏感文件) -
重载和重写
-
接口和抽象类
-
封装、继承、多态
-
ArrayList 为什么是线程不安全的
ArrayList 线程不安全,主要是因为它的底层操作没加锁。比如多个线程同时 add 元素时,都会先拿当前 size 当索引,然后赋值、size+1。这两步如果不同步,就可能两个线程用同一个索引,后加的元素把先加的覆盖了,数据就丢了。还有扩容的时候,多个线程同时复制数组,也可能搞乱数据,甚至抛出数组越界异常。另外,一边遍历一边修改,还会触发并发修改异常,因为它内部有个修改计数器,不一致就报错 -
如何解决ArrayList的线程安全问题
1)用 Vector,它的方法都加了 synchronized,强制线程排队,简单但性能一般;
2)用 Collections.synchronizedList 包装一下 ArrayList,原理和 Vector 类似,也是加锁,适合并发不高的场景;
3)高并发尤其是读多写少的话,用 CopyOnWriteArrayList,写的时候复制一份新数组改,读的时候直接读旧的,不用锁,效率高,但写起来开销大,可能读到老数据 -
HashMap的数据结构
-
HashMap put() 方法的执行过程
1)先算 key 的 hash 值,再用 hash 值和数组长度减一做与运算,确定存到数组的哪个位置(桶);
2)如果这个桶是空的,直接把键值对包装成节点放进去;
3)如果桶里有东西,就比较 key:相同的话就替换旧值;不同的话,看这个桶是链表还是红黑树(JDK8 后),链表的话就往后插,插多了(超过 8 个)会转成红黑树;红黑树就按树的规则插入;
4)最后如果元素数量超过阈值(数组长度 * 负载因子,默认 0.75),就会扩容,数组长度翻倍,把元素重新分配到新数组里。 -
HashMap get() 方法的执行过程
1)先算 key 的 hash 值,找到对应的桶位置;
2)然后遍历这个桶里的元素(链表或红黑树),用 key 的 hash 值和 equals 方法比对,找到匹配的节点,返回它的 value;
3)如果整个桶里都没找到,就返回 null。 -
HashMap 线程不安全的原因
1)并发扩容时导致死循环
JDK 7 中 HashMap 采用「头插法」处理哈希冲突的链表,并发扩容时可能导致链表成环,引发死循环
原理:
· 当 HashMap 元素数量超过阈值(capacity * loadFactor)时,会触发扩容(resize()),新建更大的数组并将旧元素迁移到新数组
· 并发扩容时,多个线程同时迁移同一链表,由于「头插法」会改变链表顺序,可能导致两个节点互相引用,形成环形链表
· 后续查询该链表时,会陷入无限循环(next 指针永远无法指向 null),最终导致 CPU 使用率飙升
(大白话:两个线程在同时进行put操作时,由于元素数量超过阈值而同时进行扩容,多个线程迁移同一链表,头插法会导致形成环形链表,后续查询该链表时,会导致死循环)
2)并发执行 put() 操作时,可能导致后插入的元素覆盖先插入的元素,造成数据丢失
原理:
线程 A 计算出 key 的哈希索引后,判断该位置为空,准备插入新节点
线程 B 同时计算出相同的哈希索引,且也判断该位置为空,先于线程 A 插入新节点
线程 A 恢复运行后,仍认为该位置为空,直接插入新节点,覆盖线程 B 插入的数据
3)get() 可能返回 null
一个线程执行 put() 插入元素后,另一个线程执行 get() 可能无法获取到该元素,返回 null
原理:
HashMap 的 size、table 等字段未被 volatile 修饰,不保证多线程间的内存可见性
线程 A 插入元素后,其修改的 table 或 size 可能未及时刷新到主内存
线程 B 读取时仍使用本地缓存中的旧数据,导致无法感知线程 A 插入的新元素
-
ConcurrentHashMap
ConcurrentHashMap 是线程安全的 HashMap,比 Hashtable 好用,因为它锁得更细,性能高。
JDK7 里是 “分段锁”,整个数组分成几个段,每个段一把锁,不同段的操作可以并发,效率高;
JDK8 之后改成了 “CAS+synchronized”,直接锁每个桶的头节点,粒度更细了,并发度更高。
它支持并发读写,读操作基本不用锁,写操作只锁当前桶,所以多线程用起来又安全又高效,适合高并发场景。 -
线程池
1)构造方法参数含义
· corePoolSize,线程池核心线程数
· maximumPoolSize 线程池最大线程数量
· keepAliveTime 非核心线程存活时间
· unit 非核心线程存活时间单位
· workQueue 工作队列
· ArrayBlockingQueue:基于数组的有界阻塞队列
· LinkedBlockingQuene:基于链表的无界阻塞队列
· SynchronousQuene:一个不缓存任务的阻塞队列
· PriorityBlockingQueue:具有优先级的无界阻塞队列
· threadFactory:线程工厂,创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等
· handler 拒绝策略
· CallerRunsPolicy:在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务
· AbortPolicy:直接丢弃任务,并抛出RejectedExecutionException异常
· DiscardPolicy:直接丢弃任务,什么都不做
· DiscardOldestPolicy:抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列
豆包:
线程池构造方法有几个核心参数,就像给线程池定规矩:
核心线程数:线程池长期保持的线程数量,即使没事干也不删;
最大线程数:线程池最多能创建的线程数,超过核心数的线程是临时的,闲久了会被删;
空闲时间:临时线程(超过核心数的)没事干多久后会被回收;
时间单位:空闲时间的单位,比如秒、毫秒;
工作队列:任务太多时,先存在这个队列里,等线程有空了再取;
线程工厂:用来创建线程的,一般用默认的就行;
拒绝策略:任务太多,队列满了且线程也到最大数了,怎么处理新任务?比如直接抛异常、让提交任务的线程自己执行,等等。
2)工作机制:最开始启用核心线程处理任务,当所有核心线程全部都在运行,并且有新的任务进来的时候,这些任务会放入阻塞队列中,如果阻塞队列满了,就会启动非核心线程处理任务,如果所有的线程都在运行,并且阻塞队列满的时候,会启用拒绝策略,拒绝策略有4种,默认的拒绝策略是直接抛出异常
豆包:
来了新任务,先看核心线程够不够,不够就新建核心线程执行;
核心线程满了,就把任务放到工作队列里,等核心线程干完手头的再去队列里取;
队列也满了,就看有没有到最大线程数,没到就新建临时线程来执行;
临时线程也满了(到最大数了),就触发拒绝策略处理新任务;
临时线程闲太久(超过空闲时间),就会被回收,最后保持在核心线程数。
- synchronized
1)作用:可以保证原子性(一段代码以原子的方式执行,不会执行到一半就中断)、可见性、有序性
2)使用方式:可以修饰方法、修饰代码块
3)底层实现原理:synchronized 属于jvm层面的,如果修饰的是代码块,则会生成两条jvm字节码指令 monitorenter 和 monitorexit,monitorenter指向代码块开始的位置,monitorexit 指向代码块结束的位置;如果修饰的是方法,则会生成一个 ACC_SYNCHRONIZED 标识符,标识这个方法是一个同步方法
4)锁的性质:非公平锁、悲观锁、可重入锁
5)锁升级的过程:在JDK1.6之前,synchronized被认为是重量级锁,在JDK1.6之后,引入了偏向锁和轻量级锁来减少获得锁和释放锁带来的性能消耗。锁的状态有四种,分别是无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,当没有线程持有锁的时候,它是无锁状态,当有一个线程持有锁的时候,它会改成偏向锁状态,如果没有其它线程来争夺这个锁,那么就一直处于偏向锁状态,当有其它线程来争夺锁的时候,这把锁就会改成轻量级锁状态,其它获取不到锁的线程一直在自旋,当自旋的次数达到一定次数还是获取不到锁的话,它就会认为自旋的成本和代价太高昂了,浪费CPU的资源,因此这把锁就会膨胀为重量级锁
6)是如何实现可重入的:每一个锁关联一个线程持有者和计数器,当计数器为0时表示锁是空闲的,当某个线程请求获取锁成功之后,计数器加1,当这个线程再次申请锁时,计数器继续加1,当退出同步代码块时,计数器会递减,当计数器为0时则表示释放锁
-
ReentrantLock
-
CAS
1.什么是 CAS
1)CAS是一种乐观锁机制,指的是比较并交换,有的称比较并设置,意思都一样,它是这样的:现在要去改变一个变量的值,在改变之前先把这个值记录下来,这个值称为旧值,然后进行设置新值的时候,会先判断一下这个旧值有没有改变,如果没有改变的话就可以设置成功,如果发生改变的话就放弃修改
2)CAS的底层实现
CAS的底层实现是基于CPU原语来保证操作的原子性。在JDK层面,提供了Unsafe这个类来进行CAS的操作
在Java层面,CAS相关的接口由sun.misc.Unsafe类提供,其中CAS相关的方法都是native方法
豆包:
CAS 是 “比较并交换”,是种无锁的并发控制方式,比加锁效率高。它有三个参数:内存地址 V、旧的预期值 A、新值 B。意思是 “我认为 V 地址现在的值是 A,如果是的话就改成 B,不是的话就不改,还告诉我现在实际是啥”。比如多个线程抢着改一个变量,每个线程用 CAS 试,只有一个能成功,其他的失败了可以重试或放弃。好处是不用加锁,避免线程切换开销,但有个 “ABA 问题”(值从 A 变 B 又变 A,CAS 以为没变),可以用版本号解决。Java 里的 AtomicInteger 这些原子类就是靠 CAS 实现的。
- volatile
1)前置知识
在理解 volatile 之前,有必要先了解 Java 内存模型和 happens-before 规则
2)java 内存模型:假如有一个线程的共享变量,这个变量是存在于主内存中的,但是为了提高效率,每个线程在自己的工作内存中有一份该变量的拷贝。(主内存映射到硬件可以理解为平常说的内存,而工作内存可以理解为CPU中的高速缓存存储器,CPU从高速缓存存储器中读数据肯定要比从内存中读数据要快得多)
3)happens-before 规则:
4)作用:可以保证可见性、有序性
· 可见性:当一个线程修改了一个 volatile 变量的值,其他线程能够立即看到这个修改
· 有序性:volatile 通过禁止指令重排序来保证代码的顺序执行,即 volatile 变量的读写操作不会被编译器和处理器重排序
5)实现原理
· 可见性:可见性是基于缓存一致性协议实现。当一个线程写入 volatile 变量时,它会强制将线程工作内存中的变量值刷新到主内存。当其他线程读取这个变量时,它们必须从主内存中读取该值,而不是从自己的工作内存中读取可能过时的副本
· 有序性是基于内存屏障(LoadLoad Barriers、LoadStore Barriers、StoreStore Barriers、StoreLoad Barries)实现的
6)使用方式
-
ThreadLocal 是什么
1)ThreadLocal 直译为线程本地,也称为线程本地变量
2)意思是在 ThreadLocal 变量中填充的值只属于当前线程,对其它线程是不可见的,从而起到线程隔离的作用,避免了线程安全问题 -
ThreadLocal 的作用(为什么需要 ThreadLocal)
1)在线程之间隔离数据,对于同一个变量,每个线程都有独立的数据
2)减少锁的使用,如果没有 ThreadLocal,在某些并发场景下需要加锁来解决 -
ThreadLocal 内存泄露问题
1)ThreadLocal 内存泄露是指 ThreadLocalMap 中的 value 没办法被回收
2)内存泄露原因:
ThreadLocal 已经使用结束了(意味着没有强引用指向堆中的 ThreadLocal 对象),而线程还存活着,JVM 在进行垃圾回收后会把只有弱引用指向的 ThreadLocal 对象回收,也就是 Entry 的 key 会被回收,但是此时 value 还在,因此就产生了内存泄露
3)如何避免内
内存泄露:在使用完 ThreadLocal 之后,调用 remove() 方法把 Entry 置空
- AQS
1)AQS 的核心组成部分
· volatile修饰的state变量:当它的值为0的时候,就表示资源是空闲的,当为1或者是其他数值的时候,就表示资源处于锁定状态
· 等待队列:它的底层数据结构是双向链表,那些获取不到资源的线程会被包装成一个Node节点,放到这个队列当中
· CAS:相当于是一种轻量级的并发处理,因为修改属性的时候是多个线程同时去修改的,但是最终只有一个线程能修改成功,修改失败的线程会通过CAS进行重试
2)AQS 的工作原理:假设一个线程来请求资源,这个资源是空闲的,那么请求资源成功的线程会被设定为工作线程,把资源的状态设定为锁定状态,这个时候如果再有线程来请求资源,那么就会请求失败,请求不到资源的线程会加入到同步队列中,当资源被释放之后,同步队列中的线程再次进行资源的争夺
豆包:
AQS 是 “抽象队列同步器”,是 Java 里很多并发工具(比如 ReentrantLock、CountDownLatch)的底层骨架。它核心是一个状态变量(state)和一个等待队列。线程抢锁时,先看 state 是不是 0(没人用),是就拿走(state 设为 1);不是就进等待队列排队,挂起。释放锁时,把 state 改回去,再叫醒队列里的线程来抢。它就像个 “排队管理器”,帮各种并发工具实现了加锁、解锁、排队的基本逻辑,上层工具只需要关心自己的 state 怎么用(比如 ReentrantLock 用 state 记录重入次数)。
-
并发工具类
1)CountDownLatch、CyclicBarrier、Semaphore 可以认为是一种控制并发流程的工具类
2)Exchanger 可以认为是线程间交换数据的工具类 -
CountDownLatch
1)CountDownLatch 的核心思想是通过计数器来控制线程的执行顺序,当计数器的值降为0时,所有等待的线程都会被唤醒,然后开始执行下一步操作
2)CountDownLatch 的使用:test219_thread/demo9/CountDownLatchDemo
3)CountDownLatch 的执行过程
· 在主调用线程中创建 CountDownLatch,并传入 count,count 通常为工作线程数量
· 在工作线程中调用 countDown() 方法,每调用一次 count 就减1
· 在主调用线程中调用 await() 方法来阻塞主调用线程
· count减到为0时,主调用线程被唤醒
4)CountDownLatch 的底层实现:基于 AQS 实现 -
CyclicBarrier
1)CyclicBarrer 的作用是让一组线程达到一个屏障(同步点)时被阻塞,直到所有的线程到达此屏障时,才会唤醒被屏障阻塞的所有线程
2)CyclicBarrer 的使用:test219_thread/demo9/CyclicBarrierDemo、CyclicBarrierDemo2、CyclicBarrierDemo3、CyclicBarrierDemo4
3)CyclicBarrer 的执行过程
· 在主调用线程中创建 CyclicBarrer,并传入 parties,parties 通常为工作线程数量
· 在工作线程中调用 await() 方法,每调用一次,就说明有一个线程抵达屏障,直到有 parties 个线程抵达屏障后,唤醒被屏障阻塞的所有线程
4)CyclicBarrier 的底层实现:基于 AQS 实现 -
Semaphore
1)信号量是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源
2)Semaphore 的使用:test219_thread/demo9/SemaphoreDemo
3)Semaphore 的工作流程
· 在主调用线程中创建 Semaphore,并传入 permits,permits 为许可证数量
· 在工作线程中调用 acquire() 方法,每调用一次,许可证数量就减1
,当许可证数量减到为0时,再调用 acquire() 会被阻塞,直到已经获得许可证的工作线程调用 release() 方法归还许可证,然后被阻塞的线程会获得许可证
4)Semaphore 的底层实现:基于 AQS 实现 -
Exchanger
Exchanger 的底层实现:使用 ThreadLocal 和 ArrayBlockingQueue 等数据结构来实现线程间的配对和数据交换 -
CountDownLatch 和 CyclicBarrier 的区别
· CountDownLatch 阻塞的是主线程,下一步动作的执行者是主线程,不可重复使用
· CyclicBarrier 阻塞的是其它线程,下一步动作的执行者是其它线程,可重复使用 -
原子类
1)原子类的出现背景:当一个线程更新一个变量时,程序如果没有正确的同步,那么这个变量对于其他线程来说是不可见的。我们通常使用 synchronized 或者 volatile 来保证线程安全的更新共享变量。在JDK1.5中,提供了 java.util.concurrent.atomic 包,这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式
2)原子类的应用案例:多个线程进行 i++ 操作,为了线程安全,需要加 synchronized 锁,锁是比较重的,因此可以考虑使用原子类AtomicInteger 来代替 synchronized
3)原子类的底层实现是基于 volatile 和 CAS,因此可以减少锁带来的性能开销