一、多线程
1. 无返回值
①. 实现runnable接口
- run方法不会抛异常
- 需要Thread的start方法启动多线程
②. 继承Thread类
- java只能单继承,所以扩展收到限制
2. 有返回值(实现Callable接口)
①. future拿 到返回值
- 拿返回值
- 判断任务是否执行完
- 中断任务
- 向线程池summit的多个任务,只有全部执行完,future才可以get到值
②. call方法会抛异常
③. 需要Thread的start方法启动多线程
3. 解决future的get方法阻塞问题completionService
①. take方法也是阻塞方法,只是会拿其中一个完成的future
②. poll方法和take类似,只是poll方法不会阻塞,没有完成的任务直接返回null,可以加等待的时间
4. ThreadLocal
①. 线程间的数据隔离
②. 解决多线程安全问题
- set(往里面放数据
- get(从里面取当前线程的数据
- 使用完get和set后要用remove去除内部map的key与value的引用关系,因为key是弱引用,下次gc的时候被回收,导致value会被线程长期持有,造成内存泄露
③. 优雅做法:帮ThreadLocal包装到单例中
二、并发包
- BlockingQueue
- ConcurrentHashMap
- ReentrantLock
- LockSupport
- CyclicBarrier
- CountDownL atch
- ReadWriteLock
- Semaphore
- Condition
三、相关问题理解
1. 对volatile的理解
①. 虚拟机提供的轻量的同步机制
- 保证可见性:前后加了内存屏障
- 不保证原子性
- 禁止指令重排:当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行
②. 在JMM的理解
- 可见性:主内存,线程工作内存
- 原子性
- 有序性质:指令重排
③. 哪些地方用过volatile
- 单例模式的double check (禁止 了指令重排)
- 在真正的new操作对象的时候,new命令其实包含了,开辟内存,初始化内存等操作:
- 多线程情况下,一个线程在new的时候,内存还没有被初始化完全,另一个线程进来后发现对象引用已经不是null了,就会返回一个未初始化完全的对象,从而造成出错
- 代理模式
2. CAS的理解(比较交换)
①. 工作内存与主存数据比较一样的时候修改,不一样的时候重复读取主存,跟新工作内存数据
②. CAS底层,unsafe的理解:native方法,unsafe类
③. CAS缺点:
A. 循环时间长,CPU开销大
B.只能保证一个共享变量的原子性:不能保证代码块的原子性
C. 有ABA问题:
- 原因:一个线程短时间内把变量由A改成B,再由B改成A
- 解决:加入时间戳版本,原子引用
- AtomicStampedReference
3. Arayit是线程不安全的,解决线程不安全的方言
- 加锁
- 使用vector线程安全的数组
- 使用Collections内部方法转为线程安全的List
4. 各种锁的理解
①. 公平锁/非公平锁
- 是什么:公平锁只等待锁的线程按先来先得顺序得到锁
- 两者区别:公平锁按FIFO的等待队列等待锁, 锁操作耗时
- ReentrantLock默认是非公平锁,synchronize是非公平锁
②. 对象锁
- Synchronize.每 个对象的头部markword字段有标志标志,monitorenter 和monitorexit 指令来实现同步的,monitor管程 来控制
- 头部mark word字段,有锁状态,是否偏向锁,锁标志
- JVM级的不需要业务代码控制
③. 偏向锁
- 偏向锁的核心思想是,如果一个线程获得 了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作
④. 可重入锁
- 是什么:同一个线程外层函数获取锁后,内存递归函数仍然能获得锁
- ReentrantLock和Snychronize是典型的可重入锁
- 底层实现原理,ReentrantLock是AQS同步队列, Snychronize是锁住的对象头部字段记录的标志,在monitor中 会做计数
⑤. 自旋锁
- SpinLock,短时间的自旋,不会释放CPU
⑥. 独占锁(写)/共享锁(读)
- ReentrantReadWriteLock
⑦. 读写锁
- 写的时候排他,读的时候共享排他写锁
⑧. 锁优化
- 偏向税→轻量级锁→自旋锁→重量级锁
5. CountDownLatch/CyclicBarrier/Semaphore
①. CountDownLatch
- 让一些线程阻塞,知道一些线程完成操作
- 一些线程执行await(阻塞,一些线程完成操作后执行CountDownQ减数
- 案例:等待人到齐,才开会
②. CyclicBarrier
- 一些线程阻塞在一个点, 然后同时进行
- await(方法阻塞
- 案例:跑步比赛到齐开始
③. Semaphore
- 用于并大资源数量的控制,同-时间只能有固定数的线程进入临界区
- 案例:抢车位
6. 阻塞队列
①. 是什么
- 阻塞队列空的时候,从队列获取数据的线程会被阻塞
- 阻塞队列满的时候,往队列放数据的线程会被阻塞
②. 好处
- 不需要关心什么时候阻塞线程,什么时候唤醒,阻塞队列帮处理了
③. Blockqueue核心方法处理出错
- 抛异常
- 返回特殊值
- 一直阻塞
- 超时退出
④. 架构种类
- ArrayBlockingQueue基于数组的有界
- LinkedBlockingDeque基于链表的有界,默认界值很大
- SynchronousQueue同步队列,不存数据,生产一个消费一 个
- PriorityBlockingQueue有优先级的无界阻塞队列
- delayQueue延迟无界阻塞队列
- LinkedTransferQueue链表结构的无界阻塞队列
- LinkedBlockingDeque链表结构阻塞双端队列
⑤. 用在哪里
- 生产者消费者模式
- 线程池
- 消息中间件
7. Java线程池,ThreadPoolExecutor的理解 田
①. 线程池优势
- 充分利用CPU
- 减少频繁创建线程的性能消耗
②. 线程池的使用
- 自定义线程池,继承ThreadPoolExecutor
- 线程池函数
- Executors.newCachedThreadPool
- Executors.newSingle ThreadPool
- Executors.newFixedThreadPool
- Executors.newScheduleThreadPool
③. 线程池的重要参数
- corePoolSize线程池的常驻核心线程数
- maximumpoolSize线程池能够容纳的最多线程数
- keepAliveTime多余核心线程数的空闲线程的最多存活时间
- unit keepAlive Time的单位
- workqueue任务队列,提交到线程池还未处理的任务
- threadFactory线程池中线程的工厂。用于创建线程,
- handler拒绝策略,任务丢列满,线程数达到最大时,做的处理
④. 线程池的工作原理
a. 创建线程池后等待提交任务
b. 调用execute添加任务,线程池做的判断:
- 如果线程数量小于核心线程数,就马上创建线程处理任务
- 如果正在运行的线程数大于核心线程数,就将任务放入任务队列
- 如果任务队列满了,且正在运行的线程数小于最大线程数,就创建非核心线程处理任务
- 如果队列满了,且线程数达到最大线程数,线程池使用饱和拒绝策略处理任务
c. 当一个线程任务处理完,会从任务队列中取下一个任务执行
d. 当一个线程空闲时间超过keepAliveTime时候,线程池处理(如果当前线程数量大于核心线程数,那么这个线程就会被销毁)
8. 线程池参数的合理配置
①. 线程池拒绝策略
- AbortPoliy直接抛出异ReiectedException.阻止系统运行
- CallerRunPoliy由提交任务的业务线程处理_
- DisadndeltPole她弃队列中等待最久的任务,然后把最新的任务加入人任务队列
- DicardPolil 直接丢弃任务,不予处理。也不抛出异常,允许任务丢弃
②. 创建线程池的方法
a. 使用自定义线程池
b. executors线程池问题:
- 默认使用链表有界阻塞队列,界很大,导致内存溢出
- 缓存调度线程池会创建大量线程导致资源占用大
③. 如何合理配置线程池数量
- CPU密集型操作: cpu核心数+1
- IO密集型操作: 2xcpu核心数
9. 死锁定位分析
①. 产生原因:
- 系统资源不足
- 资源分配不当
- 线程执行顺序不合适
- 线程互斥,资源持有等待,不可剥夺
②. 解决:jps定位到进程id, jstack定位到栈代码