AQS

全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架

特点:

  • state 属性来表示可用资源数(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
    • getState - 获取 state 状态
    • setState - 设置 state 状态
    • compareAndSetState - cas 机制设置 state 状态
  • 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet(AQS 是 Java 实现,sychronized 是 c++ 实现)
  • AQS 要实现的功能目标
    • 阻塞版本获取锁 acquire (cas 失败会 park 并添加到等待队列) 和 非阻塞的版本尝试获取锁 tryAcquire(cas 失败直接返回 false)
    • 获取锁超时机制
    • 通过打断取消机制
    • 独占机制及共享机制
    • 条件不满足时的等待机制

state 设计

  • state 使用 volatile 配合 cas 保证其修改时的原子性(cas 使用的前提是一定要用 volatile 保证可见性)
  • state 使用了 32bit int 来维护同步状态,因为当时使用 long 在很多平台下测试的结果并不理想

 

阻塞恢复设计

  • 早期的控制线程暂停和恢复的 api 有 suspend 和 resume,但它们是不可用的,因为如果先调用的 resume 那么 suspend 将感知不到
  • 解决方法是使用 park & unpark 来实现线程的暂停和恢复,具体原理在之前讲过了,先 unpark 再 park 也没问题 。park & unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细
  • park 线程还可以通过 interrupt 打断

队列设计

  • 使用了 FIFO 先入先出队列,并不支持优先级队列
  • 设计时借鉴了 CLH 队列,它是一种单向无锁队列
  • 队列中有 head 和 tail 两个指针节点,都用 volatile 修饰配合 cas 使用,每个节点有:
    1. state 维护节点状态(注意和 AQS 的状态区分。这个时等待链表中每个节点的状态)
    2. 等待的线程(用于唤醒线程 / 可重入时判断是否本线程,每次重入计数+1,重入计数减为0才真正释放)

 

等待队列节点状态

     /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;

 

可重入锁

有一个重入计数

  • 加锁时:如果加锁失败,判断锁的 owner 是否是当前线程,如果是,重入计数加一。
  • 解锁时:释放一次,计数减一。只有重入计数减为0,才代表解锁成功

 

可打断(是说 park 等待时是否可打断)

  • 不可打断模式:park 等待时时默认情况下是不可打断的。在此模式下,即使它被打断,仍会驻留在 AQS 队列中,仅会设置一个一个打断标志,一直要等到获得锁后根据打断标志得知自己被打断了,才真正打断(本线程重新把自己打断)

  • 可打断模式:park 时被打断了,直接抛出打断异常

 

公平锁

  • 非公平锁实现:如果线程一进来时 AQS 状态为0(无锁),就 直接尝试用 CAS 获取锁,这里体现了非公平性,不去检查之前已经在排队的 AQS 队列
  • 公平锁实现:如果线程一进来时 AQS 状态为 0(无锁),不会先直接尝试用 CAS 获取锁,而时先检查之前已经在排队的 AQS 队列,如果没有人排队,或者自己就在最前面,才会尝试用 CAS 竞争锁

 

用 AQS 自定义不可重入锁

tryAcquire 在 AQS 原代码里是可重入锁,这里继承了 AQS,将其重写为不可重入锁了

不可重入锁:本线程加了锁,那么后续本线程还想继续加锁,加锁是不成功的

final class MySync extends AbstractQueuedSynchronizer {
 
     @Override
     protected boolean tryAcquire(int acquires) {
      // int acquires 是可重入锁用来计数的。现在不可重入锁暂时用不到
         if (acquires == 1){
        // 通过设置 aqs 的 state 获得锁,初始值0,改为1代表获得了锁
        // 为了防止其它线程一起来修改 state 状态,所以用 compareAndSet
             if (compareAndSetState(0, 1)) {
          // 加锁成功后,设置 owner 线程为当前线程
             setExclusiveOwnerThread(Thread.currentThread());
          return true;
         }
      }
     // CAS 失败,即加锁失败
     return false;
   }
  @Override   
protected boolean tryRelease(int acquires) {     if(acquires == 1) {       // state=0表示当前没加锁,却要释放,抛出异常       if(getState() == 0) {         throw new IllegalMonitorStateException();       }       // 置空当前线程。ExclusiveOwnerThread 在 AQS 里不是 volatile 的       setExclusiveOwnerThread(null);       // state 在 AQS 里是 volatile 的,所以要放在后面,加写屏障,防止之前的指令重排       setState(0);       return true;     }     return false;    }
  
protected Condition newCondition() {     // aqs 提供的类     return new ConditionObject();    }
  @Override   
protected boolean isHeldExclusively() {     // 是否持有独占锁     return getState() == 1;   } }

原代码里的 acquire ,用 tryAcquire 尝试获取锁不成功,就放入等待队列中添加到一个链表中,并 LockSupport.park(this)

我们重写了 tryAcquire 方法为不可重入锁,所以这里的尝试获取锁会调用我们重写的方法,即获取不可重入锁

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}

原代码里的 release,用 tryRelease 尝试释放锁成功后,还会唤醒等待队列上的线程从链表中移除,并用 LockSupport.unpark(node.thread) 唤醒

我们重写了 tryRelease 方法为不可重入锁释放,所以这里的尝试释放锁会调用我们重写的方法,即释放不可重入锁

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
}

对 MySync 的封装

class MyLock implements Lock {
 static MySync sync = new MySync();

 @Override
 // 尝试获取锁(被重写的tryAcquire),不成功,进入等待队列(AQS的逻辑)
 public void lock() {
 sync.acquire(1);
 }

 @Override
 // 尝试,不成功,进入等待队列,可打断
 public void lockInterruptibly() throws InterruptedException {
 sync.acquireInterruptibly(1);
 }

 @Override
 // 尝试一次,不成功返回,不进入队列(acquire 是 tryAcquire 进行的封装,失败会进入等待队列)
 public boolean tryLock() {
 return sync.tryAcquire(1);
 }

 @Override
 // 尝试,不成功,进入等待队列,有时限
 public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
 return sync.tryAcquireNanos(1, unit.toNanos(time));
 }

 @Override
 // 释放锁。tryRelease 只会释放锁,release 还会唤醒等待的线程
 public void unlock() {
 sync.release(1);
 }

 @Override
 // 生成条件变量
 public Condition newCondition() {
 return sync.newCondition();
 }
}

测试

MyLock lock = new MyLock();
new Thread(() -> {
   lock.lock();
   try {
     log.debug("locking...");
     sleep(1);
   } finally {
     log.debug("unlocking...");
     lock.unlock();
   }
},
"t1").start();
new Thread(() -> {   lock.lock();   try {     log.debug("locking...");   } finally {     log.debug("unlocking...");     lock.unlock(); } },"t2").start();

 

ReentrantLock

见 《ReentrantLock

 

ConcurrentHashMap

见《线程安全集合类——ConcurrentHashMap

Semaphore

1.基本使用

信号量,用来限制能同时访问共享资源的线程上限。

暂时没获得许可的线程,会通过 AQS 等待

public static void main(String[] args) {
   // 1. 创建 semaphore 对象
   Semaphore semaphore = new Semaphore(3);
   // 2. 10个线程同时运行
   for (int i = 0; i < 10; i++) {
     new Thread(() -> {
       // 3. 获取许可
       try {
         semaphore.acquire();
       } catch (InterruptedException e) {
         e.printStackTrace();
       }
      
try {         log.debug("running...");         sleep(1);         log.debug("end...");       } finally {         // 4. 释放许可         semaphore.release();       }     }).start();   } }

2.应用:单机限流/连接池

semaphore 实现

  • 使用 Semaphore 限流,在访问高峰期时,让请求线程阻塞,高峰期过去再释放许可,当然它只适合限制单机 线程数量,并且仅是限制线程数,而不是限制资源数(例如连接数,请对比 Tomcat LimitLatch 的实现)
  • Semaphore 实现简单连接池,对比『享元模式』下的实现(用wait notify),性能和可读性显然更好, 注意下面的实现中线程数和数据库连接数是相等的

之前:没有空闲连接 lock.wait() ,有空闲连接了 notify()

@Slf4j(topic = "c.Pool")
class Pool {
   // 1. 连接池大小
   private final int poolSize;
   // 2. 连接对象数组
   private Connection[] connections;
   // 3. 连接状态数组 0 表示空闲, 1 表示繁忙
   private AtomicIntegerArray states;
   private Semaphore semaphore;
  
// 4. 构造方法初始化   public Pool(int poolSize) {     this.poolSize = poolSize;     // 让许可数与资源数一致     this.semaphore = new Semaphore(poolSize);     this.connections = new Connection[poolSize];     this.states = new AtomicIntegerArray(new int[poolSize]);     for (int i = 0; i < poolSize; i++) {       connections[i] = new MockConnection("连接" + (i+1));     }   }   // 5. 借连接   public Connection borrow() {// t1, t2, t3     // 获取许可     try {       semaphore.acquire(); // 没有许可的线程,会在此等待     } catch (InterruptedException e) {       e.printStackTrace();     }
    
for (int i = 0; i < poolSize; i++) {       // 获取空闲连接       if(states.get(i) == 0) {         if (states.compareAndSet(i, 0, 1)) {           log.debug("borrow {}", connections[i]);           return connections[i];         }       }     }     // 永远不会执行到这里,semaphore 保证了走到下面总能获得一个可用连接。这里只是为了语法不出错     return null;   }
  
// 6. 归还连接   public void free(Connection conn) {     for (int i = 0; i < poolSize; i++) {       if (connections[i] == conn) {         states.set(i, 0);         log.debug("free {}", conn);         semaphore.release();         break;       }     }   } }

3.原理-acquire

Semaphore 有点像一个停车场,permits 就好像停车位数量,当线程获得了 permits 就像是获得了停车位,然后 停车场显示空余车位减一

刚开始,permits(state)为 3,这时 5 个线程来获取资源

假设其中 Thread-1,Thread-2,Thread-4 cas 竞争成功,而 Thread-0 和 Thread-3 竞争失败,进入 AQS 队列 park 阻塞

AQS 的 state 表示可用资源数

源码

static final class NonfairSync extends Sync {
   private static final long serialVersionUID = -2694183684443567898L;
 
   NonfairSync(
int permits) {     // permits 即 state     super(permits);   }   // Semaphore 方法, 方便阅读, 放在此处   public void acquire() throws InterruptedException {     sync.acquireSharedInterruptibly(1);   }
  
// AQS 继承过来的方法, 方便阅读, 放在此处   public final void acquireSharedInterruptibly(int arg) throws InterruptedException {     if (Thread.interrupted())       throw new InterruptedException();
     // >=0 表明获取锁成功     
if (tryAcquireShared(arg) < 0)       doAcquireSharedInterruptibly(arg);   }   // 尝试获得共享锁   protected int tryAcquireShared(int acquires) {     return nonfairTryAcquireShared(acquires);   }   // Sync 继承过来的方法, 方便阅读, 放在此处   final int nonfairTryAcquireShared(int acquires) {     for (;;) {
       // AQS 的 state 表示可用资源数       
int available = getState();       int remaining = available - acquires;       if (          // 一次获取的许可 acquires 超过了剩余的, 获取失败         remaining < 0 ||          // 如果 cas 重试成功, 返回 >=0 的数, 表示获取成功         compareAndSetState(available, remaining)        ) {         return remaining;       }     }   }   // AQS 继承过来的方法, 方便阅读, 放在此处
   // 获取失败时,进入这里
  private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
     // 添加到等待队列 队尾     
final Node node = addWaiter(Node.SHARED);     boolean failed = true;     try {       for (;;) {
         // 获取当前节点的前驱节点         
final Node p = node.predecessor();
         // 如果前驱节点是头节点,说明它是老二(头节点是虚的,Thread一直为null)         
if (p == head) {           // 老二可以,再次尝试获取许可           int r = tryAcquireShared(arg);
           
// r>=0 表示获取成功,r 表示剩余可用资源数            
if (r >= 0) {             // 成功后本线程出队(AQS), 所在 Node设置为 head             // 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark             // 如果 head.waitStatus == 0 ==> Node.PROPAGATE              // r 表示可用资源数, 为 0 则不会继续传播             setHeadAndPropagate(node, r);             p.next = null; // help GC             failed = false;             return;           }         }
        
// 1.不是老二 或 2.老二再次获取资源不成功, 设置上一个节点 waitStatus = Node.SIGNAL(-1 有责任唤醒下一个), 此节点下轮进入 park 阻塞         if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())           throw new InterruptedException();         }    } finally {      if (failed)       cancelAcquire(node);      }    }   // Semaphore 方法, 方便阅读, 放在此处   public void release() {     sync.releaseShared(1);   }   // AQS 继承过来的方法, 方便阅读, 放在此处   public final boolean releaseShared(int arg) {
     // CAS 修改 AQS state     
if (tryReleaseShared(arg)) {
       // 做释放后续工作:头节点获得资源,并唤醒后继节点等
      doReleaseShared();       
return true;     }     return false;   }   // Sync 继承过来的方法, 方便阅读, 放在此处   protected final boolean tryReleaseShared(int releases) {     for (;;) {       int current = getState();
       // 释放 release 个,AQS 的 state 由 current 变为 current + release 个       
int next = current + releases;       if (next < current) // overflow         throw new Error("Maximum permit count exceeded");       if (compareAndSetState(current, next))         return true;     }   } }

tryAcquire 里,如果获取成功后,还有剩余的

太难了,先放弃。。。。

propagate 是什么?

 

 

CountdownLatch(倒计时锁)

用法

用来进行线程同步协作,等待所有线程完成倒计时。

其中构造参数用来初始化等待计数值,await() 用来等待计数归零,countDown() 用来让计数减一

public static void main(String[] args) throws InterruptedException {
   CountDownLatch latch = new CountDownLatch(3);
   
   new Thread(() -> {     log.debug("begin...");     sleep(1);     latch.countDown();     log.debug("end...{}", latch.getCount());   }).start();
  
new Thread(() -> {     log.debug("begin...");     sleep(2);     latch.countDown();     log.debug("end...{}", latch.getCount());   }).start();
  
new Thread(() -> {     log.debug("begin...");     sleep(1.5);     latch.countDown();     log.debug("end...{}", latch.getCount());   }).start();
  log.debug(
"waiting...");   latch.await();   log.debug("wait end..."); }

输出结果:主线程会在 latch.await() 阻塞住,等待三个线程都运行完了,计数减为0后,才恢复运行

18:44:00.778 c.TestCountDownLatch [main] - waiting... 
18:44:00.778 c.TestCountDownLatch [Thread-2] - begin... 
18:44:00.778 c.TestCountDownLatch [Thread-0] - begin... 
18:44:00.778 c.TestCountDownLatch [Thread-1] - begin... 
18:44:01.782 c.TestCountDownLatch [Thread-0] - end...2 
18:44:02.283 c.TestCountDownLatch [Thread-2] - end...1 
18:44:02.782 c.TestCountDownLatch [Thread-1] - end...0 
18:44:02.782 c.TestCountDownLatch [main] - wait end... 

源码

public class CountDownLatch {
    /**
     * Synchronization control For CountDownLatch.
     * Uses AQS state to represent count.
     */
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

        Sync(int count) {
            setState(count);
        }

        int getCount() {
            return getState();
        }

     // state 为 0 才算是获取成功,返回1。state大于表明等待的线程还没减完,会继续阻塞
protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -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; } } }
}

countDownLatch.await()

  public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

  public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
     // state 不为0,就会返回 -1,进入下面的阻塞等待。直到 state 减为 0
if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); }

countDownLatch.countdown()

将 state 减少 1

  public void countDown() {
        sync.releaseShared(1);
    }

比 join 好在哪里?

join 是等待线程运行结束。线程池里的线程一般都会重用不会主动结束。

而 CountDownLatch 可以在代码里主动调用,当线程的主要逻辑运行完了就可以主动调用,不必等待线程运行结束。

应用-等待多线程准备完毕

AtomicInteger num = new AtomicInteger(0);
ExecutorService service = Executors.newFixedThreadPool(10, (r) -> {
  return new Thread(r, "t" + num.getAndIncrement());
});
CountDownLatch latch = new CountDownLatch(10);
String[] all = new String[10];
Random r = new Random();
// 一共十个线程
for (int j = 0; j < 10; j++) {   int x = j;   service.submit(() -> {
     // 每个线程有一百个任务     
for (int i = 0; i <= 100; i++) {       try {         Thread.sleep(r.nextInt(100));       } catch (InterruptedException e) {       }
       // all[x] 表示第 j 个线程的执行进度:一百个任务完成到多少了       all[x]
= Thread.currentThread().getName() + "(" + (i + "%") + ")";       System.out.print("\r" + Arrays.toString(all));     }   latch.countDown(); }); } latch.await(); System.out.println("\n游戏开始..."); service.shutdown();

中间输出

[t0(52%), t1(47%), t2(51%), t3(40%), t4(49%), t5(44%), t6(49%), t7(52%), t8(46%), t9(46%)] 

最后输出

[t0(100%), t1(100%), t2(100%), t3(100%), t4(100%), t5(100%), t6(100%), t7(100%), t8(100%), 
t9(100%)] 
游戏开始... 

应用-等待远程调用结束-用 FutureTask 替代

CountDownLatch 只是简单的等待结束,FutureTask 可以等待接收最后的结果

RestTemplate restTemplate = new RestTemplate();
log.debug("begin");
ExecutorService service = Executors.newCachedThreadPool();
Future<Map<String,Object>> f1 = service.submit(() -> {
   Map<String, Object> r =
   restTemplate.getForObject("http://localhost:8080/order/{1}", Map.class, 1);
   return r;
});
Future<Map<String, Object>> f2 = service.submit(() -> {
   Map<String, Object> r =
   restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 1);
   return r;
});
Future<Map<String, Object>> f3 = service.submit(() -> {
   Map<String, Object> r =
   restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 2);
   return r;
});
Future<Map<String, Object>> f4 = service.submit(() -> {
   Map<String, Object> r =
   restTemplate.getForObject("http://localhost:8080/logistics/{1}", Map.class, 1);
   return r;
});
System.out.println(f1.get()); System.out.println(f2.get()); System.out.println(f3.get()); System.out.println(f4.get()); log.debug(
"执行完毕"); service.shutdown();

 

 

CyclicBarrier

CountDownLatch 只可以在 new 的时候指定数量,后面只能 countdown(),而不能修改计数。

CyclicBarrier 循环栅栏改进了这一点,计数减为 0 后,再次调用 await(),计数就是回到了开始时指定的那个。

ExecutorService service = Executors.newFixedThreadPool(2);

CyclicBarrier cb = new CyclicBarrier(2, ()->{
  log.debug("task1 task2 finish......")
}); // 个数为2时才会继续执行
// 第二个循环计数减为 0 后,cb 从 2 重新开始 for (int i=0;i<3;i++) {   service.submit(()->{     System.out.println("线程1开始.."+new Date());     try {       cb.await(); // 计数 2-1=1。同时当计数不为0时,等待      } catch (InterruptedException | BrokenBarrierException e) {       e.printStackTrace();      }      System.out.println("线程1继续向下运行..."+new Date());   });   service.submit(()->{     System.out.println("线程2开始.."+new Date());     try { Thread.sleep(2000); } catch (InterruptedException e) { }     try {       cb.await(); // 计数 1-1=0。等于 0 了,这个和上面那个线程可以恢复继续运行     } catch (InterruptedException | BrokenBarrierException e) {       e.printStackTrace();     }     System.out.println("线程2继续向下运行..."+new Date()); }); }