JDK并发包
同步控制
重入锁(ReentrantLock)
RenntrantLock通过lock()和unlock()来手动加锁, 所以在灵活性上要比synchronized好很多, 但是必须要记得释放锁, 不然会导致其他线程没有机会访问临界区, 另外, ReentrantLock的锁可以重复加锁, 但是必须也要释放同样多的锁.
另外重入锁还提供以下多个功能
- 中断响应: 可以手动中断线程
- 锁申请等待限时: 通过tryLock()来限时重试
- 公平锁
以下可以通过如下函数实现:
lock(): 获得锁, 如果已经被占用, 则等待.
lockInterruptibly(): 获得锁, 但优先响应中断.
tryLock(): 尝试获得锁, 立即返回不等待, 成功: true, 错误: false;
tryLock(long time, TimeUnit unit): 规定时间内重试
unlock(): 释放锁.
重入锁的实现原理
- 原子状态: 重入锁采用CAS操作来存储当前锁的状态.
- 等待队列: 所有没有请求到锁的线程, 会进入等待队列进行等待.
- 阻塞原语: park()和unpark()用于挂起和恢复线程.
信号量(Semaphore)
无论是内部锁synchronized还是ReentrantLock, 一次都只允许一个线程访问一个资源, 而信号量却可以指定多个线程, 同时访问某个资源
有如下两个构造参数:
public Semaphore(int permits)
public Semaphore(int permits, boolean fair) // 第二个参数指定是否公平
主要函数有如下:
public void acquire() // 尝试获得准入许可, 如无法获得, 那么就会等待, 知道有线程释放一个许可或, 当前线程被中断.
public void acquireUniterruptibly() // 和acquire类似, 但是不响应中断.
public boolean tryAcquire() // 尝试获得许可, 成功返回true, 否则返回false.
public boolean tryAcquire(long timeout, TimeUnit unit)
public void release() // 释放一个许可.
ReadWriteLock读写锁
- 读-读不互斥: 读读之间不阻塞.
- 读-写互斥: 读阻塞写, 写不会阻塞读.
- 写-写互斥: 写写互斥.
在读操作远大于写操作的系统中, 读写锁可以发挥最大功效, 提升性能.
倒计时器: CountDownLatch
主线程await(), 多线程每触发一次coutDown(), 倒计时器就减一, 当其变为0时, 主线程才会继续执行.
循环栅栏: CyclicBarrier
作用和CountDownLatch类似, 但是更加复杂, 假如将计数器设置为10, 那么凑齐第一批10个线程后, 计数器会归零, 然后接着凑齐下一批10个线程.(具体原理见书P90)
线程阻塞工具类: LockSupport
略
线程复用: 线程池
每次创建一个线程, 在run()方法结束后, 如果直接回收线程, 会出现创建和销毁线程所用时间大于线程执行消耗的时间, 而且大量的线程也会导致OOM, 就算没有也会给GC带来很大压力, 延长GC停顿时间.
Java提供了一套Executor框架, 本质就是一个线程池, 其中ThreadPoolExecutor表示一个线程池, Executors类扮演了线程池工厂的角色, 通过Executors可以取得一个特定功能的线程池. 其中提供了五种线程池:
- newFixedThreadPool(int nThread): 返回一个固定线程数量的线程池, 如果无空闲线程, 那么新的任务就会被放入任务队列中, 待空闲时执行.
- newSingleThreadExecutor(): 返回一个只有一个线程的线程池, 和上一个一样, 有多的直接放入任务队列, 空闲时按先入先出的顺序执行.
- newCachedThreadPool(): 返回一个可以根据实际情况调整数量的线程池, 数量不确定, 若有空闲线程可以复用, 优先使用可复用的线程, 若所有线程均在工作, 又有新任务提交, 那么会创建新线程处理, 所有线程在当前任务执行完后, 返回线程池复用.
- newSingleThreadScheduledExecutor(): 返回一个ScheduledExecutorService对象, 线程池大小为1, ScheduledExecutorService接口在ExecutorService接口之上扩展了在给定时间执行某任务的功能, 如固定延迟之后执行, 或周期执行.
- newScheduledThreadPool(int corePoolSize): 也返回一个ScheduledExecutorService对象, 但可以指定其中线程数量.
线程池内部实现:
五种线程池, 其内部其实都使用了ThreadPoolExecutor实现, 其构造函数如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
其中函数参数含义如下:
- corePoolSize: 线程池中的线程数量.
- maximumPoolSize: 线程池中最大线程数量.
- keepAliveTime: 当线程池线程数量超过corePoolSize时, 多余空闲线程存活时间.即超过CorePoolSize的空闲线程多久被销毁.
- unit: keepAliveTime的单位.
- workQueue: 任务队列, 被提交但尚未被执行的任务.
- threadFactory: 线程工厂, 用于创建线程, 一般默认即可.
- handler: 拒绝策略, 当任务太多时, 如何拒绝.
比如以下例子:
[图片上传失败...(image-c3a06e-1554900086425)]
以上参数当中, 有两个需要注意: workQueue和handler
参数workQueue指被提交但未执行的任务队列, 是一个BlockingQueue接口对象, 仅用于存放Runnable对象, 根据功能分类, 有如下几种BlockingQueue.
直接提交的队列(SynchronousQueue): 是一个特殊的BlockingQueue, 没有容量, 每个插入操作都要等待一个删除操作, 每个删除操作也要等待一个插入操作, 提交的任务不会被保存, 而是直接交给线程执行, 如果没有空闲线程, 那么直接创建新的线程, 所以通常要设置很大的maximumPoolSize值.
有界的任务队列(ArrayBlockingQueue): 使用时必须带一个容量参数表示队列的最大容量, 当线程池实际线程数量小于corePoolSize, 会优先创建新的线程, 若大于corePoolSize, 那么会加入等待队列, 假如等待队列也满了, 且不大于maximumPoolSize, 才会创建新的线程, 大于maximumPoolSize会直接执行拒绝策略, 所以只有任务队列满的时候, 线程数才有可能超过corePoolSize, 换言之, 除非系统特别繁忙, 不然一般线程数都维持在corePoolSize.
无界的任务队列(LinkedBlockingQueue):与有界队列不一样的是, 无界不是出现入队失败的情况, 当线程数小于corePoolSize的时候, 会生成新线程执行, 当达到corePoolSize的时候, 就不会增加, 直接进入队列等待, 当任务创建和处理速度差太多的时候, 队列会大量阻塞, 一直增长, 直到耗尽内存.
-
优先任务队列(PriorityBlockingQueue): 带优先级的队列, 是一种特殊的无界队列, 确保系统性能的同时, 按照优先级来执行.
[图片上传失败...(image-d8bf73-1554900086426)]
[图片上传失败...(image-325ef6-1554900086426)]
handler: 拒绝策略
JDK内置了4种策略
- AbortPolicy策略: 直接抛出异常, 阻止系统正常工作.
- CallerRunsPolicy策略: 只要线程池没关闭, 会直接在调用者线程执行丢弃的任务, 但是这样会让任务提交线程的性能下降.
- DiscardOledersPolicy: 丢弃最老的一个请求, 也就是即将被执行的请求, 然后尝试再次提交当前任务.
- DiscardPolicy: 直接丢弃无法处理的任务.