Java并发编程-并发工具类及线程池

时间:2023-03-08 15:34:16

  JUC中提供了几个比较常用的并发工具类,比如CountDownLatch、CyclicBarrier、Semaphore。

CountDownLatch:

  countdownlatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完毕再执行。从命名可以解读到countdown是倒数的意思,类似于我们倒计时的概念。

  countdownlatch提供了两个方法,一个是countDown(),一个是await(), countdownLatch 初始化的时候需要传入一个整数,在这个整数倒数到0之前,调用了await方法的程序都必须要等待,然后通过countDown() 来倒数。

  Demo :

public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(3);
System.out.println("计数器为"+countDownLatch.getCount());
new Thread(() -> {
countDownLatch.countDown();
System.out.println(Thread.currentThread().getName() +"执行完毕,计数器为"+countDownLatch.getCount());
}, "t1").start();
new Thread(() -> {
countDownLatch.countDown();
System.out.println(Thread.currentThread().getName() +"执行完毕,计数器为"+countDownLatch.getCount());
}, "t2").start();
new Thread(() -> {
countDownLatch.countDown();
System.out.println(Thread.currentThread().getName() +"执行完毕,计数器为"+countDownLatch.getCount());
}, "t3").start();
countDownLatch.await();//阻塞
System.out.println("所有线程执行完毕");
}
}

  执行结果 :

Java并发编程-并发工具类及线程池

  从代码的实现来看,有点类似join的功能,但是比join更加灵活。CountDownLatch构造函数会接收一个int类型的参数作为计数器的初始值,当调用CountDownLatch的countDown方法时,这个计数器就会减一。通过await方法去阻塞主流程:

Java并发编程-并发工具类及线程池

  CountDownLatch类存在一个内部类Sync,它是一个同步工具,一定继承了AbstractQueuedSynchronizer。很显然,CountDownLatch实际上是是使得线程阻塞了,既然涉及到阻塞,就一定涉及到AQS队列。await() 函数会使得当前线程在countdownLatch倒计时到0之前一直等待,除非线程中断;从源码中可以得知await方法会转发到Sync的acquireSharedInterruptibly。

  acquireSharedInterruptibly():

  这块代码主要是判断当前线程是否获取到了共享锁,AQS有两种锁类型,一种是共享锁,一种是独占锁,在这里用的是共享锁; 为什么要用共享锁,因为CountDownLatch可以多个线程同时通过。

public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
  if (Thread.interrupted()) //判断线程是否中断
    throw new InterruptedException();
  if (tryAcquireShared(arg) < 0) //如果等于0则返回1,否则返回-1,返回-1表示需要阻塞
    doAcquireSharedInterruptibly(arg);
}
//在这里,state的意义是count,如果计数器为0,表示不需要阻塞,否则,只有在满足条件的情况下才会被唤醒
protected int tryAcquireShared(int acquires) {
  return (getState() == 0) ? 1 : -1;
}

  doAcquireSharedInterruptibly():获取共享锁

private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
  final Node node = addWaiter(Node.SHARED); //创建一个共享模式的节点添加到队列中
  boolean failed = true;
  try {
    for (;;) { //自旋等待共享锁释放,也就是等待计数器等于0。
      final Node p = node.predecessor(); //获得当前节点的前一个节点
      if (p == head) {
        int r = tryAcquireShared(arg);//就判断尝试获取锁,判断计数器是否归零
        if (r >= 0) {//r>=0表示计数器已经归零了,则释放当前的共享锁
          setHeadAndPropagate(node, r);
          p.next = null; // help GC
          failed = false;
          return;
        }
      }
      //当前节点不是头节点,则尝试让当前线程阻塞,第一个方法是判断是否需要阻塞,第二个方法是阻塞
      if (shouldParkAfterFailedAcquire(p, node) &&
        parkAndCheckInterrupt())
          throw new InterruptedException();
    }
  } finally {
    if (failed)
      cancelAcquire(node);
  }
}

  setHeadAndPropagate(Node node, int propagate):

private void setHeadAndPropagate(Node node, int propagate) {
  Node h = head; // 记录头节点
  setHead(node); //设置当前节点为头节点
  //前面传过来的propagate是1,所以会进入下面的代码
  if (propagate > 0 || h == null || h.waitStatus < 0 ||
    (h = head) == null || h.waitStatus < 0) {
    Node s = node.next; //获得当前节点的下一个节点,如果下一个节点是空表示当前节点为最后一个节点,或者下一个节点是share节点
    if (s == null || s.isShared())
      doReleaseShared(); //唤醒下一个共享节点
  }
}

  doReleaseShared():释放共享锁,通知后面的节点

