JUC——共享模型之工具

8. 共享模型之工具

8.1 线程池

线程池管理着一组预先创建好的线程,这些线程可以被重复地利用来执行多个任务,而不需要为每个任务都创建和销毁线程。线程池通常由一个任务队列和一组工作线程组成。

线程池其实是一种池化的技术实现,池化技术的核心思想就是实现资源的复用,避免资源的重复创建和销毁带来的性能开销。

  • Java 主要是通过构建 ThreadPoolExecutor 来创建线程池的

1. 线程池状态

ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量:

状态名高3位接收新任务处理阻塞队列任务说明
RUNNING111YY
SHUTDOWN000NN不会接收新任务,但会处理阻塞队列剩余 任务
STOP001NN会中断正在执行的任务,并抛弃阻塞队列 任务
TIDYING010--任务全执行完毕,活动线程为 0 即将进入终结
TERMINATED011--终结状态

从数字上比较,TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING(-1)。这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作进行赋值

2. 线程池参数
public ThreadPoolExecutor(int corePoolSize,
            int maximumPoolSize,
            long keepAliveTime,
            TimeUnit unit,
            BlockingQueue<Runnable> workQueue,
            ThreadFactory threadFactory,
            RejectedExecutionHandler handler)
  • corePoolSize 核心线程数目 (最多保留的线程数)

  • maximumPoolSize 最大线程数目

  • keepAliveTime 生存时间 - 超出 corePoolSize 后创建的线程存活时间(针对救急线程)

  • unit 时间单位 - keepAliveTime 的时间单位(针对救急线程 )

  • workQueue 阻塞队列,当线程数达到核心线程数后,会将任务存储在阻塞队列

  • threadFactory 线程池内部创建线程所用的工厂

  • handler 拒绝策略。当队列已满并且线程数量达到最大线程数量时,会调用该方法处理任务。

工作原理:
  • 当有线程通过 execute 方法提交了一个任务,首先会去判断当前线程池的线程数是否小于核心线程数corePoolSize, 如果小于,那么就直接通过 ThreadFactory 创建一个线程来执行这个任务。

  • 当任务执行完之后,线程不会退出,而是会去阻塞队列中获取任务。

  • 提交任务的时,即使有线程池里的线程从阻塞队列中获取不到任务,如果线程池里的线程数还是小于核心线程数,那么依然会继续创建线程,而不是复用已有的线程。如果线程池里的线程数不再小于核心线程数,就会尝试将任务放入阻塞队列中。

  • 如果队列已满,任务放入失败,如果小于最大线程数 maximumPoolSize,那么也会创建非核心线程来执行提交的任务。就算队列中有任务,新创建的线程还是会优先处理这个提交的任务,而不是从队列中获取已有的任务执行,从这可以看出,先提交的任务不一定先执行

  • 线程数已经达到最大线程数量时就会执行拒绝策略,用RejectedExecutionHandler 对象来处理这个任务。

  • JDK 自带的 RejectedExecutionHandler 实现有 4 种,默认是 AbortPolicy 策略。

    • AbortPolicy:丢弃任务,抛出运行时异常

    • CallerRunsPolicy:由提交任务的线程来执行任务

    • DiscardPolicy:丢弃这个任务,但是不抛异常

    • DiscardOldestPolicy:从队列中剔除最先进入队列的任务,本任务取而代之

3. 工厂方法
  • newFixedThreadPool
    • 核心线程数 == 最大线程数,因此也无需超时时间。阻塞队列是无界的,可以放任意数量的任务。

    • 适用于任务量已知,相对耗时的任务

  • newCachedThreadPool
    • 核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,全都是救急线程。

    • 可以根据需要自动调整线程的数量,没有固定的线程数量上限,线程空闲一定时间后(60s)会被回收。

    • 队列采用了 SynchronousQueue ,初始不会创建任何线程,有任务提交时才会动态地创建线程。

  • newSingleThreadExecutor
    • 用于创建一个单线程的线程池。这个线程池中只有一个工作线程,用来顺序执行提交的任务。

    • 工作线程在执行完任务后不会被销毁,而是会继续等待新的任务到来。

    • 适用于希望任务排队顺序执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。

4. 提交任务
// 执行任务
void execute(Runnable command);

