Java之并发编程(三)


五、Java 常见并发容器总结

1.ConcurrentHashMap

ConcurrentHashMap : 线程安全的 HashMap

1.1 Collections.synchronizedMap()

并发时使用它方法包装HashMap同步,这属于全局锁,性能低下。

1.2 ConcurrentHashMap

读写操作都能保证很高的性能

(1)在进行读操作时(几乎)不需要加锁

(2)写操作时通过锁分段技术,只对所操作的段加锁,而不影响客户端对其他段的访问。

2.CopyOnWriteArrayList

线程安全的 List,在读多写少的场合性能非常好,远远好于 Vector。

2.1 读完全不加锁,写写加锁

写入也不会堵塞读取操作,只有写入和写入之间需要进行同步等待

2.2 如何实现?

所有可变操作(add,set 等等)都是通过创建底层数组的新副本来实现的。

(1)当需要修改时,并不修改原来数据,而是复制一份副本并对副本修改

(2)修改完完后,副本替换原来的数据

2.3 读取和写入源码简单分析

2.3.1 读取

读取操作没有任何同步控制和锁操作,理由就是内部数组 array 不会发生修改,只会被另外一个 array 替换,因此可以保证数据安全。

   /** The array, accessed only via getArray/setArray. */
    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;
    }

2.3.2 写入

写入操作 add()方法在添加集合的时候加了锁,保证了同步,避免了多线程写的时候会 copy 出多个副本出来。

 /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    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();//释放锁
        }
    }

3.ConcurrentLinkedQueue

高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。

(1)主要使用 CAS 非阻塞算法来实现线程安全

(2)适合在对性能要求相对较高,同时对队列的读写存在多个线程同时进行的场景,即如果对队列加锁的成本较高则适合使用无锁的 ConcurrentLinkedQueue 来替代。

4.BlockingQueue

这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。

4.1 应用于“生产者-消费者”,提供了可阻塞的插入和移除的方法。

(1)当队列容器已满,生产者线程会被阻塞,直到队列未满;

(2)当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。

4.2 常见实现类

4.2.1 ArrayBlockingQueue

(1)有界队列实现类,底层采用数组来实现。

(2)并发控制采用可重入锁 ReentrantLock ,读写都需要获取到锁才能进行操作

(3)队列容量满时放入元素队列操作将会阻塞;队列空时中取元素也同样会阻塞。

(4)默认非公平性队列

(5)可改成公平性队列,但会降低吞吐量,如下:

private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10,true);

4.2.2 LinkedBlockingQueue

(1)底层基于单向链表实现的阻塞队列,既可作为有界队列也可作为无界队列

(2)比ArrayBlockingQueue 具有更高的吞吐量

(3)创建时指定大小防止容量速增,降低内存消耗,否则容量为Integer.MAX_VALUE。

构造方法如下:

 /**
     *某种意义上的无界队列
     * Creates a {@code LinkedBlockingQueue} with a capacity of
     * {@link Integer#MAX_VALUE}.
     */
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

    /**
     *有界队列
     * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity.
     *
     * @param capacity the capacity of this queue
     * @throws IllegalArgumentException if {@code capacity} is not greater
     *         than zero
     */
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }

4.2.3 PriorityBlockingQueue

(1)支持优先级的无界阻塞队列,相当于 PriorityQueue 的线程安全版本

(2)默认元素自然排序,可自定义类实现 compareTo() 方法来指定元素排序规则,或者初始化时通过构造器参数 Comparator 来指定排序规则。

(3)并发控制采用可重入锁 ReentrantLock,为无界队列

(4)不可以插入 null,插入队列的对象必须是可比较大小的(comparable),否则报 ClassCastException 异常。

(5)插入操作 put 方法不会 block,因为它是无界队列(take 方法在队列为空的时候会阻塞)。

5.ConcurrentSkipListMap

跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找。

(1)跳表的本质是同时维护了多个链表,并且链表是分层的

(2)最低层的链表维护了跳表内所有的元素,每上面一层链表都是下面一层的子集。

(3)跳跃式搜索:跳表内的所有链表的元素都是排序的。从顶级链表开始查找,如果查找元素大于当前链表中的取值,就会转入下一层链表继续找。

