并发工具类
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)可以重复使用。

                    
                
                
            
        
浙公网安备 33010602011771号