// 提交任务 task,用返回值 Future 获得任务执行结果
<T> Future<T> submit(Callable<T> task);

// 提交 tasks 中所有任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
					throws InterruptedException;

// 提交 tasks 中所有任务,带超时时间
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
					long timeout, TimeUnit unit)
 					throws InterruptedException;

// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
					throws InterruptedException, ExecutionException;

// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
					long timeout, TimeUnit unit)
					throws InterruptedException, ExecutionException,                     
                    TimeoutException;
5. 关闭线程池

shutdown():- 线程池状态变为 SHUTDOWN

  • shutdown() 方法允许线程池继续执行已提交的任务,但不再接受新的任务。不会阻塞线程执行。

  • 调用 shutdown() 方法后,线程池不会立即关闭,而是等待之前提交的任务执行完毕后关闭。在调用 shutdown() 后再次提交任务会抛出 RejectedExecutionException 异常。

shutdownNow():- 线程池状态变为 STOP

  • shutdownNow() 方法会立即关闭线程池,且不会接收新任务。

  • 它会尝试中断(interrupt)正在执行的任务,并返回未执行的任务列表。

其他方法:

// 不在 RUNNING 状态的线程池,此方法就返回 true
boolean isShutdown();

// 线程池状态是否是 TERMINATED
boolean isTerminated();

// 调用 shutdown 后,由于调用线程并不会等待所有任务运行结束,因此如果它想在线程池 TERMINATED 后做些事情,可以利用此方法等待
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;

awaitTermination() 方法用于阻塞当前线程,直到线程池中的所有任务执行完成或者等待超时。

8.2 AQS原理

AQS (AbstractQueuedSynchronizer) ,即抽象队列同步器,是阻塞式锁和相关的同步器工具的框架。AQS 使用了 FIFO(先进先出)的双向链表队列来管理等待获取资源的线程,它是许多并发工具的基础,如 ReentrantLock、Semaphore、CountDownLatch 等。

  • 用一个volatile的变量state 属性来表示资源的状态,定义了几个获取和改变 state 的 protected 方法,子类可以覆盖这些方法来实现自己的逻辑:

    getState()
    setState()
    compareAndSetState()
  • AQS 内部使用了一个先进先出FIFO的双端队列,并使用了两个引用 head 和 tail 用于标识队列的头部和尾部,类似于 Monitor 的 EntryList ,但它并不直接储存线程,而是储存拥有线程的 Node 节点。

  • 条件变量(Condition)来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet

资源有两种共享模式,或者说两种同步方式:

  • 独占模式(Exclusive):资源是独占的,一次只能有一个线程获取。如ReentrantLock

  • 共享模式(Share):同时可以被多个线程获取,具体的资源个数可以通过参数指定。如Semaphore/CountDownLatch

AQS 中关于这两种资源共享模式的定义源码均在内部类 Node 中。

  • AQS 中的核心方法是 acquirerelease,它们用于获取和释放资源。当一个线程调用 acquire 方法尝试获取资源时,如果资源已经被占用,线程会被加入到等待队列中并进入阻塞状态,直到资源被释放为止。

  • 子类主要实现这样一些方法(默认抛出 UnsupportedOperationException)

    • tryAcquire - 非阻塞

    • tryRelease

    • tryAcquireShared

    • tryReleaseShared

    • isHeldExclusively

8.3 ReentrantLock原理

ReentrantLock 重入锁,是实现Lock 接口的一个类,支持重入性,表示能够对共享资源重复加锁,即当前线程获取该锁后再次获取不会被阻塞。

非公平锁原理

先从构造器开始看,默认为非公平锁实现。NonfairSync 继承自 AQS

public ReentrantLock() {
	sync = new NonfairSync();
}

竞争出现时,Thread-1 执行了

  1. CAS 尝试将 state 由 0 改为 1,结果失败

  2. 进入 tryAcquire 逻辑,这时 state 已经是1,结果仍然失败

  3. 接下来进入 addWaiter 逻辑,构造 Node 队列

    • 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态

    • Node 的创建是懒惰的

    • 其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程

