线程池

Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处:

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,

还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。
线程池的四种实现:

  1. newCachedThreadPool(可缓存线程池):如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  2. newFixedThreadPool(定长线程池):可控制线程最大并发数,超出的线程会在队列中等待。
  3. newScheduledThreadPool(周期性定长线程池):支持定时及周期性任务执行。
  4. newSingleThreadExecutor(单线程化的线程池):它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

    主要参数

  5. corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能执行新任务也会创建线程。等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。

  6. runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列。
    1. ArrayBlockingQueue:是一个基于数组结构的有界队列,此队列按FIFO(先进先出)原则对元素进行排序。
    2. LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
    3. SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工程方法Executors.newCachedThreadPool使用了这个队列。
    4. PriorityBlockingQueue:一个具有优先级的无限阻塞队列
  7. maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果。
  8. ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。使用开源框架guava提供的ThreadFactoryBuilder可以快速给线程池里的线程设置有意义的名字。
  9. RejectExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。在JDK1.5中Java提供了以下4种策略:
    1. AbortPolicy:直接抛出异常。
    2. CallerRunsPolicy:只用调用者所在的线程来运行任务。
    3. DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
    4. DiscardPolicy:不处理,丢弃掉。

也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化存储不能处理的任务。

  1. keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。
  2. TimeUnit(线程活动保持时间的单位):可选单位有天、小时、分钟、毫秒、微秒、纳秒。

    执行过程

    并发编程 - 图1

    拒绝策略

    工作队列

    重量级锁(synchronized)

    synchronized关键字也称为重量级锁,原因是JVM虚拟机对多线程的实现依赖于操作系统的轻量级进程(轻量级进程内核线程是1比1的关系),也就是需要操作系统的内核态资源,需要进行内核态和用户态的切换,对多线程的调度实现依赖于操作系统的任务调度器。其锁标志是保存在对象头Mark Word中的1bit位。
    image.png
    因为synchronized是重量级锁,开销大,所以JVM对其进行了优化,也就出现了锁升级过程。
    new -> 偏向锁 -> 轻量级锁(无锁、自旋锁、自适应自旋)-> 重量级锁
    当对象new出来之后没有被上过锁则锁记录(1bit)为0,此时任何线程可直接获取此对象的锁。
    当对象第一次被上锁时会使用偏向锁,锁记录为0,且将线程ID保存到对象头中(23bit)。
    当对象锁被占有,有新的线程来竞争锁时,偏向锁将被取消,升级为轻量级锁。轻量级锁会采用cas操作尝试获取锁,当线程尝试10次或超过CPU最大核心数的一半次数后还没获取到锁,则会升级为重量级锁。重量级锁(重量级锁下有一个队列无序存放竞争此资源的线程)则会申请操作系统中的mutex资源并使线程进入阻塞状态,等待操作系统的任务调度器进行调度。
    image.png

    并发包(JUC——java.util.concurrent)

    Java并发包(JUC)中提供了与锁相关的API和组件,其中最核心的就是java.util.concurrent.locks包下的Lock接口和AbstractQueuedSynchronizer抽象类。

    锁接口(Lock)

    锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多个线程访问共享资源,比如读写锁)。
    在Lock接口出现以前,Java程序靠synchronized关键字实现锁功能,而Java SE 5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁的功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取锁和释放锁。
    虽然它缺少了隐式获取锁释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超市获取锁等多种synchronized关键字所不具备的同步特性。
    1. public static void main(String[] args) {
    2. //创建一个可重入锁(Lock实现类之一)
    3. ReentrantLock lock = new ReentrantLock();
    4. //获取锁
    5. lock.lock();
    6. try {
    7. //做点什么
    8. } finally {
    9. //finally确保释放锁
    10. lock.unlock();
    11. }
    12. }
    | 特性 | 描述 | | —- | —- | | 尝试非阻塞地获取锁 | 当前线程尝试获取锁,如果这一时刻所没有被其他线程获取到,则成功获取并持有锁 | | 能被中断的获取锁 | 与synchronized不同,获取到所的线程能够响应中断,当获取到锁的线程被中断时,中断异常会被抛出,同时锁会被释放 | | 超时获取锁 | 在指定的截止时间之前获取锁,如果截止时间到了仍然无法获取锁,则返回 |

队列同步器(AbstractQueuedSynchronizer抽象类)

同步器是用来构建锁或者其他同步组件的基础框架,使用一个int成员变量表示同步状态,内置FIFO队列来完成资源获取线程的排队工作。同步器可重写的方法有:

