并发工具类

CountDownLatch

CountDownLatch是一个同步工具类,它允许一个或者多个线程一直等待,知道其他线程的操作执行完毕再执行。

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

public static void main(String[] args) throws InterruptedException {

  final CountDownLatch countDownLatch = new CountDownLatch(4);

  new Thread(new Runnable() {
    @Override
    public void run() {
      System.out.println("" + Thread.currentThread().getName() + "-执行中");
      countDownLatch.countDown();
      System.out.println("" + Thread.currentThread().getName() + "-执行完毕");
    }
  }, "t1").start();

  new Thread(new Runnable() {
    @Override
    public void run() {
      System.out.println("" + Thread.currentThread().getName() + "-执行中");
      countDownLatch.countDown();
      System.out.println("" + Thread.currentThread().getName() + "-执行完毕");
    }
  }, "t2").start();

  new Thread(new Runnable() {
    @Override
    public void run() {
      System.out.println("" + Thread.currentThread().getName() + "-执行中");
      countDownLatch.countDown();
      System.out.println("" + Thread.currentThread().getName() + "-执行完毕");
    }
  }, "t3").start();

  countDownLatch.await();
  System.out.println("所有线程已经执行完毕");
}

从代码实现看,类似join的功能,但是比join更加灵活。CountDownLatch构造函数会接受一个int类型的参数作为计数器的初始值,当调用CountDownLatch的countDown方法时,这和计数器就会减一。

模拟高并发场景

static CountDownLatch countDownLatch = new CountDownLatch(1);
public static void main(String[] args) {

  for (int i = 0; i < 1000; i++) {
    new Thread(new CountDownLatchDemo()).start();
  }
  countDownLatch.countDown();
}

@Override
public void run() {
  try {
    countDownLatch.await();
  }catch (InterruptedException e){
    e.printStackTrace();
  }
  System.out.println("ThreadName:"+Thread.currentThread().getName());
}

总的来说,凡是涉及到需要指定某个任务再执行之前,要等到前置任务执行完毕之后才能执行的场景,都可以使用CountDownLatch。

CountDownLatch源码解析

对于countDownLatch,只要有await()方法和countDown()方法。

countDown()方法每次调用都会将state减1,直到state的值为0;而await是一个阻塞方法,当state减为0的时候,await方法才会返回。await可以被多个线程调用。所有调用了await方法的线程阻塞在AQS的紫色队列来,条件满足(state==0),将线程从队列中一个个唤醒过来。

acquireSharedInterruptibly

countDownLatch也使用到AQS,在CountDownLatch内部写了一个Sync并且继承了AQS这个抽象类重写了AQS中的共享锁方法。如下代码,这块代码只要是判断当前线程是否获取到了共享锁;(在CountDownLatch中,使用的是共享锁机制,因为CountDownLatch并不需要实现互斥的特性)

public final void acquireSharedInterruptibly(int arg)
  throws InterruptedException {
  if (Thread.interrupted())
    throw new InterruptedException();
  //state如果不等于0,说明当前线程需要加入带共享锁队列
  if (tryAcquireShared(arg) < 0)
    doAcquireSharedInterruptibly(arg);
}

protected int tryAcquireShared(int acquires) {
  return (getState() == 0) ? 1 : -1;
}

doAcquireSharedInterruptibly

1.addWaiter设置为shared模式

2.tryAcquire和tryAcquireShared的返回值不同,因此会多出一个判断过程

3.在判断前驱结点是头结点后,调用了setHeadAndPropagate方法,而不是简单地更新了一下头结点

private void doAcquireSharedInterruptibly(int arg)
  throws InterruptedE
  //创建一个共享模式的节点添加到队列中
  final Node node = addWaiter(Node.SHARED);
  boolean failed = true;
  try {
    for (;;) {
      final Node p = node.predecessor();
      if (p == head) {
        //判断尝试获得锁
        int r = tryAcquireShared(arg);
        //r>=0表示获取到了执行权限,这个时候state!=0,所以不会执行这段代码
        if (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);
  }
}

图解分析

加入这个时候有3个线程调用了await方法,由于这个时候state的值还不为0,所以这三个线程都会加入到AQS队列中。并且三个线程都处于阻塞状态。

CountDownLatch.countDown

由于线程被await方法阻塞了,所以只有等到countDown方法使得state=0的时候才会被唤醒。

1.只有当state减为0的时候,tryReleaseShared才会返回true,否则只是简单的state=state-1

2.如果state=0,则调用doReleaseShared唤醒处于await状态下的线程

public final boolean releaseShared(int arg) {
  if (tryReleaseShared(arg)) {
    doReleaseShared();
    return true;
  }
  return false;
}

//用自旋的方式实现state减1
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;
  }
}

AQS.doReleaseShared

共享锁的释放和独占锁的释放有一定的差别,前面唤醒锁的逻辑和独占锁是一样的,先判断头结点是不是SIGNAL状态,如果是,则修改为0,并且唤醒头结点的而下一个节点

private void doReleaseShared() {
  for (;;) {
    Node h = head;
    if (h != null && h != tail) {
      int ws = h.waitStatus;
      if (ws == Node.SIGNAL) {
        if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
          continue;            // loop to recheck cases
        unparkSuccessor(h);
      }
      //这个CAS失败的场景是:执行到这里的时候,刚好有一个节点入队,入队会将这个ws设置为-1
      else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
        continue;                // loop on failed CAS
    }
    //如果当到这里的时候,前面唤醒的线程已经占了了head,那么再循环
    //通过检查头节点是否改变了,如果改变了就继续循环
    if (h == head)                   // loop if head changed
      break;
  }
}
PROPAGATE:标识为PROPAGATE状态的节点,是共享模式下的节点状态,处于这个状态下的节点,会对县城内的唤醒进行传播