private void doReleaseShared() {
  for (;;) {
    Node h = head; //获得头节点
    if (h != null && h != tail) { //如果头节点不为空且不等于tail节点
      int ws = h.waitStatus;
      if (ws == Node.SIGNAL) { //头节点状态为SIGNAL,
        if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) //修改当前头节点的状态为0,避免下次再进入到这个里面
          continue; // loop to recheck cases
        unparkSuccessor(h); //释放后续节点
      }
      else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
        continue; // loop on failed CAS
      }
      if (h == head) // loop if head changed
        break;
    }
}

  countdown(): 以共享模式释放锁,并且会调用tryReleaseShared函数,根据判断条件也可能会调用doReleaseShared函数

public final boolean releaseShared(int arg) {
  if (tryReleaseShared(arg)) { //如果为true,表示计数器已归0了
    doReleaseShared(); //唤醒处于阻塞的线程
    return true;
  }
  return false;
}

  tryReleaseShared():这里主要是对state做原子递减,其实就是我们构造的CountDownLatch的计数器,如果等于0返回 false,也就是意味着已经不需要做doReleaseShared 了。当计数器倒数到最后1个的时候 运行到本方法后会返回 true,然后进入释放锁

protected boolean tryReleaseShared(int releases) {
  // Decrement count; signal when transition to zero
  for (;;) {
    int c = getState();
    if (c == 0)
      return false;
    int nextc = c-1;
    if (compareAndSetState(c, nextc))
      return nextc == 0;
  }
}

Semaphore(信号量):

  semaphore也就是我们常说的信号灯,semaphore可以控制同时访问的线程个数,通过acquire获取一个许可,如果没有就等待,通过release释放一个许可。有点类似限流的作用。叫信号灯的原因也和他的用处有关,比如某商场就5个停车位,每个停车位只能停一辆车,如果这个时候来了10辆车,必须要等前面有空的车位才能进入。先来看一下一个案例,简单实现一下限流的功能:

public class SemaphoreDemo {

	public static void main(String[] args) {
Semaphore semaphore = new Semaphore(5);
for (int i = 0; i < 10; i++) {
new Car(i, semaphore).start();
}
} static class Car extends Thread {
private int num;
private Semaphore semaphore; public Car(int num, Semaphore semaphore) {
this.num = num;
this.semaphore = semaphore;
} public void run() {
try {
semaphore.acquire();// 获取一个许可
System.out.println("第" + num + "占用一个停车位");
TimeUnit.SECONDS.sleep(2);
System.out.println("第" + num + "俩车走喽");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

  semaphore也是基于AQS来实现的,内部使用state表示许可数量;它的实现方式和CountDownLatch的差异点在于acquireSharedInterruptibly中的tryAcquireShared方法的实现,这个方法是在Semaphore方法中重写的,其他的源码都是一样的,根本原理还是基于AQS队列,然后基于lockSupport 调用unsafe 的底层操作对线程进行 park/unpark 操作。

  在semaphore中存在公平和非公平的方式,和重入锁是一样的,如果通过FairSync表示公平的信号量、NonFairSync表示非公平的信号量;公平和非公平取决于是否按照FIFO队列中的顺序去分配Semaphore所维护的许可,非公平锁通过自选的方式,类似于synchronized底层偏向锁的意思,去抢占执行权限,而公平锁的实现中则是判断是否为头节点,只有头节点才能去抢占。

原子操作:

  当在多线程情况下,同时更新一个共享变量,由于我们前面讲过的原子性问题,可能得不到预期的结果。如果要达到期望的结果,可以通过synchronized来加锁解决,因为synchronized会保证多线程对共享变量的访问进行排队。

  在Java5以后,提供了原子操作类,这些原子操作类提供了一种简单、高效以及线程安全的更新操作。而由于变量的类型很多,所以Atomic一共提供了12个类分别对应四种类型的原子更新操作,基本类型、数组类型、引用类型、属性类型

基本类型对应:AtomicBoolean、AtomicInteger、AtomicLong

数组类型对应:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

引用类型对应:AtomicReference、AtomicReferenceFieldUpdater、AtomicMarkableReference

字段类型对应:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicStampedReference

  基本操作案例:经过原子操作最后会输出 1000.

public class AtomicDemo {

	private static AtomicInteger count = new AtomicInteger(0);

	public static synchronized void inc() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count.getAndIncrement();
} public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
AtomicDemo.inc();
}).start();
}
Thread.sleep(4000);
System.out.println(count.get());
}
}

  AtomicInteger实现原理:由于所有的原子操作类都是大同小异的,所以我们只分析其中一个原子操作类

