java并发编程JUC补充学习:JUC包常用类

在这里插入图片描述
主要包含

  • atomic包:
    • 基本类型:AtomicInteger、AtomicLong、AtomicBoolean
    • 数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
    • 引用类型:AtomicReference、AtomicMarkableReference、AtomicStampedReference
    • 对象的属性修改类型:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
  • locks包:ReentrantLock、ReentrantReadWriteLock
  • 同步工具类:CountDownLatch、Semaphore、Cyclicbarrier、FutureTask
  • 并发容器类:ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue、BlockingQueue、ConcurrentSkipListMap
  • Executor框架相关类

一、atomic包

在这里插入图片描述

1.基本类型
  • AtomicInteger、AtomicLong、AtomicBoolean
  • 原子性更新基本类型
  • 常用方法get()、getAndSet(int)、getAndIncrement()、getAndAdd(int)、compareAndSet(int, int)等
  • 优点
    • 不需要加锁就可以保证线程安全
  • 线程安全原理
    • CAS+volatile+(native方法),保证线程总能拿到变量的最新值
2.数组类型
  • AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
  • 原子性更新数组中某个元素
  • 常用方法get(int)、getAndSet(int, int)、getAndIncrement(int)、getAndAdd(int, int)、compareAndSet(int, int, int)等
3.引用类型
  • AtomicReference、AtomicMarkableReference(带有版本号,可以解决ABA问题)、AtomicStampedReference(带有标记,不能解决ABA问题)
  • 可以原子性更新多个变量
  • 常用方法:set()、compareAndSet()、get()等
  • 将需要修改的对象放入AtomicReference 对象中set(Object),再修改整个对象compareAndSet(Object, Object),另外两个需要设置版本号(Integer)或标记(Boolean)
4.对象的属性修改类型
  • AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
  • 可以原子性修改对象内部分字段
  • 常用方法newUpdater()等
  • 先创建更新器,传入需要修改的类和字段newUpdater(class,String),修改的字段必须是public volatile修饰的

二、locks包

在这里插入图片描述

1.ReentrantLock
  • 可重入锁,一个线程能够对共享资源多次加锁。
  • 支持公平锁和非公平锁
  • 加锁流程:
    • 若通过 CAS 设置变量 State(同步状态)成功,也就是获取锁成功,则将当前线程设置为独占线程。否则使用Acquire 方法进行后续处理(将线程放入队列等待唤醒)。
    • Lock方法实际调用内部类Sync的Lock方法,本质上执行AQS的Acquire方法,Acquire方法会执行tryAcquire方法,如果获取锁失败,执行AQS后续方法。
  • 解锁流程:
    • Unlock方法实际调用内部类Sync的Release方法(继承于AQS),Release调用tryRelease方法,释放成功后,执行AQS后续方法。
      在这里插入图片描述
2.ReentrantReadWriteLock
  • 读写锁 ReentrantReadWriteLock 可以保证多个线程可以同时读,所以在读操作远大于写操作的时候,读写锁就非常有用了。