h==head:说明头节点还没有被刚刚用unparkSuccessor唤醒的线程(这里可以理解为ThreadB)占有,是break退出循环。

h!=head:头节点被刚刚唤醒的线程(这里可以理解为ThreadB)占有,那么这里重新进入下一轮玄幻,唤醒下一个节点(这里是ThreadB)。然后后面唤醒传递。。

一旦ThreadA被唤醒,代码又回到了doAcquireSharedInterruptibly中来执行。如果当前state满足等于0的条件,则会执行setHeadAndPropagate方法

if (p == head) {
  //判断尝试获得锁
  int r = tryAcquireShared(arg);
  if (r >= 0) {
    setHeadAndPropagate(node, r);
    p.next = null; // help GC
    failed = false;
    return;
  }
}

setHeadAndPropagate

这个方法主要作用是把被唤醒的节点,设置成head节点。然后继续唤醒队列中的其他线程。

由于现在队列有3个线程处于阻塞状态,一旦ThreadA被唤醒,并且设置为head之后,会继续唤醒后续的ThreadB。

private void setHeadAndPropagate(Node node, int propagate) {
  Node h = head; // Record old head for check below
  setHead(node);
  if (propagate > 0 || h == null || h.waitStatus < 0 ||
      (h = head) == null || h.waitStatus < 0) {
    Node s = node.next;
    if (s == null || s.isShared())
      doReleaseShared();
  }
}

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);
                semaphore.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

使用场景

Semaphore比较常见的就是用来作限流操作。

Semaphore源码分析

从Semaphore的功能来看,我们可以猜测它的底层原理一定是基于AQS的共享锁。

创建Semaphore实例的时候,需要一个参数permits,这个基本上可以确定是设置给AQS的state的,然后每个线程调用acquire的时候,执行state=state-1,release的时候执行state=state+1,当然,acquire的时候,如果state=0,说明没有资源了,需要等待其他的线程release。

Semaphore分公平策略和非公平策略

FairSync

static final class FairSync extends Sync {
  private static final long serialVersionUID = 2014338818796000944L;

  FairSync(int permits) {
    super(permits);
  }

  protected int tryAcquireShared(int acquires) {
    for (;;) {
      //区别就在于是不是会先判断是否有线程在排队,然后才进行CAS键操作
      if (hasQueuedPredecessors())
        return -1;
      int available = getState();
      int remaining = available - acquires;
      if (remaining < 0 ||
          compareAndSetState(available, remaining))
        return remaining;
    }
  }
}

NoFairSync

通过对别发现公平锁和非公平锁的区别就是在于是否多了一个hasQueuedPredecessors的判断

static final class NonfairSync extends Sync {
  private static final long serialVersionUID = -2694183684443567898L;

  NonfairSync(int permits) {
    super(permits);
  }

  protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
  }
}

final int nonfairTryAcquireShared(int acquires) {
  for (;;) {
    int available = getState();
    int remaining = available - acquires;
    if (remaining < 0 ||
        compareAndSetState(available, remaining))
      return remaining;
  }
}
//都是基于共享锁来实现的

CyclicBarrier

CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。他要做的事情是,让一组线程到达一个屏障(也可以佳作同步点)时被阻塞,知道最后一个线程到达平衡住那个是,屏障才会开门,所有被屏障拦截的线程才会继续工作。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier当前线程已经到达了屏障,然后当前线程被阻塞。

使用场景

当存在需要所有的子任务都完成时,才会执行主任务,这个时候就可以选择使用CyclicBarrier。

案例

DataImportThread

public class DataImportThread extends Thread{


    private CyclicBarrier cyclicBarrier;

    private String path;

    public DataImportThread(CyclicBarrier cyclicBarrier,String path){
        this.cyclicBarrier = cyclicBarrier;
        this.path = path;
    }

    @Override
    public void run() {
        System.out.println("开始导入:"+path+"位置的数据");
        try {
            cyclicBarrier.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}

CyclicBarrierDemo

public class CyclicBarrierDemo extends Thread{

    @Override
    public void run() {

        System.out.println("开始进行数据分析");
    }

    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3,new CyclicBarrierDemo());
        new Thread(new DataImportThread(cyclicBarrier,"file1")).start();
        new Thread(new DataImportThread(cyclicBarrier,"file2")).start();
        new Thread(new DataImportThread(cyclicBarrier,"file3")).start();
    }
}

注意点:

1)对于制定计数值parties。若由于某种原因,没有足够的线程调用CyclicBarrier的await,则所有调用await的线程都会被阻塞。

2)同样的CyclicBarrier也可以调用await(timeout, unit),设置超时时间,在设定时间内,如果没有足够线程到达,则解除阻塞状态,继续工作。

3)通过reset重置计数,会使得进入await的线程出现BrokenBarrierExecption;

4)如果采用是CyclicBarrier(int parteis, Runnable barrierAction)构造方法,执行barrierAction操作的是最后一个到达的线程。

实现原理

CyclicBarrier相比CountDownLatch来说,要简单很多,源码实现是基于ReentrantLock和Condition的组合使用。如下图,CyclicBarrier和CountDownLatch是不是很像,只是CyclicBarrier可以不止一个栅栏,因为他的栅栏(Barrier)可以重复使用。

技术分享图片

posted @ 2020-08-27 22:48  snail灬  阅读(157)  评论(0编辑  收藏  举报