基本概念

1. AQS

AQS概述
AQS 是一个用于构建锁、同步器等线程协作工具类的框架,有了 AQS 之后,可以让更上层的开发极大的减少工作量,避免重复造轮子,同时也避免了上层因处理不当而导致的线程安全问题。

AQS内部原理解析
AQS 最核心的三大部分就是状态、队列和期望协作工具类去实现的获取/释放等重要方法。

1. state 状态
在 AQS 中有 state 这样的一个属性,是被 volatile 修饰的,会被并发修改,它代表当前工具类的某种状态,在不同的类中代表不同的含义。

2. FIFO 队列
FIFO 队列,即先进先出队列,这个队列最主要的作用是存储等待的线程。假设很多线程都想要同时抢锁,那么大部分的线程是抢不到的,那怎么去处理这些抢不到锁的线程呢?就得需要有一个队列来存放、管理它们。所以 AQS 的一大功能就是充当线程的“排队管理器”。
image.png
在队列中,分别用 head 和 tail 来表示头节点和尾节点,两者在初始化的时候都指向了一个空节点。头节点可以理解为“当前持有锁的线程”,而在头节点之后的线程就被阻塞了,它们会等待被唤醒,唤醒也是由 AQS 负责操作的。

3. 获取/释放方法
在 AQS 中除了刚才讲过的 state 和队列之外,还有一部分非常重要,那就是获取和释放相关的重要方法,这些方法是协作工具类的逻辑的具体体现,需要每一个协作工具类自己去实现,所以在不同的工具类中,它们的实现和含义各不相同。

  • 获取方法

“获取方法”在不同的类中代表不同的含义,但往往和 state 值相关,也经常会让线程进入阻塞状态,这也同样证明了 state 状态在 AQS 类中的重要地位。

  • 释放方法

释放方法是站在获取方法的对立面的,通常和刚才的获取方法配合使用。我们刚才讲的获取方法可能会让线程阻塞,比如说获取不到锁就会让线程进入阻塞状态,但是释放方法通常是不会阻塞线程的。

AQS 用法
如果想使用 AQS 来写一个自己的线程协作工具类,通常而言是分为以下三步,这也是 JDK 里利用 AQS 类的主要步骤:

  • 第一步,新建一个自己的线程协作工具类,在内部写一个 Sync 类,该 Sync 类继承 AbstractQueuedSynchronizer,即 AQS;
  • 第二步,想好设计的线程协作工具类的协作逻辑,在 Sync 类里,根据是否是独占,来重写对应的方法。如果是独占,则重写 tryAcquire 和 tryRelease 等方法;如果是非独占,则重写 tryAcquireShared 和 tryReleaseShared 等方法;
  • 第三步,在自己的线程协作工具类中,实现获取/释放的相关方法,并在里面调用 AQS 对应的方法,如果是独占则调用 acquire 或 release 等方法,非独占则调用 acquireShared 或 releaseShared 或 acquireSharedInterruptibly 等方法。

2. CAS

CAS概述
CAS(Compare-And-Swap)“比较并交换”,它是一种思想、一种算法。在多线程的情况下,各个代码的执行顺序是不能确定的,所以为了保证并发安全,我们可以使用互斥锁。而 CAS 的特点是避免使用互斥锁,当多个线程同时使用 CAS 更新同一个变量时,只有其中一个线程能够操作成功,而其他线程都会更新失败。不过和同步互斥锁不同的是,更新失败的线程并不会被阻塞,而是被告知这次由于竞争而导致的操作失败,但还可以再次尝试。

CAS的缺点
1. ABA问题
一个值从 A 变成了 B,再由 B 变回了 A,正常情况是认为它变化了两次,而CAS会认为变量的值在此期间没有发生过变化。所以,CAS 并不能检测出在此期间值是不是被修改过,它只能检查出现在的值和最初的值是不是一样。
我们可以在变量值自身之外,再添加一个版本号,那么这个值的变化路径就从 A→B→A 变成了 1A→2B→3A,这样一来,就可以通过对比版本号来判断值是否变化过。在 atomic 包中提供了 AtomicStampedReference 这个类,它是专门用来解决 ABA 问题的。