方法名称 描述
protected boolean tryAcquire(int arg) 独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态
protected boolean tryRelease(int arg) 独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态
protected int tryAcquireShared(int arg) 共享式获取同步状态,返回大于等于0的值,表示获取成功,反之则获取失败。
protected boolean tryReleaseShared(int arg) 共享式释放同步锁
protected boolean isHeldExclusively() 当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占

同步器的使用方法是继承,子类通过集成同步器并实现它的抽象方法来管理同步状态,子类中对同步状态进行更改时需要使用同步器提供的3个方法(getState、setState、compareAndSetState)。

  1. class Mutex implements Lock {
  2. // 静态内部类,自定义同步器
  3. private static class Sync extends AbstractQueuedSynchronizer {
  4. // 是否处于占用状态
  5. protected boolean isHeldExclusively() {
  6. return getState() == 1;
  7. }
  8. // 当状态为0的时候获取锁
  9. public boolean tryAcquire(int acquires) {
  10. if (compareAndSetState(0, 1)) {
  11. setExclusiveOwnerThread(Thread.currentThread());
  12. return true;
  13. }
  14. return false;
  15. }
  16. // 释放锁,将状态设置为0
  17. protected boolean tryRelease(int releases) {
  18. if (getState() == 0) throw new
  19. IllegalMonitorStateException();
  20. setExclusiveOwnerThread(null);
  21. setState(0);
  22. return true;
  23. }
  24. // 返回一个Condition,每个condition都包含了一个condition队列
  25. Condition newCondition() { return new ConditionObject(); }
  26. }
  27. // 仅需要将操作代理到Sync上即可
  28. private final Sync sync = new Sync();
  29. public void lock() { sync.acquire(1); }
  30. public boolean tryLock() { return sync.tryAcquire(1); }
  31. public void unlock() { sync.release(1); }
  32. public Condition newCondition() { return sync.newCondition(); }
  33. public boolean isLocked() { return sync.isHeldExclusively(); }
  34. public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
  35. public void lockInterruptibly() throws InterruptedException {
  36. sync.acquireInterruptibly(1);
  37. }
  38. public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
  39. return sync.tryAcquireNanos(1, unit.toNanos(timeout));
  40. }
  41. }

同步器依赖内部的同步队列来实现同步状态的管理,当线程获取同步状态失败时会将当前线程以及等待状态等信息构造为一个节点(Node)并将其加入同步队列中,同时会阻塞当前线程,当同步状态释放时,会唤醒线程使其再次尝试获取同步状态。
当前有用同步状态的节点为头(head)节点,其余线程都进入阻塞状态作为普通节点。当获取锁失败时通过CAS操作将当前线程的等待节点添加到队列尾部(队列的实现是双向链表结构)。
image.png
image.png
image.png

重入锁(ReentrantLock)

重入锁顾名思义就是可重入的锁,什么是重入呢?表示该锁能够支持一个线程对资源的重复加锁(也就是拿到锁线程可以多次获取锁,synchronized是不可重入的),重入锁会记录此线程锁的获取成功次数(锁状态初始为0,获取后+1,重复获取继续+1),获取了多少次锁(锁状态的值),就得释放多少次锁(释放一次则-1,直到锁状态的值为0)。
重入锁还支持公平和非公平策略,公平指的是按照同步资源的请求时间顺序获取锁,先请求的先得到,依次获取(避免饥饿现象)。非公平指的是所有等待的线程竞争获取锁,一旦锁被释放,所有阻塞的线程都有机会抢到锁(也存在同一线程连续获取到锁的情况)。
image.png
默认是非公平锁,原因是公平性锁会导致线程的上下文切换(耗时操作),公平性锁在吞吐量上不如非公平性锁。

读写锁(ReentrantReadWriteLock)

读写锁顾名思义,将锁分为读与写两部分,在大多数应用场景中读比写频繁,而读操作与读操作(共享式同步)之间不存在不一致性问题,导致不一致性问题的主要原因是写操作(独占式同步)。将锁区分为读锁和写锁可以在很大程度上带来性能的提升。
当存在线程持有读锁时,获取写锁的操作会被阻塞。
当存在线程持有写锁时,获取读锁和写的操作都会被阻塞。
image.png

锁降级

指的是写锁降级为读锁,持有写锁的线程不需要重新竞争读锁,可避免阻塞以及上下文切换。锁降级的操作步骤:

  • 持有写锁
  • 执行写操作
  • 获取读锁(先申请获取读锁)
  • 释放写锁(再释放写锁)
  • 执行读操作
  • 释放读锁

    LockSupport工具

Condition接口

参考文献

《Java并发编程的艺术》
java线程池核心基础