【Java】JUC并发(JUC并发集合、线程池)

一、概念

        针对List、Map、Set、Queue等集合接口,提供了支持并发的线程安全的集合实现类。

1、CopyOnWriteArrayList

        我们对该集合进行增、删、改时,并不会在原集合中进行操作,而是将原集合复制到一个新的集合中,对新集合进行操作后,再将新操作放回原集合。改集合使用 ReentrantLock 锁来实现线程安全,但是运行多线程并发进行读取,只允许一个线程进行写入。

    public boolean add(E e) {
        // 使用synchronized来实现,我的版本是jdk17,jdk8是使用ReentrantLock来实现
        synchronized (lock) {
            Object[] es = getArray();
            int len = es.length;
            es = Arrays.copyOf(es, len + 1);
            es[len] = e;
            setArray(es);
            return true;
        }
    }


    public E set(int index, E element) {
        // 使用synchronized来实现,我的版本是jdk17,jdk8是使用ReentrantLock来实现
        synchronized (lock) {
            Object[] es = getArray();
            E oldValue = elementAt(es, index);

            if (oldValue != element) {
                es = es.clone();
                es[index] = element;
            }
            // Ensure volatile write semantics even when oldvalue == element
            setArray(es);
            return oldValue;
        }
    }

2、ConcurrentHashMap

        该集合主要采用 CAS 和 synchronized 来实现线程安全,当多线程并发但没有哈希冲突时,则使用CAS来进行存储,当存在哈希冲突时该集合通过 synchronized 锁来锁定链表的头结点和红黑树的根结点实现线程安全。

【Java】JUC并发(JUC并发集合、线程池)_第1张图片

3、BlockingQueue阻塞队列

        实现机制是使用两条线程,允许两个线程同时操作队列。当队列中数据写满时,写入线程会进行自动阻塞;当对列中没有数据时,读取线程会被阻塞。

【Java】JUC并发(JUC并发集合、线程池)_第2张图片

4、ArrayBlockingQueue

         该阻塞队列是一个有界队列,基于数组实现,创建时必须设计大小,也可以设置其锁是公平锁还非公平锁,读写不能同时操作。

    public void put(E e) throws InterruptedException {
        Objects.requireNonNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            // 当队列中的元素装满时,则对写入队列进行堵塞
            while (count == items.length)
                notFull.await(); // 阻塞写入线程
            enqueue(e);
        } finally {
            lock.unlock(); // 释放锁
        }
    }


    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            // 当队列中的元素为0时,则对写入队列进行堵塞
            while (count == 0)
                notEmpty.await(); // 阻塞读取线程
            return dequeue();
        } finally {
            lock.unlock(); // 释放锁
        }
    }

4、LinkedBlockingQueue

        该阻塞队列是一个有界队列,基于链表实现,创建也可以设置其容量大小,如果不设置则最大值为Integer.MAX_VALVE。入队和出队由两把锁分别控制,从而使入队和出队操作可以同时进行。

public void put(E e) throws InterruptedException {
    // 禁止插入null元素
    if (e == null) throw new NullPointerException();
    // c变量用于记录插入元素前的队列大小
    final int c;
    // 创建新节点包装元素
    final Node node = new Node(e);
    // 获取put操作的锁(可中断锁)
    final ReentrantLock putLock = this.putLock;
    // 获取当前队列元素计数(原子操作)
    final AtomicInteger count = this.count;
    // 加锁(支持中断)
    putLock.lockInterruptibly();
    try {
        // 如果队列已满,则线程进入等待状态
        while (count.get() == capacity) {
            notFull.await();
        }
        // 将元素加入队列尾部
        enqueue(node);
        // 获取插入前的队列大小,并将计数加1
        c = count.getAndIncrement();
        // 如果插入后队列仍未满,唤醒其他可能在等待的put线程
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        // 释放锁
        putLock.unlock();
    }
    // 如果插入前队列为空(插入后至少有一个元素),唤醒可能在等待的take线程
    if (c == 0)
        signalNotEmpty();
}
public E take() throws InterruptedException {
    final E x;
    final int c;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    // 加锁(支持中断)
    takeLock.lockInterruptibly();
    try {
        while (count.get() == 0) {
            notEmpty.await();
        }
        // 从队列头部移除元素
        x = dequeue();
        // 获取移除前的队列大小,并将计数减1
        c = count.getAndDecrement();       
        // 如果移除后队列仍不为空,唤醒其他可能在等待的take线程
        if (c > 1)
            notEmpty.signal();
    } finally {
        // 释放锁
        takeLock.unlock();
    }
    // 如果移除前队列已满(移除后队列有空间),唤醒可能在等待的put线程
    if (c == capacity)
        signalNotFull();
    return x;
}

二、线程池

1、作用

        对线程进行管理和复用。

2、线程池的创建

// 创建线程池对象
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
                10, // 核心线程数量
                100, // 线程总数
                60L, // 线程存活时间
                TimeUnit.SECONDS, // 时间单位
                new LinkedBlockingQueue<>());// 工作队列(阻塞队列)

3、实现过程

【Java】JUC并发(JUC并发集合、线程池)_第3张图片

1、提交线程,判断线程池中是否有空闲线程,如果有则直接为其分配空闲线程,若没有则判断已有线程数量是否超过核心线程数量,若没有超过则创建核心线程分配给线程,如果已经超出则判断工作队列是否已满,如果未满则将线程放入工作队列,如果已满则判断已有线程数量是否超过最大线程数,如果没有超出则创建非核心线程,如果超出则执行拒绝策略。

4、拒绝策略

        线程池的四种拒绝策略分别是:AbortPolicy、CallerRunsPolicy、DiscardPolicy 和 DiscardOldestPolicy。它们分别适用于不同的场景:

1. AbortPolicy(默认策略):

  • 场景:当线程池的任务队列已满,并且线程池中的线程数已达到最大线程数时,新提交的任务会被拒绝,并抛出 RejectedExecutionException 异常。
  • 适用场景:适用于需要明确知道任务被拒绝的场景,比如关键任务不能丢失,需要及时处理异常。

2. CallerRunsPolicy:

  • 场景:当线程池的任务队列已满,并且线程池中的线程数已达到最大线程数时,新提交的任务会由提交任务的线程(调用者线程)直接执行。
  • 适用场景:适用于任务提交速率较慢,或者任务执行时间较短的场景。这样可以减缓任务提交的速度,避免线程池过载。

3. DiscardPolicy:

  • 场景:当线程池的任务队列已满,并且线程池中的线程数已达到最大线程数时,新提交的任务会被直接丢弃,不会抛出异常。
  • 适用场景:适用于可以容忍任务丢失的场景,比如日志记录、非关键任务等。

4. DiscardOldestPolicy:

  • 场景:当线程池的任务队列已满,并且线程池中的线程数已达到最大线程数时,新提交的任务会丢弃队列中最旧的任务(即队列头部的任务),然后尝试将新任务加入队列。
  • 适用场景:适用于可以容忍丢弃旧任务的场景,比如实时性要求较高的任务,新任务比旧任务更重要。

你可能感兴趣的:(java,开发语言,经验分享,idea,jvm)