public final int getAndIncrement() {
  return unsafe.getAndAddInt(this, valueOffset, 1);
}

  又是熟悉的 unsafe ,提供了一些底层操作如直接内存访问、线程调度等,然后调用unsafe类中的getAndAddInt方法,这个方法如下:

public final int getAndAddInt(Object var1, long var2, int var4) {
  int var5;
  do {
    var5 = this.getIntVolatile(var1, var2);// 方法获取对象中offset偏移地址对应的整型field的值
  } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  return var5;
}

  通过循环以及cas的方式实现原子更新,从而达到在多线程情况下仍然能够保证原子性的目的。

线程池:

  Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。线程池就像数据库连接池的作用类似,只是线程池是用来重复管理线程避免创建大量线程增加开销。所以合理的使用线程池可以

1. 降低创建线程和销毁线程的性能开销

2. 合理的设置线程池大小可以避免因为线程数超出硬件资源瓶颈带来的问题,类似起到了限流的作用;线程是稀缺资源,如果无线创建,会造成系统稳定性问题线程池的使用

  JDK 为我们内置了几种常见线程池的实现,均可以使用 Executors 工厂类创建为了更好的控制多线程,JDK提供了一套线程框架Executor,帮助开发人员有效的进行线程控制。它们都在

java.util.concurrent包中,是JDK并发包的核心。其中有一个比较重要的类:Executors,他扮演着线程工厂的角色,我们通过Executors可以创建特定功能的线程池

1.newFixedThreadPool:该方法返回一个固定数量的线程池,线程数不变,当有一个任务提交时,若线程池中空闲,则立即执行,若没有,则会被暂缓在一个任务队列中,等待有空闲的线程去执行。

  FixedThreadPool 的核心线程数和最大线程数都是指定值,也就是说当线程池中的线程数超过核心线程数后,任务都会被放到阻塞队列中。另外 keepAliveTime 为 0,也就是超出核心线程数量以外的线程空余存活时间而这里选用的阻塞队列是 LinkedBlockingQueue,使用的是默认容量 Integer.MAX_VALUE,相当于没有上限这个线程池执行任务的流程如下:

\1. 线程数少于核心线程数,也就是设置的线程数时,新建线程执行任务

\2. 线程数等于核心线程数后,将任务加入阻塞队列

\3. 由于队列容量非常大,可以一直添加

\4. 执行完任务的线程反复去队列中取任务执行

用途:FixedThreadPool 用于负载比较大的服务器,为了资源的合理利用,需要限制当前线程数量

2.newSingleThreadExecutor: 创建一个线程的线程池,若空闲则执行,若没有空闲线程则暂缓在任务队列中。

  创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行.

3.newCachedThreadPool:返回一个可根据实际情况调整线程个数的线程池,不限制最大线程数量,若用空闲的线程则执行任务,若无任务则不创建线程。并且每一个空闲线程会在60秒后自动回收

  CachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程; 并且没有核心线程,非核心线程数无上限,但是每个空闲的时间只有 60

秒,超过后就会被回收。它的执行流程如下:

\1. 没有核心线程,直接向 SynchronousQueue 中提交任务

\2. 如果有空闲线程,就去取出任务执行;如果没有空闲线程,就新建一个

\3. 执行完任务的线程有 60 秒生存时间,如果在这个时间内可以接到新任务,就可以继续活下去,否则就被回收

4.newScheduledThreadPool: 创建一个可以指定线程的数量的线程池,但是这个线程池还带有延迟和周期性执行任务的功能,类似定时器。

submit和execute的区别:

  执行一个任务,可以使用submit和execute,这两者有什么区别呢?

\1. execute只能接受Runnable类型的任务

\2. submit不管是Runnable还是Callable类型的任务都可以接受,但是Runnable返回值均为void,所以使用Future的get()获得的还是null

  在 https://www.cnblogs.com/wuzhenzhao/p/9928639.html 中我介绍了各种线程池的使用案例及使用场景,前面说的四种线程池构建工具,都是基于ThreadPoolExecutor 类,看看它的构造函数参数:

