并发编程从入门到放弃系列开始和结束(四)

wg204wg
发布于 2022-6-13 17:41
浏览
0收藏

 

CyclicBarrier
CyclicBarrier叫做回环屏障,它的作用是让一组线程全部达到一个状态之后再全部同时执行,他和CountDownLatch主要区别在于,CountDownLatch的计数器只能用一次,而CyclicBarrier的计数器状态则是可以一直重用的。

我们可以使用CyclicBarrier一样实现上面的需求。

public class CyclicBarrierTest {
    public static void main(String[] args) throws Exception{
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        CyclicBarrier cyclicBarrier = new CyclicBarrier(2, () -> {
            System.out.println("开始打包发送数据..");
        });
        executorService.submit(()->{
            try {
                Thread.sleep(1000);
                System.out.println("写excelA完成");
                cyclicBarrier.await();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });

        executorService.submit(()->{
            try {
                Thread.sleep(3000);
                System.out.println("写excelB完成");
                cyclicBarrier.await();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });
        System.out.println("等待excel写入完成");
        executorService.shutdown();

    }
}
//输出
等待excel写入完成
写excelA完成
写excelB完成
开始打包发送数据..

初始化的时候我们传入2个线程和一个回调方法,线程调用await()之后进入阻塞状态并且计数器-1,这个阻塞点被称作为屏障点或者同步点,只有最后一个线程到达屏障点的时候,所有被屏障拦截的线程才能继续运行,这也是叫做回环屏障的名称原因。

而当计数器为0时,就去执行CyclicBarrier构造函数中的回调方法,回调方法执行完成之后,就会退出屏障点,唤醒其他阻塞中的线程。

CyclicBarrier基于ReentrantLock实现,本质上还是基于AQS实现的,内部维护parties记录总线程数,count用于计数,最开始count=parties,调用await()之后count原子递减,当count为0之后,再次将parties赋值给count,这就是复用的原理。

  1. 当子线程调用await()方法时,获取独占锁ReentrantLock,同时对count递减,进入阻塞队列,然后释放锁
  2. 当第一个线程被阻塞同时释放锁之后,其他子线程竞争获取锁,操作同1
  3. 直到最后count为0,执行CyclicBarrier构造函数中的任务,执行完毕之后子线程继续向下执行,计数重置,开始下一轮循环
    Semaphore
    Semaphore叫做信号量,和前面两个不同的是,他的计数器是递增的,信号量这玩意儿在限流中就经常使用到。
public class SemaphoreTest {

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        Semaphore semaphore = new Semaphore(0);
        executorService.submit(() -> {
            try {
                Thread.sleep(1000);
                System.out.println("写excelA完成");
                semaphore.release();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        executorService.submit(() -> {
            try {
                Thread.sleep(3000);
                System.out.println("写excelB完成");
                semaphore.release();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        System.out.println("等待excel写入完成");
        semaphore.acquire(2);
        System.out.println("开始打包发送数据..");

        executorService.shutdown();
    }
}
//输出
等待excel写入完成
写excelA完成
写excelB完成
开始打包发送数据..

稍微和前两个有点区别,构造函数接受参数表示可用的许可证的数量,acquire方法表示获取一个许可证,使用完之后release归还许可证。

当子线程调用release()方法时,计数器递增,主线程acquire()传参为2则说明主线程一直阻塞,直到计数器为2才会返回。

Semaphore还还还是基于AQS实现的,同时获取信号量有公平和非公平两种策略,通过构造函数的传参可以修改,默认则是非公平的策略。

  1. 先说非公平的策略,主线程调用acquire()方法时,用当前信号量值-需要获取的值,如果小于0,说明还没有达到信号量的要求值,则会进入AQS的阻塞队列,大于0则通过CAS设置当前信号量为剩余值,同时返回剩余值。而对于公平策略来说,如果当前有其他线程在等待获取资源,那么自己就会进入AQS阻塞队列排队。
  2. 子线程调用release()给当前信号量值计数器+1(增加的值数量由传参决定),同时不停的尝试唤醒因为调用acquire()进入阻塞的线程
    Exchanger
    Exchanger用于两个线程之间交换数据,如果两个线程都到达同步点,这两个线程可以互相交换他们的数据。

举个栗子,A和B两个线程需要交换他们自己写的数据以便核对数据是否一致。

public class ExchangerTest {
    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        Exchanger<String> exchanger = new Exchanger<>();
        executorService.submit(() -> {
            try {
                Thread.sleep(1000);
                System.out.println("写excelA完成");
                System.out.println("A获取到数据=" + exchanger.exchange("excelA"));
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        executorService.submit(() -> {
            try {
                Thread.sleep(3000);
                System.out.println("写excelB完成");
                System.out.println("B获取到数据=" + exchanger.exchange("excelB"));

            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        executorService.shutdown();

    }
}
//输出
写excelA完成
写excelB完成
B获取到数据=excelA
A获取到数据=excelB

A写完之后exchange会一直阻塞等待,直到另外一个线程也exchange之后,才会继续执行。

ThreadLocalRandom
通常我们都会用 Random 去生成随机数,但是 Random 有点小问题,在多线程并发的情况下为了保证生成的随机性,通过 CAS 的方式保证生成新种子的原子性,但是这样带来了性能的问题,多线程并发去生成随机数,但是只有一个线程能成功,其他的线程会一直自旋,性能不高,所以 ThreadLocalRandom 就是为了解决这个问题而诞生。

//多线程下通过CAS保证新种子生成的原子性
protected int next(int bits) {
        long oldseed, nextseed;
        AtomicLong seed = this.seed;
        do {
            oldseed = seed.get();
            nextseed = (oldseed * multiplier + addend) & mask;
        } while (!seed.compareAndSet(oldseed, nextseed));
        return (int)(nextseed >>> (48 - bits));
}

ThreadLocalRandom 我们从名字就能看出来,肯定使用了 ThreadLocal,作用就是用 ThreadLocal 保存每个种子的变量,防止在高并发下对同一个种子的争夺。

使用也非常简单:

 ThreadLocalRandom.current().nextInt(100);

看下源码实现,current 方法获取当前的 ThreadLocalRandom 实例。

public static ThreadLocalRandom current() {
        if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
            localInit();
        return instance;
}

nextInt 方法和 Random 看起来差不多,上面是生成新的种子,下面是固定的基于新种子计算随机数,主要看 nextSeed。

public int nextInt(int bound) {
    if (bound <= 0)
        throw new IllegalArgumentException(BadBound);
    int r = mix32(nextSeed()); //生成新种子
    int m = bound - 1;
    if ((bound & m) == 0) // power of two
        r &= m;
    else { // reject over-represented candidates
        for (int u = r >>> 1;
             u + m - (r = u % bound) < 0;
             u = mix32(nextSeed()) >>> 1)
            ;
    }
    return r;
}

r = UNSAFE.getLong(t, SEED) + GAMMA 计算出新的种子,然后使用 UNSAFE 的方法放入当前线程中。

final long nextSeed() {
    Thread t; long r; // read and update per-thread seed
    UNSAFE.putLong(t = Thread.currentThread(), SEED,
                   r = UNSAFE.getLong(t, SEED) + GAMMA);
    return r;
}

ConcurrentHashMap
这个我们就不说了,说的太多了,之前的文章也写过了,可以参考之前写过的。

CopyOnWriteArrayList&CopyOnWriteArraySet
这是线程安全的 ArrayList ,从名字我们就能看出来,写的时候复制,这叫做写时复制,也就是写的操作是对拷贝的数组的操作。

先看构造函数,有3个,分别是无参,传参为集合和传参数组,其实都差不多,无参构造函数创建一个新的数组,集合则是把集合类的元素拷贝到新的数组,数组也是一样。

public CopyOnWriteArrayList() {
  setArray(new Object[0]);
}

public CopyOnWriteArrayList(Collection<? extends E> c) {
  Object[] elements;
  if (c.getClass() == CopyOnWriteArrayList.class)
    elements = ((CopyOnWriteArrayList<?>)c).getArray();
  else {
    elements = c.toArray();
    if (c.getClass() != ArrayList.class)
      elements = Arrays.copyOf(elements, elements.length, Object[].class);
  }
  setArray(elements);
}

public CopyOnWriteArrayList(E[] toCopyIn) {
  setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

我们看 add 方法,你一眼就能看出来非常简单的实现,通过 ReentrantLock 加锁,然后拷贝出一个新的数组,数组长度+1,再把新数组赋值,所以这就是名字的由来,写入的时候操作的是数组的拷贝,其他的删除修改就不看了,基本上是一样的。

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

再看看 get 方法,也非常简单,直接获取数组当前索引的值,这里需要注意的是,读数据是没有加锁的,所以会有一致性的问题,它并不能保证读到的一定是最新的数据。

public E get(int index) {
    return get(getArray(), index);
}

private E get(Object[] a, int index) {
 return (E) a[index];
}

final Object[] getArray() {
 return array;
}

至于 CopyOnWriteArraySet ,他就是基于 CopyOnWriteArrayList 实现的,这里我们不再赘述。

public CopyOnWriteArraySet() {
    al = new CopyOnWriteArrayList<E>();
}
public boolean add(E e) {
   return al.addIfAbsent(e);
}
public boolean addIfAbsent(E e) {
   Object[] snapshot = getArray();
   return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
   addIfAbsent(e, snapshot);
}

 

文章转自公众号:艾小仙

分类
标签
已于2022-6-13 17:41:39修改
收藏
回复
举报
回复
    相关推荐