当前线程进入 acquireQueued 逻辑

  1. acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞

  2. 如果自己是紧邻着 head(排第二位),那么再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败

  3. 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1,这次返回 false

  4. shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败。

  5. 当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node 的 waitStatus 已经是 -1,这次返回 true

  6. 进入 parkAndCheckInterrupt, Thread-1 park(灰色表示)

  7. 再次有多个线程经历上述过程竞争失败,变成下图样子

Thread-0 释放锁,进入 tryRelease 流程,如果成功,设置 exclusiveOwnerThread 为 null ,state = 0

当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor 流程

找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1 回到 Thread-1 的 acquireQueued 流程。如果加锁成功(没有竞争),会设置

  • exclusiveOwnerThread 为 Thread-1,state = 1

  • head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread

  • 原本的 head 因为从链表断开,而可被垃圾回收 如果这时候有其它线程来竞争(非公平的体现),例如这时有 Thread-4 来了

如果这时候有其它线程来竞争(非公平的体现),竞争失败会再次进入park阻塞。

可重入原理
static final class NonfairSync extends Sync {
    // ...
 
    // Sync 继承过来的方法, 方便阅读, 放在此处
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) { // 如果当前锁的状态值为 0,表示锁当前没有被持有,即可以尝试获取锁。
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入
        else if (current == getExclusiveOwnerThread()) {
            // 将当前锁的状态增加 acquires(即当前线程尝试获取的资源数量)
            // 表示重入了 acquires 次
            int nextc = c + acquires;
          
            // 如累加后的状态值超过了整数的最大值(Integer.MAX_VALUE)
            // 则会发生整数溢出,导致状态值变成负数。
            if (nextc < 0) 
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
 
    protected final boolean tryRelease(int releases) {
        // 获取当前锁的状态值,并减去要释放的资源数量 releases
        int c = getState() - releases;
      
        // 如果当前线程不是锁的拥有者,没有权限释放锁
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        // 如果 c 减为 0, 表示释放了锁的所有资源,即锁的状态归零
        if (c == 0) {
            free = true; // free为true表示释放成功
            setExclusiveOwnerThread(null);
        }
        setState(c);  // 设置status为0
        return free;
    }
}
可打断原理
  • 不可打断模式

在此模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了

  • 可打断模式

如果线程在获取资源时被中断,它会立即停止等待,清理其在队列中的状态

if (shouldParkAfterFailedAcquire(p, node) &&
		parkAndCheckInterrupt()) {
 		// 在 park 过程中如果被 interrupt 会进入此
 		// 这时候抛出异常, 而不会再次进入 for (;;)
 		throw new InterruptedException();
公平锁实现原理
// 与非公平锁主要区别在于 tryAcquire 方法的实现
 protected final boolean tryAcquire(int acquires) {
 	final Thread current = Thread.currentThread();
 	int c = getState();
 	if (c == 0) {
 		// 先检查 AQS 队列中是否有前驱节点, 没有才去竞争
 		if (!hasQueuedPredecessors() &&
 		compareAndSetState(0, acquires)) {
 			setExclusiveOwnerThread(current);
 			return true;
 		}
	}
}
条件变量实现原理

每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject

await流程
  • 开始 Thread-0 持有锁,调用 await,进入 ConditionObject 的 addConditionWaiter 流程——创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部

  • 接下来进入 AQS 的 fullyRelease 流程,释放同步器上的锁,exclusiveOwnerThread设为null

  • unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功,park 阻塞 Thread-0

signal流程
  • 假设 Thread-1 要来唤醒 Thread-0,进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node

  • 执行 transferForSignal 流程,将该 Node 加入 AQS 队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的 waitStatus 改为 -1

  • Thread-1 释放锁,进入 unlock 流程

8.4 读写锁原理

ReentrantReadWriteLock 是 Java 的一种读写锁,读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。这种锁的设计适用于读操作的数量远远超过写操作的情况下。

读写锁的特性:

1)公平性选择:支持非公平性(默认)和公平的锁获取方式,非公平的吞吐量优于公平;

2)重入性:支持重入,读锁获取后能再次获取,写锁获取之后能够再次获取写锁,同时也能够获取读锁;

3)锁降级:写锁降级是一种允许写锁转换为读锁的过程。通常的顺序是:

  • 获取写锁:线程首先获取写锁,确保在修改数据时排它访问。

  • 获取读锁:在写锁保持的同时,线程可以再次获取读锁。

  • 释放写锁:线程保持读锁的同时释放写锁。

  • 释放读锁:最后线程释放读锁。