2. 自旋时间过长
由于单次 CAS 不一定能执行成功,所以 CAS 往往是配合着循环来实现的,有的时候甚至是死循环,不停地进行重试,直到线程竞争不激烈的时候,才能修改成功。在高并发场景,CAS操作可能会一直不成功,这样会导致cpu资源一直被占用,所以通常通常 CAS 的效率是不高的。

3. 范围围不能灵活控制
通常我们去执行 CAS 的时候,是针对某一个,而不是多个共享变量的,这个变量可能是 Integer 类型,也有可能是 Long 类型、对象类型等等,但是我们不能针对多个共享变量同时进行 CAS 操作,因为这多个变量之间是独立的,简单的把原子操作组合到一起,并不具备原子性。因此如果我们想对多个对象同时进行 CAS 操作并想保证线程安全的话,是比较困难的。

Java中的锁

1. Lock

Lock是一个接口,它定义了锁获取和释放的基本操作。

2. Condition

Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。

3. ReentrantLock

重入锁ReentrantLock,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。

4. ReadWriteLock

读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。

5. LockSupport

LockSupport主要是为了阻塞和唤醒线程

同步工具类

1. CountDownLatch

CountDownLatch允许一个或多个线程等待其他线程完成操作。

  1. public static void main(String[] args) throws InterruptedException {
  2. CountDownLatch c = new CountDownLatch(2);
  3. new Thread(new Runnable() {
  4. @Override
  5. public void run() {
  6. System.out.println("1");
  7. c.countDown();
  8. System.out.println("2");
  9. c.countDown();
  10. }
  11. }).start();
  12. c.await();
  13. System.out.println("3");
  14. }
  • CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完成,这里就传入N。
  • 当我们调用CountDownLatch的countDown方法时,N就会减1,CountDownLatch的await方法会阻塞当前线程,直到N变成零。
  • 由于countDown方法可以用在任何地方,所以这里说的N个点,可以是N个线程,也可以是1个线程里的N个执行步骤。
  • 用在多个线程时,只需要把这个CountDownLatch的引用传递到线程里即可。
  • 如果有某个解析sheet的线程处理得比较慢,我们不可能让主线程一直等待,所以可以使用另外一个带指定时间的await方法,这个方法等待特定时间后,就会不再阻塞当前线程。join也有类似的方法。

2. Semaphore

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。

  1. public static void main(String[] args) {
  2. int threadCount = 30;
  3. ExecutorService threadPool = Executors.newFixedThreadPool(threadCount);
  4. Semaphore s = new Semaphore(10);
  5. for (int i = 0; i < threadCount; i++) {
  6. threadPool.execute(new Runnable() {
  7. @Override
  8. public void run() {
  9. try {
  10. s.acquire();
  11. System.out.println("save data");
  12. s.release();
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. }
  17. });
  18. }
  19. threadPool.shutdown();
  20. }
  • 有30个线程在执行,但是只允许10个并发执行。
  • Semaphore的构造方法接受一个整型的数字,表示可用的许可证数量。
  • Semaphore(10)表示允 许10个线程获取许可证,也就是最大并发数是10。
  • Semaphore的用法也很简单,首先线程使用 Semaphore的acquire()方法获取一个许可证,使用完之后调用release()方法归还许可证。
  • 还可以用tryAcquire()方法尝试获取许可证。

3. CyclicBarrier

CyclicBarrier⽤于协调多个线程同步执⾏操作的场合。

4. Exchanger

Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。

5. Phaser

从JDK7开始,新增了⼀个同步⼯具类Phaser,其功能⽐CyclicBarrier和CountDownLatch更加强⼤。