基本概念
- 同步与异步
- 同步方法等待返回,异步方法立即返回
- 并发和并行
- 并行是两个任务同时进行,并发是两个任务切换执行
- 并发是交替的执行
- 并行是多个任务真实的同时执行
- 临界区
- 公共资源或者共享数据,可以被多个线程使用。但每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程想要使用这个资源就必须等待
- 阻塞和非阻塞
- 如果一个线程占用了临界区资源,其他的线程就得在这个临界区中等待,这就是阻塞;非阻塞强调没有一个线程可以妨碍其他线程执行
- 死锁、饥饿、活锁
- 死锁:两个及两个以上的进程互相等待,【哲学家进餐】;总是按照一个全局的固定顺序获取锁,可避免死锁
- 活锁:多个线程谦让导致无法使用资源
- 饥饿:某一个或多个线程一直占用资源,使其他需要资源的线程无法执行
- 并发级别
- 阻塞: 当一个线程进入临界区之后,其他线程必须等待
- 无饥饿:非公平锁会导致低优先级的线程饥饿,而公平锁饥饿就不会产生
- 无障碍:自由出入临界区,无竞争时有限步内完成操作,有竞争时回滚数据
- 无锁:无障碍,保证有一个线程可以胜出
- 无等待:无锁,要求所有线程都必须在有限步内完成,无饥饿
- JMM的特性
- Atomicity: 一个操作不可中断
- Visibility: 一个线程修改了某一个变量值,其他线程能否立即获取到这个改动值
- Ordering: 对于一个线程来说,它内部的执行顺序一定是一致的,多个线程之间涉及到指令重排,所以顺序是没有保障的
- Happen-before规则:指令重排的原则:
- 程序顺序原则:一个线程内保证语义的串行性
- volatile规则:volatile变量的写先发生于读,保证了volatile变量的可见性
- 锁规则:解锁必然发生在加锁前
- 传递性:A先于,B先于C,那么A一定先于C
- 线程的
start()
方法先于它的每一个动作 - 线程的所有操作先于线程的终结
- 线程的中断先于被中断线程的代码
- 对象的构造函数执行、结束先于
finalize()
方法
- synchronized的用法:
- 指定加锁对象
- 作用于实例方法
- 作用于静态方法
多线程-同步控制
- 重入锁
java.util.concurrent.locks.ReentrantLock
,又名递归锁
- 中断响应,
lock.lockInterruptibly()
优先响应中断 - 锁申请等待时限,
lock.tryLock(); lock.tryLock(long time, TimeUnit tu)
给定时间获取锁,如果未指定时间则立即返回 - 公平锁,
new ReentrantLock(boolean fair)
- 中断响应,
- Condition:与lock搭配使用,使线程在合适的时间等待,或者在一个合适的时间得到通知继续执行
await()
会使当前线程等待,并释放当前锁,当其他线程使用signal()
或者signalAll()
方法时线程或重新得到锁继续执行,或者等待的时候被中断时跳出等待awaitUninterruptibly()
与await()
类似,但不会在等待中响应中断signal()
会唤醒一个等待的线程,signalAll()
会唤醒所有等待的线程- 重入锁+condition在jdk内部广泛使用,如
ArrayBlockingQueue
的put() take()
方法
- 信号量
Semaphore
:指定多个线程访问一个资源,通过acquire() tryAcqire()
获取授权,使用完毕后通过release()
释放资源授权;
使用场景
:数据库连接数固定,可以采用信号两个让数据库连接得以充分使用
ReadWriteLock
,读写分离锁可有效减少锁竞争,以提升系统性能;读写锁约束:
- 读-读:不阻塞
- 读-写、写-写:阻塞
StampedLock
:对读写锁的改进,不仅读不阻塞读,读也不阻塞写;在读的时候发生了写,则应当重读而不是阻塞写(读写锁的时候读多写少会发生写饥饿);
StampedLock
控制锁带有三种模式读
写
乐观读
- 此控制锁的状态由版本和模式组成,锁获取操作返回一个用于展示和访问锁状态的stamp变量,锁释放以及其他相关方法使用stamp变量作为参数,如果与当前锁状态不符则失败
- 三种模式之
写入
:writeLock
独占访问阻塞当前线程返回一个stamp变量,当锁被写模式占有,没有读和乐观读能成功 - 三种模式之
读取
:readLock
获取访问而阻塞写入,返回一个stamp变量 - 三种模式之
乐观读
:tryOptimisticRead
返回一个非0 stamp变量,仅在当前锁没有被写入模式持有;validate
用于检查乐观读时是否发生了写操作,如果发生则重读(悲观锁)
CountDownLatch
,await()
后等待线程计数完成可继续执行,与join
的区别:
- 假如每个线程的工作有两阶段
- 第一个线程需要等待第二、第三个线程的第一阶段工作结束时才能开始执行
- 此时join不能做到但
CountDownLatch
可以做到,count.countDown()
CyclicBarrier
: 循环屏障,当所有线程到达屏障(await()
)之后,被屏障阻塞的线程才会继续执行,并且可重复设置屏障; 场景:
- 数据合并:多个线程统计各自的数据,统计完毕后,使用
barrierAction
进行合并
- 数据合并:多个线程统计各自的数据,统计完毕后,使用
LockSupport
: 线程阻塞工具,可以在线程内任意位置阻塞,无需提前获取锁。
park()
如果许可可用,则消费许可后立即返回;如果许可不可用,则阻塞等待许可;阻塞时可被中断unpark()
使许可变为可用,许可不能累加只能有一个
多线程-线程池
在生产环境中线程的数量必须加以管控,否则大量线程轻则拖慢系统速度,重则导致CPU、内存资源耗尽
- JDK Executor框架
Executors
工厂类产生各种线程池
newFixedThreadPool()
返回一个固定线程数量的线程池newSingleThreadExecutor()
返回一个只有一个线程的线程池newCachedThreadPool()
返回一个可根据实际情况调整数量的线程池,优先复用newSingleThreadScheduledExecutor()
返回一个ScheduledExecutor
对象,线程池大小为1
newScheduledThreadPool()
返回指定数量ScheduledExecutor
对象的线程池
ScheduledExecutorService
根据时间需要对线程进行调度
schedule(Runnable cmd, long delay, TimeUnit unit)
给定时间的时候,调度一次scheduleAtFixedRate(Runnable cmd, long initialDelay, long period, TimeUnit unit)
固定频率进行调度,以上次任务开始时间为参照,但任务不会被堆叠,当执行周期过短时,上次任务结束立即会调用本次任务scheduleWithFixedDelay(Runnable cmd, long initialDelay, long delay, TimeUnit unit)
固定延迟进行调度,以上次任务结束时间为参照
ThreadPoolExecutor
线程池的内部实现:Executors
工厂类产生线程池的方法主要是调用:
new ThreadPoolExecutor(int coreSize, int maxPoolSize, long keepAlive, TimeUnit unit, BlockingQueue<Runnable> wordQueue, ThreadFactory factory, ThreadPoolExecutor handler)
- coreSize: 线程池中的线程数量
- maxPoolSize: 线程池中最大线程池数量
- keepAlive: 当线程池数量超过coreSize时,多余的空闲线程的存活时间
- workQueue: 任务队列,被提交但尚未执行的任务
- factory:线程工厂用于创建线程;
- handler:拒绝策略,当任务太多时如何拒绝任务
- 参数
workQueue
- 直接提交队列-
SynchronousQueue
,没有容量,每一个插入操作都需要等待一个相应的删除操作;提交的任务不会被真实保存,总将新任务提交给线程执行,如果没有空闲线程则创建新的线程,如果线程数已到最大值,则执行拒绝策略,如newCachedThreadPool
使用队列 - 有界任务队列-
ArrayBlockingQueue
若线程池中线程实际数量小于coreSize则创建新的线程,如果池中线程数等于coreSize则将任务加入队列,如果队里已满,则在coreSize不小于maxSize的前提下创建新的线程,否则执行拒绝策略, - 无界任务队列-
LinkedBlockingQueue
若池中线程数量等于coreSize则后续的任务都会被加入到队列中,直到资源耗尽否则任务不会被拒绝(失败),如newFixedThreadPool
- 优先任务队列-
PriorityBolckingQueue
带有优先级的队列,可控制任务执行的先后顺序,是一个特殊的无界队列
- 直接提交队列-
- 拒绝策略:当线程池中的线程不足处理现有任务的时候,会触发任务拒绝策略
- AbortPolicy:抛出异常,阻止系统工作
- CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者的线程中执行当前被拒绝的任务
- DiscardOledestPolicy:丢去最老的一个任务,并提交当前任务
- DiscardPolicy:丢弃当前任务
- 如果无法满足则可以自定策略,实现
RejectedExecutionHandler
- 自定义线程池的创建:实现
ThreadFactory
接口即可,在创建时可以给线程分组、设置优先级、设置名称、设置是否守护线程等 - jdk线程池扩展:
ThreadPoolExecutor
提供了beforeExecute()
和afterExecute()
以及terminated()
方法 - 线程池数量:一般考虑CPU数量、内存大小等,最优的线程池数量:
n = Ncpu * Ucpu * (1 + W/C)
- Ncpu:cpu的数量
- Ucpu:目标CPU的使用率
- W/C: 等待时间与计算时间的比率
- Fork/Join框架:分治思想,如
MapReduce
RecursiveTask<V>
有返回值,超出当前线程处理能力时,进行fork,fork结束后进行joinRecursiveAction
无返回值
参考资料:《Java高并发程序设计》