这里是目录
线程和进程的区别
- 进程是一个程序执行一次创建的,是系统运行程序的基本单位。
- 线程是⼀个⽐进程更⼩的执⾏单位,一个进程在运行期间可以产生多个线程。多个线程可以共享进程的堆和⽅法区资源,每个线程有⾃⼰的程序计数器、虚拟机栈和本地⽅法栈。
- 线程是进程划分成的更⼩的运⾏单位。
- 线程和进程最⼤的不同在于基本上各进程是独⽴的,⽽各线程则可以有共享的资源也有自己独有的资源。
- 线程执⾏开销⼩,但不利于资源的管理和保护;⽽进程正相反
多线程的几个对象
- 任务对象
- Runable
- 核心方法run方法
- 需要通过executor的execute方法执行
- Callable:
- 核心方法 call方法
- 需要通过executor的submit方法执行
- Runable
- 执行对象
- executor:顶层接口,包含execute方法
- ExecutorService:继承executor接口,包含submit和shutdown方法
- 结果
- Future:线程返回结果的顶层接口,包含get方法
- FutureTask是Future的实现类
线程池
-
ThreadPoolExecutor
- 核心参数
- 核心线程数
- 最大线程数
- 等待队列最大长度
- Executors工具类实现的线程池
- FixedThreadPool:固定线程数,使用无界等待队列,大量任务堆积等待队列中会出现OOM
- SingleThreadExecutor:线程池只有一个线程,使用无界等待队列,大量任务堆积等待队列中会出现OOM
- CachedThreadPool:使用的同步队列,不存储等待任务,只要任务进来,就创建线程执行任务,且允许创建的线程数量为 Integer.MAX_VALUE,会出现OOM
- ScheduledThreadPool:使用的无界的延迟阻塞队列,内容使用的最小堆排列,同样大量任务堆积等待队列中会出现OOM
- 线程池的拒绝策略
- 抛出异常之后拒绝任务
- 将任务回退给调用者,使用调用者的线程来执行任务
- 直接丢弃掉
- 丢弃等待队列中的最早进来的未处理的任务请求
- 线程池中任务进来的判断流程
- 可以看出是先判断等待队列是否可以放入,才判断是否是小于最大线程数,然后创建线程
- 核心参数
-
线程池创建核心参数设置
- 计算密集型
- 可以将核心线程数设置为 N(CPU 核心数)+1
- 最大线程数不适合过大,根据实际情况调整
- IO密集型
- 设置核心最大进程数为2N
- 设置最大线程数为4N
- 计算密集型
-
线程池的参数调整过程
- 首先根据上面类型设置理论数
- 根据实际情况进行压测
- 根据压测结果进行调整
- 循环步骤2和3,直到得到最佳效果
-
线程池尽量不要放耗时任务
- 线程池本身的目的是为了提高任务执行效率,避免因频繁创建和销毁线程而带来的性能开销。如果将耗时任务提交到线程池中执行,可能会导致线程池中的线程被长时间占用,无法及时响应其他任务,甚至会导致线程池崩溃或者程序假死。
线程间的同步的方式
- 互斥:
- 只有拿到互斥对象的线程才能访问资源
- synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源
- 信号量:
- 允许多个线程访问资源,当时线程数必须在信号量允许的范围内
- Semaphore(信号量)可以用来控制同时访问特定资源的线程数量
- 事件:通过事件通知的方式实现线程同步。
AQS的关系
- AQS 是抽象队列同步器(AbstractQueuedSynchronizer),是在java.util.concurrent.locks包下
- AbstractQueuedSynchronizer这个类是一个抽象类,主要是被子类继承可以很方便实现构造出同步器(ReentrantLock 互斥同步和Semaphore 共享同步)
- 原理:当一个线程请求资源时,通过CAS操作,判断是否允许线程持有资源,如果CAS操作失败,则将当前线程封装为一个node节点,加入一个双向等待队列的尾部(遵循FIFO)。
CAS和AQS的关系
- CAS(Compare-And-Swap)是一种无锁算法,通过值比较来保证操作的原子性。
- AQS是一个用来构建线程同步的框架,例如,ReentrantLock和Semaphore等同步器都是基于AQS实现的
- AQS利用CAS来保证其内部状态(state)的原子性更新。
ThreadLocal
ThreadLocal基本方法
- 使用ThreadLocal目的:保证线程所修改的属性不被其他线程修改
- ThreadLocal的方法get的实质是获取当前线程内部的ThreadLocalMap的get方法,key是ThreadLocal,通过hashcode方法计算出ThreadLocal对应的索引下标,然后获取ThreadLocalMap中对应的内容
- ThreadLocal的方法set的实质是对当前线程内部的ThreadLocalMap进行set,首先判断线程的ThreadLocalMap是否存在,不存在则创建一个并复制到线程内部。set过程中会创建一个ThreadLocal的弱引用作为key进行设置。
ThreadLocal中的软引用引发的问题
- ThreadLocalMap是Map结构,自然也会发生Hash冲突,HashMap通过拉链发解决的。ThreadLocalMap则是通过开放地址法解决冲突,在计算出的Hash下标的数组中发现已经存在entry,则会向后移动下标,继续找可以存放数据的地方。
- ThreadLocalMap中每一个key、value都是通过entry存放的,但是由于entry中的key是ThreadLocal的弱引用,所以当ThreadLocal的强引用被赋值null,在下次GC的过程中就会导致ThreadLocal对象被回收,导致entry中key为null,entry也就会被认为是过期数据。
- 为什么key要使用ThreadLocal的弱引用,而不是直接使用强引用?如果在entry内部ThreadLocal是强引用,在ThreadLocal threadLocal = null;时,相当于ThreadLocal对应的对象已经不能再使用了,但是ThreadLocalMap中取值需要通过ThreadLocal作为key才能get到内容,相当于Map中的数据永远无法获取到,会导致内存泄漏。
- 而使用ThreadLocal的弱引用,在ThreadLocal强引用被删除,而entry中的key被gc之后就会变为null,通过这个特征就可以在代码中进行回收了。
- 如何回收,在ThreadLocalMap的set,get方法中都有主动清楚存在entry但是key为null得过期数据的逻辑。
- 如果一直不调用get,set方法,存过期的entry在就会造成内存泄漏。所以在不适用ThreadLocal时,通过remove主动删除过期数据。
几个锁的对应和理解
- 公平锁和非公平锁:等待锁的过程中是否有插队的
- 悲观锁和乐观锁:操作之前是否要上锁
- 共享锁和互斥锁:一个资源是否可以被多个线程持有
ReentrantLock 的概念
- ReentrantLock是java.util.concurrent.locks包下的,通过AQS实现的悲观锁
- 原理:ReentrantLock内部有一个抽象类Sync,这个类继承AbstractQueuedSynchronizer(AQS),同时在ReentrantLock内部对Sync有两种实现,公平锁和非公平锁。生成ReentrantLock对象的过程其实就是构建Sync实例对象的过程。同时上锁也是通过Sync实例对象的lock方法实现的。(这块建议看下源码,很好理解)
- ReentrantLock 的特性是可重入,这块跟是否是公平锁没关系,在获取锁的时候,会判断持有当前资源的线程是不是当前线程,如果是的话,可以直接获取锁
- 非公平锁,一个线程进来直接会尝试一下CAS,不成功则进入等待队列。成功则会在等待队列前占有资源(直接越过了排队过程,对等待队列中的其他线程是不公平的)
- 公平锁,在实例化ReentrantLock 对象的时候,通过传入布尔值,来创建公平锁。线程进来,如果有等待的线程,则先进等待队列排队,依次获取资源。