这样,写锁就降级为读锁,允许其他线程进行并发读取,但仍然排除其他线程的写操作。下面的代码展示了如何使用 ReentrantReadWriteLock 来降级写锁:

ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();

writeLock.lock(); // 获取写锁
try {
    // 执行写操作
    readLock.lock(); // 获取读锁
} finally {
    writeLock.unlock(); // 释放写锁
}

try {
    // 执行读操作
} finally {
    readLock.unlock(); // 释放读锁
}

8.5 Semaphore

Semaphore是信号量,通过维护一组许可(permits)来限制能同时访问共享资源的线程上限。

Semaphore 类提供了几个关键方法来实现线程的同步控制:

  • Semaphore(int permits, boolean fair):构造方法,创建一个具有给定许可数的信号量,除了许可数外,还可以指定公平性。如果设置为 true,则等待时间最长的线程会首先获得许可

  • void acquire(int permits):获取指定数量的许可,如果无可用许可,线程将被阻塞直到有许可可用。

  • void release(int permits):释放多个许可,增加信号量的可用许可数。如果有其他线程因为调用 acquire() 被阻塞,释放许可后可能会唤醒这些线程。

  • int availablePermits():返回当前可用的许可数。

  • boolean tryAcquire():尝试无阻塞地获取许可。如果有可用许可,立即返回 true;如果没有可用许可,立即返回 false

  • boolean tryAcquire(long timeout, TimeUnit unit):尝试在指定的时间内获取许可。如果在指定时间内获取了许可,则返回 true;否则,返回 false

加锁解锁流程:

假设 permits (state) 数量为 3,这时 5 个线程来获取资源,假设其中 Thread-1,Thread-2,Thread-4 通过CAS 竞争成功,设置 permits 为0。而 Thread-0 和 Thread-3 竞争失败,则失败者进入 AQS 队列 park 阻塞。

这时 Thread-4 释放了 permits,接下来 Thread-0 竞争成功,permits 再次设置为 0,设置自己为 head 节点,断开原来的 head 节点,unpark 接 下来的 Thread-3 节点,但由于 permits 是 0,因此 Thread-3 在尝试不成功后再次进入 park 状态。

8.6 CountdownLatch & CyclicBarrier

CountdownLatch
  • CountDownLatch 主要用于进行线程同步协作,等待其他线程完成某些操作,完成倒计时。 其中构造参数用来初始化等待计数值,await() 用来等待计数归零,countDown() 用来让计数减一。

  • 这在系统初始化或并行任务分配时非常有用,例如,在启动过程中等待外部资源加载完成。

  • 在一个示例中,主线程创建并启动了三个工作线程,每个线程完成任务后调用 countDown() 方法。主线程通过调用 await() 方法等待三个线程全部完成。当所有工作线程执行完成后,CountDownLatch 的计数达到零,主线程恢复执行。这确保了在所有资源都加载完毕之前,主逻辑不会开始执行。

  • 可以配合线程池使用

CyclicBarrier

CyclicBarrier(int parties):构造一个新的 CyclicBarrier,其中 parties 指的是必须到达屏障点的线程数量。循环栅栏,用来进行线程协作,等待线程满足某个计数。

CyclicBarrier 与 CountDownLatch 的主要区别在于 CyclicBarrier 是可以重用的,不用重复创建对象

CyclicBarrier cb = new CyclicBarrier(2); // 个数为2时才会继续执行
​
new Thread(()->{
    System.out.println("线程1开始.."+new Date());
    try {
        cb.await(); // 当个数不足时,等待
    } catch (InterruptedException | BrokenBarrierException e) {
        e.printStackTrace();
    }
    System.out.println("线程1继续向下运行..."+new Date());
}).start();
new Thread(()->{
    System.out.println("线程2开始.."+new Date());
    try { Thread.sleep(2000); } catch (InterruptedException e) { }
    try {
        cb.await(); // 2 秒后,线程个数够2,继续运行
    } catch (InterruptedException | BrokenBarrierException e) {
        e.printStackTrace();
    }
    System.out.println("线程2继续向下运行..."+new Date());
}).start();

  • 17
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值