如下图:查找18原来需要遍历18次,现在只需要7次
Java之并发编程(三)_第1张图片
(4)利用空间换时间

(5)与map不同:跳表内所有的元素都是排序的。

六、AQS 详解

AbstractQueuedSynchronizer:抽象队列同步器,构建锁和同步器

基于 AQS的同步器: ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue

Java之并发编程(三)_第2张图片

1.AQS核心思想

2.1 核心思想

2.1.1 如果被请求的共享资源空闲,则将当前线程设置为有效的工作线程,并且将共享资源设置为锁定状态。

2.1.1 如果被请求的共享资源被占用,就将暂时获取不到锁的线程加入到队列中。过程中需要线程阻塞等待、被唤醒时锁分配的机制,该机制用 CLH 队列锁 实现。

(1)CLH(Craig,Landin,and Hagersten) 队列是一个虚拟的双向队列

CLH队列结构如下图:
Java之并发编程(三)_第3张图片
(2)AQS 将每个请求共享资源的线程,封装成CLH 队列的一个结点(Node),来实现锁的分配。

(3)在 CLH 队列中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。


补充:

  • AQS 框架下的锁先通过对状态进行CAS操作(乐观锁)获取锁,减少线程竞争,提高并发性能
  • 但是如果多次CAS操作失败,表明该锁已被占用,就转为悲观锁(如ReentrantLock的Synchronize方式)来等待已获取锁的线程释放锁,减少cpu开销。

2.AQS核心原理

AQS核心原理图:
Java之并发编程(三)_第4张图片

2.1 AQS 使用 int 成员变量 state 表示同步状态,通过内置的线程等待队列来完成获取资源线程的排队工作。

(1)state 变量由 volatile 修饰,用于展示当前临界资源的获锁情况。

// 共享变量,使用volatile修饰保证线程可见性
private volatile int state;

(2)状态信息 state 可以通过 protected 类型的getState()、setState()和compareAndSetState() 进行操作。这几个方法都是 final 修饰的,在子类中无法被重写。

//返回同步状态的当前值
protected final int getState() {
     return state;
}
 // 设置同步状态的值