3.AQS类(AbstractQueuedSynchronizer
  • 构建锁和同步器的框架,使用 AQS 能简单且高效地构造出大量应用广泛的同步器,包括以下类:

  • ReentrantLockReentrantReadWriteLockSynchronousQueueFutureTaskSemaphoreCountDownLatch

  • AQS在ThreadPoolExecutor的Worker中也有应用:利用 AQS 同步状态实现对独占线程变量的设置(tryAcquire 和 tryRelease)

  • 锁是针对使用者而言的,底层是通过队列同步器实现的。

  • 子类仅使用getState(获取当前状态)、setState(设置状态)和compareAndSetState(CAS保证下设置状态)方法操作原子更新的int值(volatile修饰的同步状态值state)

    • state:标记资源状态:未占用0,大于0每加1则加一重锁(可重入锁),每释放一重锁就减1。(可用于计算共享锁数量)
    • head、tail:CLH队列的头节点和尾节点
    • exclusiveOwnerThread:存放当前获得锁的线程(可重入锁的判断:该属性和申请锁的线程进行对比)
  • 资源共享方式

    • 独占Exclusive:只有一个线程能占有资源,如ReentrantLock
    • 共享Share:多个线程可以同时占有资源,如 SemaphoreCountDownLatch
    • ReentrantReadWriteLock 允许多个线程同时对某一资源进行读,但是只允许一个线程进行写。
  • 如何基于AQS设计同步器

    • 自定义同步器类继承AQS并重写指定方法(钩子方法)

      • 可选择性地重写:独占或共享可重写相应方法即可,也可全部重写实现读共享和写独占(ReentrantReadWriteLock

        //独占方式。获取和释放资源,arg为获取锁次数
        protected boolean tryAcquire(int arg)
        protected boolean tryRelease(int arg)
        //共享方式。获取和释放资源
        //负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
        protected int tryAcquireShared(int arg)
        protected boolean tryReleaseShared(int arg)
        //该线程是否正在独占资源。只有用到condition才需要实现。
        protected boolean isHeldExclusively()
        
    • 自定义锁组件类(实现Lock接口),重写加锁lock()、尝试加锁trylock()、带超时加锁trylock(long time, TimeUnit unit) throws InterruptedException、释放锁unlock()、条件变量newCondition()(可选)

    • 将自定义同步器类组合进自定义锁组件类中,可以在锁组件类中调用同步器类的方法,也可以将同步器类作为锁组件类的内部类。

  • 如果线程A请求的共享资源空闲,则将资源分配给A并将资源加锁。如果A请求的共享资源被占用了,则将A加入到CLH队列(FIFO,双向队列,公平)中。

    • 线程调用acquire方法获取锁,获取失败则将线程阻塞,同时保存为一个Node,包含了线程的状态字段(waitStatus、prev、next等),插入到CLH同步队列的队尾,当同步状态变化时,将CLH队列的头节点唤醒。

      • waitStatus:节点状态,默认0尝试获取锁,1获取锁请求取消,-2等待唤醒,-3共享锁下使用,-1线程准备好等待资源释放
      • thread:节点对应线程
      • prev、next:节点前驱和后继节点
      • predecessor:返回前驱节点,没有则抛出npe

    * [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2hhwE7ax-1658553222923)(../img/image-20220720144543441.png)]

    • 双向队列中,head节点为虚拟头节点,不存储任何信息。

    • 入队:addWaiter(Node mode),其中插入时使用compareAndSetTail(Node expect, Node update)保证线程安全

    •     private Node addWaiter(Node mode) {
              Node node = new Node(Thread.currentThread(), mode);
              // 插入到队列尾部
              Node pred = tail;
              if (pred != null) {
                  node.prev = pred;
                  if (compareAndSetTail(pred, node)) {
                      pred.next = node;
                      return node;
                  }
              }
              // 如果上面插入失败,则多次尝试
              enq(node);
              return node;
          }
          private Node enq(final Node node) {
              // 自旋保证节点插入队列
              for (;;) {
                  Node t = tail;
                  if (t == null) { // 队列为空,直接初始化
                      if (compareAndSetHead(new Node()))
                          tail = head;
                  } else {
                      // 否则多次尝试插入队列尾部
                      node.prev = t;
                      if (compareAndSetTail(t, node)) {
                          t.next = node;
                          return t;
                      }
                  }
              }
          }
      
    • 出队:acquireQueued(final Node node, int arg)、挂起shouldParkAfterFailedAcquire()

      • 入队后,如果当前节点的前驱节点为头节点,则自旋获取锁,可以设置是否阻塞(LockSupport.park)防止cpu浪费
      • 线程A入队如果前驱节点H状态(waitStatus)为0,则自旋获取锁,如果获取成功,则设A为新的头节点,否则将H状态设为-1(阻塞A)。
      • 当有线程释放锁时发现头节点状态为-1就会唤醒其后继节点。这里不需要CAS保证因为只有后继节点能够获取锁(公平锁)
      • 如果状态大于0则取消前驱节点cancelAcquire(Node node)直到前驱节点状态小于等于0,这时只修改next指针,当获取锁失败时(shouldParkAfterFailedAcquire)才修改prev指针。(防止之前的节点获取锁出队了,prev指针指向了队列外的节点,不安全)
4.补充:ReentrantLock和Synchronized对比

image-20220720210651022

三、同步工具类

1.CountDownLatch(倒计时器)
  • 可以让count个线程阻塞,直到所有线程执行完毕。用于主线程等待其他线程处理完毕后再执行、实现多个线程在某一时刻同时开始执行。

    • 商品详情页的加载需要等待商品信息、评论信息等数据加载完毕后再包装发送给前端
  • 基于AQS的共享锁机制实现,默认构造AQS的statecount

  • 不足:一次性的,使用不当容易产生死锁,count到不了0的话

  • 使用:

    • // 1.主线程等待其他线程处理完毕后再执行
      // 构造函数,初始化计数器count——550
      final CountDownLatch countDownLatch = new CountDownLatch(550);
      // 
      threadPool.execute(() -> {
          try {
              test(threadnum);
          } catch (InterruptedException e) {
              e.printStackTrace();
          } finally {
              // 一个线程经处理完毕,count减1
          	countDownLatch.countDown();
          }
      
          });
          }
      	// 当count为0时唤醒countDownLatch上的线程,否则阻塞当前线程,不执行后面的代码
          countDownLatch.await();
      // 2.实现多个线程在某一时刻同时开始执行
      // (1)初始化一个共享的 CountDownLatch 对象,将其计数器初始化为 1 (new CountDownLatch(1))
      // (2)多个线程在开始执行任务前首先 coundownlatch.await()
      // (3)当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。
      
    • countDown():让count减1,减为0时唤醒countDownLatch上所有线程

    • await():使当前线程进入同步队列中等待,直到count为0,可带超时时间

    • getCount():获取当前count