public ThreadPoolExecutor(int corePoolSize, //核心线程数量
  int maximumPoolSize, //最大线程数
  long keepAliveTime, //超时时间,超出核心线程数量以外的线程空余存活时间
  TimeUnit unit, //存活时间单位
  BlockingQueue<Runnable> workQueue, //保存执行任务的队列
  ThreadFactory threadFactory,//创建新线程使用的工厂
  RejectedExecutionHandler handler //当任务无法执行的时候的处理方式
) {
  this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
  Executors.defaultThreadFactory(), defaultHandler);
}

  ThreadPoolExecutor是线程池的核心,提供了线程池的实现。ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,并另外提供一些调度方法以支持定时和周期任务。Executers是工具类,主要用来创建线程池对象我们把一个任务提交给线程池去处理的时候,线程池的处理过程是什么样的呢?

  线程池用一个AtomicInteger来保存 [线程数量] 和 [线程池状态] ,一个int数值一共有32位,高3位用于保存运行状态,低29位用于保存线程数量,下面是他的一些成员变量属性的定义:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); //一个原子操作类
private static final int COUNT_BITS = Integer.SIZE - 3; //32-3
private static final int CAPACITY = (1 << COUNT_BITS) - 1; //将1的二进制向右位移29位,再减1表示最大线程容量
//运行状态保存在int值的高3位 (所有数值左移29位)
private static final int RUNNING = -1 << COUNT_BITS;// 接收新任务,并执行队列中的任务
private static final int SHUTDOWN = 0 << COUNT_BITS;// 不接收新任务,但是执行队列中的任务
private static final int STOP = 1 << COUNT_BITS;// 不接收新任务,不执行队列中的任务,中断正在执行中的任务
private static final int TIDYING = 2 << COUNT_BITS; //所有的任务都已结束,线程数量为0,处于该状态的线程池即将调用terminated()方法
private static final int TERMINATED = 3 << COUNT_BITS;// terminated()方法执行完成
// Packing and unpacking ctl
private static int runStateOf(int c) { return c & ~CAPACITY; } //获取运行状态
private static int workerCountOf(int c) { return c & CAPACITY; } //获取线程数量

  先看一下 execute 的源码:

public void execute(Runnable command) {
  if (command == null)
    throw new NullPointerException();
  int c = ctl.get();
  if (workerCountOf(c) < corePoolSize) {//1.当前池中线程比核心数少,新建一个线程执行任务
    if (addWorker(command, true))
      return;
    c = ctl.get();
  }
  if (isRunning(c) && workQueue.offer(command)) {//2.核心池已满,但任务队列未满,添加到队列中
    int recheck = ctl.get();
    //任务成功添加到队列以后,再次检查是否需要添加新的线程,因为已存在的线程可能被销毁了
    if (! isRunning(recheck) && remove(command))
      reject(command);//如果线程池处于非运行状态,并且把当前的任务从任务队列中移除成功,则拒绝该任务
    else if (workerCountOf(recheck) == 0)//如果之前的线程已被销毁完,新建一个线程
      addWorker(null, false);
  }
  else if (!addWorker(command, false)) //3.核心池已满,队列已满,试着创建一个新线程
    reject(command); //如果创建新线程失败了,说明线程池被关闭或者线程池完全满了,拒绝任务
}

  这里的主要逻辑用流程图表示如下:

Java并发编程-并发工具类及线程池

  创建核心线程的时候是通过 addWorker(command, true) 方法把当前需要执行的任务放进去,通过构造一个 Worker 对象,先是通过双层的自旋循化去判断线程的状态及当前核心线程的数量,最后通过CAS操作递增了线程计器compareAndIncrementWorkerCount(c),最后调用start方法启动一个线程,如果核心线程数满了,则通过判断线程是否运行,然后将任务丢进 workQueue,然后判断是否超出最大线程数跟是否允许被执行,在前面的任务执行完的时候他就将被执行。

  这里的饱和策略 reject 有4个基本实现:

Java并发编程-并发工具类及线程池

  从上至下分别是:

1.该策略是线程池的默认策略。使用该策略时,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。

2.这个策略和AbortPolicy的slient版本,如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常。

3. 这个策略从字面上也很好理解,丢弃最老的。也就是说如果队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列。因为队列是队尾进,队头出,所以队头元素是最老的,因此每次都是移除对头元素后再尝试入队。

4.使用此策略,如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行。就像是个急脾气的人,我等不到别人来做这件事就干脆自己干。