protected final void setState(int newState) {
     state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
      return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

2.2 举例ReentrantLock

(1)state 初始值为 0,表示未锁定状态。

(2)A 线程 lock() 时,会调用 tryAcquire() 独占该锁并将 state+1

(3)此后,其他线程再 tryAcquire() 时就会失败,直到 A 线程 unlock() 到 state=0(即释放锁)为止,其它线程才有机会获取该锁。

(4)释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。

(5)但是,获取多少次就要释放多少次,这样才能保证 state 是能回到零态的。

2.3 举例CountDownLatch

(1)任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。

(2)这 N 个子线程是并行执行的,每个子线程执行完后countDown() 一次,state 会 CAS(Compare and Swap) 减 1。

(3)等到所有子线程都执行完后(即 state=0 ),会 unpark() 主调用线程,然后主调用线程就会从 await() 函数返回,继续后余动作。

3.AQS 资源共享方式

3.1 2种方式:

(1)Exclusive(独占,只有一个线程能执行,如ReentrantLock)

(2)Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

3.2 方式确定

(1)自定义同步器的共享方式一般是独占、共享的一种,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。

(2)但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

4.自定义同步器

同步器的设计是基于模板方法模式的
(1)使用者继承 AbstractQueuedSynchronizer 并重写指定的方法。

(2)将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。

(3)AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的钩子方法:

//独占方式。尝试获取资源,成功则返回true,失败则返回false。
protected boolean tryAcquire(int)
//独占方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean tryRelease(int)
//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected int tryAcquireShared(int)
//共享方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean tryReleaseShared(int)
//该线程是否正在独占资源。只有用到condition才需要去实现它。
protected boolean isHeldExclusively()

5.常见同步工具类

5.1 Semaphore(信号量)

synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,而Semaphore(信号量)可以用来控制同时访问特定资源的线程数量。

5.1.1 原理

Semaphore 是共享锁的一种实现,它默认构造 AQS 的 state 值为 permits,你可以将 permits 的值理解为许可证的数量,只有拿到许可证的线程才能执行。

5.1.1.1 acquire()

(1)调用semaphore.acquire() ,线程尝试获取许可证

(2)如果 state >= 0 的话,则表示可以获取成功。

(3)如果获取成功的话,使用 CAS 操作去修改 state 的值 state=state-1。

(4)如果 state<0 的话,则表示许可证数量不足。此时会创建一个 Node 节点加入阻塞队列,挂起当前线程。

/**
 *  获取1个许可证
 */
public void acquire() throws InterruptedException {
 	 sync.acquireSharedInterruptibly(1);
}
/**
 * 共享模式下获取许可证,获取成功则返回,失败则加入阻塞队列,挂起线程
 */
public final void acquireSharedInterruptibly(int arg)
    throws InterruptedException {
    if (Thread.interrupted())
      throw new InterruptedException();
        // 尝试获取许可证,arg为获取许可证个数,当可用许可证数减当前获取的许可证数结果小于0,则创建一个节点加入阻塞队列,挂起当前线程。
    if (tryAcquireShared(arg) < 0)
      doAcquireSharedInterruptibly(arg);
}
5.1.1.2 release()

(1)调用semaphore.release(); ,线程尝试释放许可证,并使用 CAS 操作去修改 state 的值 state=state+1。

(2)释放许可证成功之后,同时会唤醒同步队列中的一个线程。

(3)被唤醒的线程会重新尝试去修改 state 的值 state=state-1

(4)如果 state>=0 则获取令牌成功,否则重新进入阻塞队列,挂起线程。

// 释放一个许可证
public void release() {
  	sync.releaseShared(1);
}

// 释放共享锁,同时会唤醒同步队列中的一个线程。
public final boolean releaseShared(int arg) {
    //释放共享锁
    if (tryReleaseShared(arg)) {
      //唤醒同步队列中的一个线程
      doReleaseShared();
      return true;
    }
    return false;
}

5.1.2 实战

/**
 *
 * @author Snailclimb
 * @date 2018年9月30日
 * @Description: 需要一次性拿一个许可的情况
 */
public class SemaphoreExample1 {
  // 请求的数量
  private static final int threadCount = 550;

  public static void main(String[] args) throws InterruptedException {
    // 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢)
    ExecutorService threadPool = Executors.newFixedThreadPool(300);
    // 初始许可证数量
    final Semaphore semaphore = new Semaphore(20);

    for (int i = 0; i < threadCount; i++) {
      final int threadnum = i;
      threadPool.execute(() -> {// Lambda 表达式的运用
        try {
          semaphore.acquire();// 获取一个许可,所以可运行线程数量为20/1=20
          test(threadnum);
          semaphore.release();// 释放一个许可
        } catch (InterruptedException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
        }

      });
    }
    threadPool.shutdown();
    System.out.println("finish");
  }

  public static void test(int threadnum) throws InterruptedException {
    Thread.sleep(1000);// 模拟请求的耗时操作
    System.out.println("threadnum:" + threadnum);
    Thread.sleep(1000);// 模拟请求的耗时操作
  }
}

5.2 CountDownLatch (倒计时器)

CountDownLatch 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。

5.2.1 原理

(1)默认构造 AQS 的 state 值为 count。

(2)当线程使用 countDown() 方法时,其实使用了tryReleaseShared方法以 CAS 的操作来减少 state,直至 state 为 0 。

(3)当调用 await() 方法的时候,如果 state 不为 0,那就证明任务还没有执行完毕,await() 方法就会一直阻塞,也就是说 await() 方法之后的语句不会被执行。

(4)然后,CountDownLatch 会自旋 CAS 判断 state == 0,如果 state == 0 的话,就会释放所有等待的线程,await() 方法之后的语句得到执行。

5.2.2 实战

(1)某一线程在开始运行前等待 n 个线程执行完毕

典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。

(1)将 CountDownLatch 的计数器初始化为 n (new CountDownLatch(n))

(2)每当一个任务线程执行完毕,就将计数器减 1 (countdownlatch.countDown())

(3)当计数器的值变为 0 时,在 CountDownLatch 上 await() 的线程就会被唤醒。

/**
 *
 * @author SnailClimb
 * @date 2018年10月1日
 * @Description: CountDownLatch 使用方法示例
 */
public class CountDownLatchExample1 {
  // 请求的数量
  private static final int threadCount = 550;

  public static void main(String[] args) throws InterruptedException {
    // 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢)
    ExecutorService threadPool = Executors.newFixedThreadPool(300);
    final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
    for (int i = 0; i < threadCount; i++) {
      final int threadnum = i;
      threadPool.execute(() -> {// Lambda 表达式的运用
        try {
          test(threadnum);
        } catch (InterruptedException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
        } finally {
          countDownLatch.countDown();// 表示一个请求已经被完成
        }

      });
    }
    countDownLatch.await();
    threadPool.shutdown();
    System.out.println("finish");
  }

  public static void test(int threadnum) throws InterruptedException {
    Thread.sleep(1000);// 模拟请求的耗时操作
    System.out.println("threadnum:" + threadnum);
    Thread.sleep(1000);// 模拟请求的耗时操作
  }
}
(2)实现多个线程开始执行任务的最大并行性

注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。
类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。

(1)初始化一个共享的 CountDownLatch 对象,将其计数器初始化为 1 (new CountDownLatch(1))

(2)多个线程在开始执行任务前首先 coundownlatch.await()

(3)当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。

5.3 CyclicBarrier(循环栅栏)

CyclicBarrier 和 CountDownLatch 非常类似,包括应用场景,也可以实现线程间的技术等待,但是功能比 CountDownLatch 更加复杂和强大。

CycliBarrier 是基于 ReentrantLock(ReentrantLock 也属于 AQS 同步器)和 Condition 的。

5.3.1 原理

(1)CyclicBarrier 内部通过一个 count 变量作为计数器,count 的初始值为 parties 属性的初始化值,每当一个线程到了栅栏这里了,那么就将计数器减 1。
如果 count 值为 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。

//每次拦截的线程数
private final int parties;
//计数器
private int count;

(2)CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量。
每个线程调用 await() 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

public CyclicBarrier(int parties) {
    this(parties, null);
}

public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

(3)当调用 CyclicBarrier 对象调用 await() 方法时,实际上调用的是 dowait(false, 0L)方法。
await() 方法就像树立起一个栅栏的行为一样,将线程挡住了。
当拦住的线程数量达到 parties 的值时,栅栏才会打开,线程才得以通过执行。

public int await() throws InterruptedException, BrokenBarrierException {
  try {
    	return dowait(false, 0L);
  } catch (TimeoutException toe) {
   	 throw new Error(toe); // cannot happen
  }
}

5.3.2 实战

/**
 *
 * @author Snailclimb
 * @date 2018年10月1日
 * @Description: 测试 CyclicBarrier 类中带参数的 await() 方法
 */
public class CyclicBarrierExample1 {
  // 请求的数量
  private static final int threadCount = 550;
  // 需要同步的线程数量
  private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);

  public static void main(String[] args) throws InterruptedException {
    // 创建线程池
    ExecutorService threadPool = Executors.newFixedThreadPool(10);

    for (int i = 0; i < threadCount; i++) {
      final int threadNum = i;
      Thread.sleep(1000);
      threadPool.execute(() -> {
        try {
          test(threadNum);
        } catch (InterruptedException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
        } catch (BrokenBarrierException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
        }
      });
    }
    threadPool.shutdown();
  }

  public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
    System.out.println("threadnum:" + threadnum + "is ready");
    try {
      /**等待60秒,保证子线程完全执行结束*/
      cyclicBarrier.await(60, TimeUnit.SECONDS);
    } catch (Exception e) {
      System.out.println("-----CyclicBarrierException------");
    }
    System.out.println("threadnum:" + threadnum + "is finish");
  }

}

运行结果,如下:

threadnum:0is ready
threadnum:1is ready
threadnum:2is ready
threadnum:3is ready
threadnum:4is ready
threadnum:4is finish
threadnum:0is finish
threadnum:1is finish
threadnum:2is finish
threadnum:3is finish
......

上一篇跳转—Java之并发编程(二)                下一篇跳转—Java之并发编程(四)


本篇文章主要参考链接如下:

参考链接-JavaGuide


持续更新中…

随心所往,看见未来。Follow your heart,see light!

欢迎点赞、关注、留言,一起学习、交流!

你可能感兴趣的:(后端Java,开发学习拓展,java,并发编程,多线程,线程池)