2.Semaphore(信号量)
  • 可以指定多个线程同时访问某个资源。用于控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源

    • 数据库连接限制
  • 基于AQS的共享锁机制实现

  • 使用:

    • // 允许同时执行的线程数量——20。
      final Semaphore semaphore = new Semaphore(20);
      //获取、释放许可,也可一次获取和释放多个许可(一般不用)
      threadPool.execute(() -> {
          try {
              semaphore.acquire();//获取
              test(threadnum);
              semaphore.release();//释放
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      });
      
    • acquire():获取不到许可阻塞,响应中断

    • tryAcquire():获取不到许可立即返回false,不阻塞不响应中断,可带超时时间

    • acquireUninterruptibly(),获取不到许可阻塞,不响应中断

  • 可设置公平模式和非公平模式

    • 公平模式:调用acquire()方法遵循FIFO,可以使用Semaphore(int permits, boolean fair)构造。
    • 非公平模式:抢占,使用Semaphore(int permits)构造默认是非公平模式。
    • 公平模式在获取许可时先调用hasQueuedPredecessors方法判断队列中是否有其他线程在排队
3.Cyclicbarrier(循环栅栏)
  • CountDownLatch 类似,也可以实现多线程等待,但是功能更多。应用场景也和 CountDownLatch 类似。所有线程到达设定的屏障(同步点)时才会继续运行,否则阻塞。

  • 基于 ReentrantLockCondition实现的( ReentrantLock也是基于AQS)

  • 使用

    • // 构造函数,parties表示屏障拦截的线程数
      // 当满足条件时,优先执行barrierAction
      CyclicBarrier(int parties)
      CyclicBarrier(int parties, Runnable barrierAction)
      // 线程调用 await() 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞
      cyclicBarrier.await();
      
    • await():实际调用dowait(false, 0L)方法,每次count减1,减为0时继续执行

4.FutureTask
  • 传入Runnable或Callable任务给FutureTask,调用run()方法运行,之后外部线程调用get()方法异步获取结果。用于异步获取执行结果或取消执行任务,处理各个线程之间结果相互依赖的情况,适合耗时的计算。

  • 继承了RunnableFuture(Runnable和Future),通过state(int值)标志对象状态,共有七种状态,状态转换通过CAS保证原子性

  • 使用

    • //构造方法
      FutureTask(Callable<V> callable)
      FutureTask(Runnable runnable, V result)
      
    • run():执行提交的任务

    • get():检索结果值,未完成则阻塞线程,可设置超时时间

    • cancel():取消任务。如果当前任务已经结束或者已经取消,则无法再次取消;如果任务尚未开始,则直接不执行任务。

5.补充
CountDownLatchCyclicBarrier 的不同之处
  • CountDownLatch 是一次性的,只能用一次,而 CyclicBarrier可以多次使用(reset)
  • 当计数为0时,下一步的动作实施者是不一样的。对于CountDownLatch,,下一步的动作实施者是“主线程”;对于CyclicBarrier,下一步动作实施者是“其他线程”。从定义理解:CountDownLatch是一个或多个线程等待其他线程;而CyclicBarrier是多个线程互相等待

四、并发容器类

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.ConcurrentHashMap
  • 保证HashMap读写时的并发安全
  • 详见java容器
2.CopyOnWriteArrayList
  • 类似ReentrantReadWriteLock读操作时共享锁,写操作时独占锁,CopyOnWriteArrayList的读操作完全不加锁,写操作不会阻塞读操作,只有写操作和写操作之间相互阻塞,大幅提高读操作性能。

  • 原理:所有修改的操作都是通过创建底层数组的副本,然后修改副本的内容,修改完后再替换掉原来的数组,因此不会影响读操作。

  • CopyOnWrite:修改时不在原来的数据上进行修改,而是将数据拷贝一份,修改新的数据,修改完后将指向原来数据的指针指向新的数据,最后回收原来的数据。

  • 常用方法

    • 读取get(int)

    •     private transient volatile Object[] array;
          public E get(int index) {
              return get(getArray(), index);
          }
      	@SuppressWarnings("unchecked")
          private E get(Object[] a, int index) {
              return (E) a[index];
          }
          final Object[] getArray() {
              return array;
          }
      
    • 写入add(E):通过ReentrantLock对复制过程Arrays.copyOf()(本质上还是调用System.arraycopy())加锁,保证多线程安全

    •     public E set(int index, E element) {
              final ReentrantLock lock = this.lock;
              lock.lock();
              try {
                  Object[] elements = getArray();
                  E oldValue = get(elements, index);
      
                  if (oldValue != element) {
                      int len = elements.length;
                      Object[] newElements = Arrays.copyOf(elements, len);
                      newElements[index] = element;
                      setArray(newElements);
                  } else {
                      // Not quite a no-op; ensures volatile write semantics
                      setArray(elements);
                  }
                  return oldValue;
              } finally {
                  lock.unlock();
              }
          }
      
3.ConcurrentLinkedQueue
  • 非阻塞队列,通过CAS操作实现,底层使用链表实现,性能好
  • 适合性能要求高,多线程读写队列的情况(加锁成本较高)
4.BlockingQueue
  • 阻塞队列(FIFO),通过加锁实现,提供可阻塞的插入和移除方法
  • 广泛应用于“生产者-消费者”问题中,队列满时阻塞生产者线程;队列空时阻塞消费者线程
  • 常用方法add()、offer()、remove()、poll()(同队列,不阻塞)、put()、take()(阻塞,可设置超时时间)
  • 批量操作不一定是原子操作addAll()、removeall()等
  • 常用实现类:ArrayBlockingQueueLinkedBlockingQueuePriorityBlockingQueue
ArrayBlockingQueue
  • 底层使用数组实现,创建后容量不能改变(有界),通过ReentrantLock对读写操作进行加锁,队列满时,阻塞插入操作;队列空时,阻塞移除操作。多个线程阻塞时默认是非公平的等待(ReentrantLock默认非公平),可以通过以下构造函数实现公平等待

  • // 实际上还是将fair传入ReentrantLock实现公平锁
        public ArrayBlockingQueue(int capacity, boolean fair) {
            if (capacity <= 0)
                throw new IllegalArgumentException();
            this.items = new Object[capacity];
            lock = new ReentrantLock(fair);
            notEmpty = lock.newCondition();
            notFull =  lock.newCondition();
        }
    
LinkedBlockingQueue
  • 底层使用链表实现,创建时可以指定容量也可以不指定容量(上限为Integer.MAX_VALUE,可能占用大量内存)
  • 使用虚拟头节点,put和take操作需要获取相应的锁和条件(put时需要获取putLock且notFull,take时需要获取putLock且notFull。
PriorityBlockingQueue
  • 基于数组的二叉堆(根节点最小)实现,支持优先级的无界阻塞队列,指定初始大小后面自动扩容(默认初始大小11,上限为Integer.MAX_VALUE - 8),不能插入null,插入对象必须可比较大小(comparable),put不会阻塞(无界),take会阻塞。

  • 遍历时不能保证有序性,可以通过Arrays.sort(queue.toArray())得到有序数组,默认升序排序,可以通过以下构造函数自定义比较器

  • public PriorityBlockingQueue(int initialCapacity,
                                     Comparator<? super E> comparator) {
            if (initialCapacity < 1)
                throw new IllegalArgumentException();
            this.lock = new ReentrantLock();
            this.notEmpty = lock.newCondition();
            this.comparator = comparator;
            this.queue = new Object[initialCapacity];
        }
    
  • 自动扩容机制:扩容前先释放锁(扩容和读操作可以同时进行),CAS操作保证原子性,设原来容量为x,如果x小于64,则增加x+2的容量;如果大于64,则增加x / 2的容量。扩容后判断是否超出最大容量,若超过则设置为最大容量(防止溢出)

  • 复习:二叉堆的插入和删除(手写堆排序)

5.ConcurrentSkipListMap
  • 底层基于跳表实现,遍历时保证有序性。对比基于哈希算法实现的Map是无序的。
  • 跳表:多层链表,最底层为原始链表,包含所有元素,往上为一层层索引,上层元素为下层元素的子集,同一列元素是相同的,每一行链表都是排好序的
    • 查找:从左到右从上到下查找,若查找元素大于当前元素,则跳到下一层继续向右查找
    • 详见数据结构

五、Executor框架相关类

在这里插入图片描述

1.简介
  • Executor框架是java5之后引入的,通过Executor启动线程比Thread的start方法更好,更易于管理、效率更好、有助于避免this逃逸问题
    • this 逃逸:在构造函数返回之前其他线程就持有该对象的引用. 调用尚未构造完全的对象的方法
2.结构
  • 任务
    • Runnable或Callable接口(一个有返回一个无返回)
  • 任务执行
    • Executor和ExecutorService接口(继承自Executor)
    • ThreadPoolExecutor 、ScheduledThreadPoolExecutor(继承了ThreadPoolExecutor,实现了ScheduledExecutorService) 实现了 ExecutorService 接口。
  • 异步计算的结果
    • Future接口及其实现类FutureTask
    • 将任务实现类提交给ThreadPoolExecutor执行,调用submit()提交时会返回一个FutureTask对象
3.使用
  • 创建Runnable或Callable接口的任务实现类
  • 将实现类对象交给ExecutorService执行
    • ExecutorService.execute(Runnable command)或ExecutorService.submit(Runnable task)
  • 上一步中调用submit()方法会返回一个FutureTask对象,由于 FutureTask 实现了 Runnable,也可以直接创建 FutureTask,然后直接交给 ExecutorService 执行
  • 最后,主线程可以执行FutureTask.get()方法来等待任务执行完成,或者执行FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行
4.ThreadPoolExecutor(核心)
  • 创建线程池使用ThreadPoolExecutor而不是Executors!
    在这里插入图片描述

在这里插入图片描述

对比
  • shutdown()和shutdownNow()
    • shutdown()关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务,但是队列里的任务得执行完毕。
    • shutdownNow()关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,不再执行队列中的任务并返回任务队列。
  • isTerminated()和isShutdown()
    • 调用shutdown()后isShutdown()为true,等所有任务执行完毕后isTerminated()为true
常见线程池
  • FixedThreadPool,可重用固定线程数的线程池
    • 可设置corePoolSize 和 maximumPoolSize
    • 使用无界队列 LinkedBlockingQueue(队列的容量为 Integer.MAX_VALUE)作为线程池的工作队列,新任务会一直放入队列中,导致maximumPoolSize和keepAliveTime无效,不会拒绝任务,任务多时容易导致OOM
  • SingleThreadExecutor
    • corePoolSize 和 maximumPoolSize设置为1
    • 同FixedThreadPool使用无界队列
  • CachedThreadPool
    • corePoolSize 设置为0,maximumPoolSize设置为 Integer.MAX.VALUE
    • 如果任务提交速度大于任务处理速度会不断创建新线程,容易导致cpu和内存资源耗尽
  • ScheduledThreadPool
    • 实际项目一般不使用,也不推荐使用,了解即可
    • 用于再给定延时后执行任务,或定期执行任务
    • 使用DelayQueue(实现优先级)存放任务,时间短的任务先执行,若时间相同则先提交的任务优先。
    • 执行周期任务时从DelayQueue获取任务时间大于当前时间的任务并执行,然后将这个任务时间修改为下次要执行的时间,最后放回DelayQueue中
    • ScheduledThreadPoolExecutor 和 Timer 的比较
      • Timer基于单线程、系统时间实现。执行时出错将导致线程终止,所有任务都终止;如果任务执行时间过长会导致周期性不准确;修改系统时间会破坏周期性;
      • ScheduledThreadPoolExecutor 基于多线程、JVM时间实现。任务之间不会互相影响,周期性有保证,更加精确。
  • 8
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值