Java中线程的实现方式
-
继承 Thread 类,重写 run 方法,创建对象,调用 start 方法
-
实现 Runable 接口,实现 run 方法,通过 Thread 构造函数构造,调用 start 方法
-
实现 Callable 重写 call 方法,将其对象传入 FutureTask 对象中,然后传入 Thread 对象中,调用 start 方法
底层只有一种,实现 Runable 接口。
Java中线程的状态
-
NEW(新建)
-
RUNABLE(就绪/运行)
-
BLOCKED(阻塞,获取不到锁)
-
WAITING(等待唤醒)
-
TIMED_WAITING(超时自动唤醒)
-
TERMINATED(终止)
如何停止线程
-
通过 stop 函数,已弃用,不推荐使用
-
设置共享变量,子线程中判断,然后结束 run 方法
-
(推荐) 调用 interrupt 方法,中断标记 true/false,默认 false。
Java中 sleep 和 wait 方法区别
-
sleep 属于线程的静态方法, wait属于 Object 类的方法
-
sleep 属于 TIMED_WAITING ,自动被唤醒;wait 属于 WAITING,需要手动唤醒
-
sleep 在持有锁的时候执行,不会释放锁资源;wait 在执行后,会释放锁资源
-
sleep 有锁无锁都可以执行;wait 必须要在有锁的情况下执行
wait 方法会将持有锁的线程从 owner 放到 WaitSet 集合中,这个操作是在修改 ObjectMonitor 对象,如果没有持有 synchronized 锁,是无法操作 ObjectMonitor 对象的。
扩展点:ObjectMonitor、对象锁、锁的本质底层实现
Java 并发编程的三大特性
原子性:一个操作是不可分割的,不可中断的,一个线程执行时,另一个线程不会影响它。 - synchronized 拿到锁才能执行,线程之间互斥 - CAS 乐观锁,首先查看内存中的值是否和预期值一致,如果一致,则执行替换操作,这是一个原子性操作 - Lock 锁 lock()、unlock()
可见性:保证多线程获取到的都是最新的值。这是由于 JMM 影响,线程会操作数据都是在本地内存中,如果一个线程执行完后, - volatile 内存屏障,读写都是在主内存内,读的时候,告诉 cpu 不从缓存中获取数据,直接从主内存中获取值;写的时候,JMM 会将 cpu 缓存即使刷新到主内存中 - synchronized 执行那一刻保证可见性:在执行加锁的过程中,会将内部涉及到的变量从 CPU 内存中移除,必须从主内存中重新拿数据,释放锁之后,会将CPU缓存中的数据同步到主内存。 - lock CAS 加操作一个 volatile 修饰的变量,也是在执行的那一刻保证可见性。 - final 运行时期不可修改,则多线程来获取肯定是一样,并不是每次都要从主内存中获取。
有序性:编译时,在不影响最终结果的情况下,为提高效率,允许指令进行重排序。 - volatile 内存屏障,在指令之间添加一道指令,这个指令可以避免执行其他指令的重新排序,禁止重排序,保证多线程情况下,其他线程获取到的是完整的数据。
扩展点: - JMM 内存模型 - 如果对 volatile 修饰的属性进行写操作,会生成到 lock 前缀的指令,cpu 执行修改时,会将缓存立即同步到主内存中, 还会将其他线程中的本地内存这个数据设置为无效,必须重新从主内存中拉取。
什么是CAS,有什么特性?
compare and swap 他在替换内存中的某个值时,首先查看内存中的值是否和预期的值一直,如果一致,则执行替换操作,这个是原子性操作。 实现是Java中的Unsafe类,JVM会帮助我们将方法实现CAS汇编指令
-
不会带来线程的挂起和唤醒,带来用户态和内核态切换带来的性能消耗,缺点是,ABA问题,自旋时间过程(自旋次数限制),性能消耗严重
-
但是会产生 ABA 问题,可以通过版本号机制避免 ABA 问题。
@Content 注解的作用
cpu 缓存是以缓存行进行存储的,如果缓存行中某个数据发生变化,缓存行其他的数据都需要去主内存同步, 加上这个注解,会将缓存行的空闲位置填充没有意义的数据,避免同步数据消耗的时间。
Java 中的四种引用类型
-
强引用,一般程序创建对象,然后复制给一个引用变量,那这个引用变量就是一个强引用。如果一个对象被强引用变量引用时,始终处于可达状态,那么不会被垃圾回收机制回收的,即使这个对象以后都没有被使用到,造成OOM的主要原因之一
-
软引用,一般通过 SoftRefference,系统内存不足时,会被回收,一般作为缓存,比如处理文件资源,可以先全部缓存,进行处理
-
弱引用,weakRefference, 生命周期更短,垃圾回收机制一运行,不管内存是否足够,都会回收。
-
虚应用,主要作用用户跟踪对象被垃圾回收的状态,开发中一般用不到。
ThreadLocal 是什么以及内存泄漏问题
每个 Thread 中都有一个成员变量 ThreadLocalMap,ThreadLocal 本身不存储数据,基于 ThreadLocal 去操作 ThreadLocalMap ThreadLocalMap是基于 Entry[] 实现的,一个线程可以绑定多个 ThreadLocal ,可能需要存储多个数据,所以采用 Entry[] 的形式实现 每个线程都有独立的 ThreadLocalMap,ThreadLocal 对象作为 Key,对 value 进行存取。 ThreadLocalMap 中 key 是弱引用,弱引用的特点是,gc时,会进行回收,设计成弱引用是当ThreadLocal对象失去强引用后,这个ThreadLocal对象可以被回收,这是设计层面的考虑,可以避免OOM
ThreadLocal 内存泄漏问题: 如果 ThreadLocal 对象应用丢失,key 因为是弱引用,会被GC回收,但是如果线程没有被回收,内存中的 value 无法被回收,也无法被获取到,因此 value 会导致内存泄漏 解决办法是在使用完 ThreadLocal 对象后,调用remove方法,移除Entry即可
使用场景: - spring 事务管理,保证所有的事务都使用同一个数据库连接,就是通过 ThreadLocal 方式
Java 中锁的分类
重入锁和不可重入锁: 重入锁:synchronized ReentrantLock 当一个线程获取锁后,可以在当前线程再次获取这个锁,反之则是不可重入锁(线程池中的worker就是不可重入锁)
乐观锁和悲观锁 synchromized、ReentrantLoc k都是悲观锁,当线程获取不到锁时,会将当前线程挂起,挂起会涉及到内核态和用户态的切换,是比较耗资源的。 用户态:JVM 可以自行执行的指令,不需要借助操作系统执行 内核态:JVM 不可以执行的指令,需要借助操作系统执行 乐观锁: CAS 操作就是乐观锁的一种实现,当获取不到锁资源,可以让 cpu 再次调度,尝试获取锁资源 Atomic 原子类中,就是基于 CAS 乐观锁实现的
公平锁和非公平锁: synchronized只能是非公平锁 Lock可以实现公平锁和非公平锁 公平锁:当线程获取锁资源,需要排队获取。 非公平锁:A拿到锁资源,B也在排队获取,C来获取锁时,会先尝试,如果获取到锁,那么插队成功,如果没有依旧要排队到B后面,等B拿到或者取消后,才可以尝试获取锁。
互斥锁和共享锁: 互斥锁:同一时间点,只有一个线程拥有互斥锁,Synchronized、ReentrantLock 都是互斥锁 共享锁:同一时间点,当前共享锁可以被多个线程持有 ReentrantReadWriteLock 有互斥锁也有共享锁
synchronized 在1.6的优化
锁消除,如果修饰的代码中不存在临界资源,加或不加没有区别,会触发锁消除,即便用了,也不会触发。
锁膨胀,如果在一个循环中频繁的获取和释放资源,消耗很大,锁膨胀就是将锁的范围扩大,避免频繁的竞争带来不必要的消耗。
锁升级,在1.6之前,如果获取不到锁就直接挂起线程,性能较差,1.6之后,做了几个分别 - 无锁、匿名偏向:当前对象没有作为锁的存在 - 偏向锁:如果当前资源只有一个线程在频繁的获取和释放,那么当这个线程过来只需要判断是否是当前线程,如果是,那么直接获取,如果不是,基于CAS方式,尝试将偏向锁指向当前线程,如果获取不到,会进行锁升级,升级为轻量级锁 - 轻量级锁:会采用自旋锁的方式去频繁的以CAS形式获取锁资源,如果自旋一定次数,获取不到,则进行锁升级 - 重量级锁:传统的锁方式,拿不到资源,就挂起当前线程,涉及到用户态和内核态的切换。
synchronized 实现原理
-
多线程情况下,访问共享资源会出现线程安全问题,Synchronized就是用来保证线程安全的
-
JMM java memory management, 主内存以及线程本地内存(Java内存模型的抽象概念)
-
使用Synchronized 时,会将本地内存中的共享变量删除,需要从主内存中获取
-
Synchronized 可以保证原子性、可见性、有序性
-
可实现悲观锁、非公平锁、排他锁、可重入锁
-
修饰普通同步方法、修改静态同步方法、修改同步代码块
-
锁基于对象实现的,对象在堆内存中存储的
-
主要包含对象头、实例数据、对象填充
-
锁信息主要在对象头,对象头里有个 MarkWord,标记中四种锁状态,无锁、偏向锁、轻量级锁、重量级锁
-
不同状态的锁存储了不同的信息
-
重量级锁信息存储在 ObjectMonitor 中,其中 _owner(持有锁的线程)、_WaitSet(等待的线程)和 _EntryList(阻塞的线程)字段比较重要
当多个线程访问同步代码块,首先会进入的 EntryList 中,通过 CAS 方式尝试将 Monitor 中的 owner 字段设置为当前线程,同时 count+1,如果 owner 就是当前线程,则重入次数加 1, 当获取的锁的线程调用 wait 方法,则将 owner 设置为 null, count 减1,重入减1,当前线程加入 waitset 中,等待唤醒 当线程执行完同步代码块,释放锁,count 减 1, 重入减 1。
Java 虚拟机是通过进入和退出Monitor对象来实现代码块同步和方法同步的,代码块同步使用 monitorenter 和 moniterexit 指令实现的,而方法同步是通过 Access flags 后面的标识来确定改方法是否为同步方法。
什么是AQS
AbstractQueuedSynchronizer 抽象类,是 JUC 包下的一个基类,很多类都是基于 AQS 实现了部分功能
-
AQS 提供一个有 volatile 修饰并采用 CAS 方式修改的 int 类型的 state 变量
-
AQS 中维护了一个双向链表,有 head,有 tail,每个节点都是 node 对象,将需要获取锁的对象存放在双向链表中
AQS 唤醒节点,为何从后往前找
和插入有关,当节点插入时,上一个指针指向上一个节点,当上一个节点的下一个指针还未指向当前节点,存在错过风险。 取消一个节点也类似,也先操作 prev 节点。
synchronized 和 ReentrantLock 的区别
从使用层面和原理层面进行说明 - synchronized 是 Java 关键字,是在 JVM 层面实现的互斥锁的方式,不需要手动加解锁,由 JVM 实现;ReentrantLock 是类,加解锁需要调用 lock unlock 方法。 - synchronized 只支持非公平锁,ReentrantLock 支持公平锁和非公平锁。 - synchronized 是基于 ObjectMonitor 实现的;ReentranLock 是基于 AQS 实现的。 - synchronized 发生异常会自动释放锁,不会造成死锁问题;ReentrantLock 需要手动释放,否则会导致死锁。 - synchronized 适用于少了同步代码块场景;ReentrantLock 适用于大量同步代码块场景。 - synchronized 不能判断锁状态;ReentrantLock 可以。 - synchronized 不可中断,需要线程执行完;ReentrantLock 可以中断。
ReentrantReadWriteLock 实现原理
还是基于 AQS 实现的,还是对 state 进行操作,拿到锁就执行,如果没拿到,就去 AQS 队列中排队
读操作,基于 state 的高 16 位进行操作 写操作,基于 state 的低 16 位进行操作
写重入:基本和 ReentrantLock 一致,持有锁资源的线程,是当前线程,state 低位加 1 即可。 读重入:读锁时共享锁,除了对高位 state 加 1,还需要在各个线程中,通过 ThreadLocal 记录重入次数
写锁饥饿问题:读锁时共享锁,如果大量读锁来读取资源,绕过写锁,那么会操作写锁无法获取到资源,所以读锁在获取锁之前,需要判断当前资源是否有写锁等待,如果有,则排在之后。
JDK 提供了几种线程池
5种: newFixedThreadPool 核心线程数和最大线程数一样,是固定值 newSingleThreadExecutor 核心线程数只有一个,任务队列是有序的,时候顺序执行的任务 newCachedThreadPool 没有核心线程数,最大线程数是Integer的最大值,当任务不断增加时,会自动创建线程执行 SynchronizedQueue,最大特点是,只有有任务,就会有线程执行 newScheduleThreadPool 可以以一定周期去执行一个任务,或者延迟多久执行一个任务一次,原理是用的DelayQueue实现延迟执行,周期任务是执行完成后,再次扔回阻塞队列。 newWorkStealingPool 基于ForkJoin实现,每个线程都有自己的阻塞队列,可以任务拆分,然后合并。
线程池的核心参数有什么?
核心线程数、最大线程数、非核心线程数在无任务后存活时间、时间单位、阻塞队列,ThreadFactory构建线程的一些信息,拒绝策略
拒绝策略: 1. 抛出异常 AbortPolicy 2. 自行执行 CallerRunsPolicy 3. 丢弃 DiscardPolicy 4. DiscardOldestPolicy 5. 自定义策略,实现 RejectedExecutionHandler
线程池的状态
有一个 int 变量,前 3 位用于记录线程池的状态,有五种,分别是 RUNNING(构建后状态)、SHUTDOWN(调用shutdown方法,不会接收新任务,当所有任务执行完后,进入下一个状态)、 STOP(调用shutdownNow方法,不会接收新任务,队列中的任务也不会执行,每个现在也会执行中断方法)、tindying(线程关闭的过渡状态)、teiminated
20 线程池的执行流程
任务加入线程池,先判断任务是否是空,为空抛出异常;接着判断核心线程是否可以添加,没有则去创建(创建的时候也存在失败) 成功了,则继续执行,失败了以及核心线程创建完了,就判断线程池状态是否正常,处于Running,就加入阻塞队列中,在加入队列中的时候也可能 发生失败,如果成功了,则在判断线程池状态是否处于 Running 中,如果没有,则执行拒绝策略,然后结束,如果正常,判断工作线程是否是 0 个,不是 0 个,则直接执行,如果是 0 个 则创建非核心线程处理(可以设置核心线程数是0)。在加入队列失败后,判断是否能添加非核心线程数,添加失败,走拒绝策略,然后结束,添加成功则执行。
线程池添加工作线程的流程
addWorker 1. 校验线程池状态以及工作线程个数 2. 创建工作线程,然后启动线程
检查线程池状态,再判断工作线程个数,包括核心以及非核心线程,通过 CAS 进行 ctl 低 29 位进行 +1,接着创建 Woker 对象,里面包含一个 Thread,接着将 work 加入线程集合中也即一个 HashSet, 然后调用 start方法执行任务。
线程池为何要构建空任务的非核心线程?
当阻塞队列中有任务,但是没有工作线程为 0,会导致任务饥饿,导致的原因有 2 个,创建核心线程的时候,可以设置成 0,第二个是可以设置属性运行核心线程数空闲多久后销毁。
线程池使用完后为何必须 shutdown ?
创建核心线程,创建一个 worker,内部是一个 Thread,属于 JVM 层面,是一个 GCRoot 节点,如果这个线程无法回收,那么 Worker 也无法回收,整个线程池也无法回收。 调用 shutdown,会改变线程池状态,没阻塞的现在在获取任务之前,其次也会终止所有空闲的线程。
线程池的核心参数如何设置
核心线程数、阻塞队列、拒绝策略设置
实际任务类型不确定,可能是cpu密集型,也可能是io密集型,还有混合型的, 还有各个部署的服务器不一样,执行时间也不一样,最好的方式是通过测试,将项目部署到测试环境或者沙箱环境,经过各种压测获得一个符合的参数。 但是每次修改项目都需要重新部署,成本太高,可通过线程池提供的查询、设置接口实现一个动态监控以及修改线程池方案, 也可通过开源的一些框架去做监控和修改 hippo4j
ConcurrentHashMap 优化点
-
存储结构 数组+链表+红黑树,链表转红黑树,链表长度大于 8,数组长度大于 64。
-
存储操作 CAS + Synchronized 进行加锁,数据在数组上不需要加锁,通过 CAS 获取,如果在链表上,对 node 进行 Synchronized,锁的粒度细很多。
-
扩容操作,协助扩容,加快操作
-
计数器 基于 LongAddr 数组,分段统计,汇总总数
ConcurrentHashMap 散列算法
-
hashcode 的高低位都能参与运算
-
保证最终的值不是负数
ConcurrentHashMap 初始化数组的流程
数组是懒加载的,只有在放值的时候,初始化数组,sizeCtl 控制数组初始化和扩容的变量,-1 正在初始化 0 表示没有初始化,大于0 当前数组扩容阈值,或者当前数组的初始化大小
while循环加DCL(双重判断,中间加了一把锁,锁是CAS), 判断通过后,初始化数组
ConcurrentHashMap 扩容流程
触发方式:链表转红黑树,数组长度小于64, putAll 数组不够时,达到数据阈值时。 计算扩容标识戳,计算每个线程迁移的长度,初始化新数组,线程领取迁移任务, 老数据迁移到新数组,迁移结束后,判断是否是最后一个线程迁移完成, 如果是最后一个,还要再检查一次有没有遗漏的数据。
ConcurrentHashMap 读取数据的流程?
get 不会加锁,将 key 进行 hashcode,获取到数组的索引位置,如果数据在数组上,直接返回, 如果不在,则继续查询链表或者红黑树,如果是查询链表,通过 next 进行查询, 如果数据有迁移,则在新 table 中查询,如果在红黑树中查询,如果没有写操作,那么会以 Ologn 进行查找, 如果有写操作,那么会查询原来的双向列表。
ConcurrentHashMap 计数器的实现
记录元素个数,为避免并发过高,计数效率太低,仿照 LongAddr,有一个 baseCount, 以及一个 CounterCell 数组,在并发量的情况下,向 CounterCell 中追加个数,当线程通过 size 方法统计元素个数的时候,将 baseCount 以及 CounterCell 数组中的value进行累加