AQS 同步器

公平锁和非公平锁的区别

公平锁和非公平锁的释放操作一样, 调用的一个方法 . 区别在于获取锁的时候,

  • 非公平锁, 当一个线程 unlock() 后调用 lock(), 如果线程没有在改时间片切换, 则本线程可以立刻 CAS 成功修改 state 属性, 此时就发生锁连续获得. (tryAccquire 方法)
  • 公平锁, 当一个线程 unlock() 后调用 lock(), lock 会先检查 AQS 等待队列中没有前驱结点(!hasQueuedPredecessors 返回 true )再尝试 CAS 修改锁(tryAccquire 方法内多了这个条件检查)
    public final boolean hasQueuedPredecessors() {
            // The correctness of this depends on head being initialized
            // before tail and on head.next being accurate if the current
            // thread is first in queue.
            Node t = tail; // Read fields in reverse initialization order
            Node h = head;
            Node s;
            return h != t &&
                ((s = h.next) == null || s.thread != Thread.currentThread());
        }
    

因此, 有一个结论, 非公平锁比公平锁效率更高. 因为非公平锁尽量让同一个线程在 unlock() 后的 lock() 操作不阻塞; 而公平锁完全遵守 FIFO, 前程切换开销稍大.
如下交替打印 1,2,3 的例子. 使用公平锁可以实现简体打印,

public static void main(String[] args) {
    Runnable r = new Runnable() {
        // 一定是公平锁才能达到交替打印的目的
        final ReentrantLock lock = new ReentrantLock(false);

        public void run() {
            for (int i = 0; i < 3; i++) {
                lock.lock();
                System.out.println(Thread.currentThread().getId() + "(thread)==> " + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock.unlock();
            }
        }
    };

    for (int i = 0; i < 7; i++) {
        new Thread(r).start();
    }
}

输出如下:

// 公平锁交替打印   
11(thread)==> 0
12(thread)==> 0
13(thread)==> 0
11(thread)==> 1
12(thread)==> 1
13(thread)==> 1
11(thread)==> 2
12(thread)==> 2
13(thread)==> 2

如果使用非公平锁, 线程1 会打印完所有1,3,4 再由线程2 打印. 输出如下

// 非公平锁持续打印
11(thread)==> 0
11(thread)==> 1
11(thread)==> 2
12(thread)==> 0
12(thread)==> 1
12(thread)==> 2
13(thread)==> 0
13(thread)==> 1
13(thread)==> 2
条件等待队列

每个对象都可以作为一个"条件等待队列", 正如每个对象都可以作为"锁"一样. 对象的 “条件等待队列” API有2个

  • ``wait()` - wait() 要写在 while 循环内
while( 条件检查 ) {
  obj.wait()
}

因为线程可能被错误唤醒, 所以wait醒来后要再次检查等待条件

  • notify()
    jvm 从对象的条件等待队列中选择1个线程进行唤醒.
    这个操作是危险的, 因为可能造成``信号丢失问题. 线程B使用obj.notify()唤醒了线程A, 而线程A被唤醒后, 如果再次检查等待条件未满足后, 重新进入obj.wait()`, 而其它同样阻塞的线程C, 其条件检查可能可以通过, 但无法被唤醒, 造成本次唤醒操作丢失.
  • notifyAll():
    为了避免唤醒信号丢失, 通常用 notifyAll() 代替 notify() 进行唤醒.
    唯一的缺点是 notifyAll() 唤醒所有等待线程后可能有绝大多数都不满足等待条件从而进入再次等待, 造成大量上下文切换和锁竞争. 因此, 一般在 notifyAll() 外面加上 if 条件, 限制唤醒操作的适用范围. 比如: 条件等待版本的有界队列实现中, put()和take()的通知机制是保守的: 每当放入一个对象或从队列中取走一个对象就发起一次通知. 我们可以对其优化:
    (1) 首先, 仅当队列从空变为非空时, 或者从满变为不满时才释放一个线程
    (2) 并且, 仅当put()或take()影响到这些状态转换时才发出通知
    [总结]: wait() 和 notify() 的模板如下:
while(条件检查){
  obj.notify();
}

if(判断条件范围){
  obj.notifyAll();
}
用 Condition 扩充对象的条件等待队列

JVM 中, 一个内置锁只能关联1个内置条件等待队列, 这会造成多个线程在同一个条件等待队列上等待不同的条件谓词, 无法满足 “notifyAll()” 时唤醒的是同一类操作这个语义. 因此, 如果要让一个锁关联多个条件等待队列, 就不能用内置锁, 要用AQS版本的 RentraintLockCondition

Synchronized 和 RentraintLock 的区别

(1) jvm 内置锁和 AQS 实现的显示锁区别
(2) Synchronized 使用的内置锁只能关联一个内置的条件等待队列, 而显示锁可以关联多个条件等待队列

请求资源空闲:
获取资源, 将资源持有线程设置为当前线程

请求资源被占用:
资源获取失败, 将请求线程封装成 Node 加入 CLH 队列 (虚拟双向队列)

Node类:
enque: 插入新的 tail

deque: 设置新的 head 指针

// AbstractQueuedSynchronizer.java

// synchronization state. 在 ReentrantLock 中表示锁持有的数量
private volatile int state;

// 排它模式下. 资源持有者线程
private transient Thread exclusiveOwnerThread;

// 常量: 自旋时间
static final long spinForTimeoutThreshold = 1000L;

// AQS 同步类
abstract static class Sync extends AbstractQueuedSynchronizer {
      // 子类实现 lock()
      abstract void lock();

      /** 用执行外部的非公平 tryLock 方法. 无论是公平锁还是非公平锁, 都需要非公平的 tryLock()
          public boolean tryLock() {
             return sync.nonfairTryAcquire(1);
          }
      */
      final boolean nonfairTryAcquire(int acquires) {  // tryLock()时, 传入的 qcquires = 1
          final Thread current = Thread.currentThread();
          int c = getState();  // 当前 lock hold 的数量
          // 分之一: 没有现成持有锁 (lock hold = 0)
          if (c == 0) {  
              if (compareAndSetState(0, acquires)) {   // CAS 置换变量
                  setExclusiveOwnerThread(current);
                  return true;
              }
          }
          // 分支二: 资源持有线程锁重入
          else if (current == getExclusiveOwnerThread()) { 
              int nextc = c + acquires;
              if (nextc < 0) // overflow (整形溢出)
                  throw new Error("Maximum lock count exceeded");
              setState(nextc); // state属性的 setter 方法, 因为是冲入, 没有线程安全问题
              return true;
          }
          // 分支三: 其它线程持有锁
          return false;
      }

      // 尝试释放资源, 返回是否释放干净
      // 该方法在父类 AQS 的 release(int releases) 方法中被使用
      protected final boolean tryRelease(int releases) {
          // 预计释放后, state 数量降低
          int c = getState() - releases;
          // 分支一: 锁的持有者和不是当前线程, 直接报错
          if (Thread.currentThread() != getExclusiveOwnerThread())
              throw new IllegalMonitorStateException();
          // 分支二: 所得持有者就是当前线程
          boolean free = false;
          if (c == 0) { 
              free = true; // 释放干净, 资源变成 true
              setExclusiveOwnerThread(null);
          }
          setState(c);
          return free;
      }

      // 看当前线程是否是资源的排它持有者 
      protected final boolean isHeldExclusively() {
          return getExclusiveOwnerThread() == Thread.currentThread();
      }

      final ConditionObject newCondition() {
          return new ConditionObject();
      }


      final Thread getOwner() {
          // state == 0, 表示资源没有持有者
          return getState() == 0 ? null : getExclusiveOwnerThread();
      }

      // 资源持有数量: 判断持有者是当前线程后获取 state 属性
      final int getHoldCount() {
          return isHeldExclusively() ? getState() : 0;
      }

      final boolean isLocked() {
          return getState() != 0;
      }

      // 反序列化方法 - 不重要 (反序列化后重设 state=0)
      private void readObject(java.io.ObjectInputStream s)
          throws java.io.IOException, ClassNotFoundException {
          s.defaultReadObject();
          setState(0); // reset to unlocked state
      }
  }


// 默认非公平锁
static final class NonfairSync extends Sync {

    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     */
    final void lock() {
        // 分支一: 没有线程持有锁 
        if (compareAndSetState(0, 1))    // CAS 更改 state 字段, 从 0 到 1
            setExclusiveOwnerThread(Thread.currentThread());   // 设置持有者线程
        // 分之二: 有线程持有锁
        else
            acquire(1);    // AQS内置方法
    }

    // AQS 子类应该覆盖的 tryAcquire
    protected final boolean tryAcquire(int acquires) {  
        return nonfairTryAcquire(acquires);   // 更改 state 和 exclusiveOwnerThread 属性
    }

    // AQS 子类应该覆盖的 tryRelease 在父类 Sync 中统一定会有
}


// 公平锁
static final class FairSync extends Sync {

    final void lock() {
        acquire(1);       // AQS内置方法
    }

    /**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     */
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}
AQS属性
  • (1) state: 表示资源持有的数量
  • (2) exclusiveOwnerThread: 资源持有者, 这个属性可以在操作资源时, 判断是否有资格操作; 而且在操作资源后, 在无锁的情况下执行 setState(). (因为在同一个线程下, 不会有并发问题)
继承AQS同步器, 需要重写的方法

AQS 暴露出的方法, 只要用户操作 stateexclusiveOwnerThread 属性即可实现获取资源, 等待资源, 释放资源等需求
tryAcquire(int arg):独占式获取同步状态,获取同步状态成功后,其他线程需要等待该线程释放同步状态才能获取同步状态。
tryRelease(int arg):独占式释放同步状态。
tryAcquireShared(int arg):共享式获取同步状态,返回值大于等于 0 ,则表示获取成功;否则,获取失败。
tryReleaseShared(int arg):共享式释放同步状态。
isHeldExclusively():同步器是否被当前线程所独占

AQS内置方法

acquire(int arg):独占式获取同步状态。如果当前线程获取同步状态成功,则由该方法返回;否则,将会进入同步队列等待。该方法将会调用可重写的 #tryAcquire(int arg) 方法;
acquireInterruptibly(int arg):与 #acquire(int arg) 相同,但是该方法响应中断。当前线程为获取到同步状态而进入到同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException 异常并返回。
tryAcquireNanos(int arg, long nanos):超时获取同步状态。如果当前线程在 nanos 时间内没有获取到同步状态,那么将会返回 false ,已经获取则返回 true 。
acquireShared(int arg):共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态;
acquireSharedInterruptibly(int arg):共享式获取同步状态,响应中断。
tryAcquireSharedNanos(int arg, long nanosTimeout):共享式获取同步状态,增加超时限制。
release(int arg):独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒。
releaseShared(int arg):共享式释放同步状态。

AQS等待队列 - CLH队列
static final class Node {
    // 共享模式还是独占模式
    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;

    /***** 节点状态常量定义 *******/
    // 因为超时或者中断,节点会被设置为取消状态,被取消的节点不会参与到竞争中,他会一直保持取消状态不会转变为其他状态
    static final int CANCELLED =  1;
    // 后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点的线程得以运行
    static final int SIGNAL    = -1;
    // 节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()后,该节点将会从等待队列中转移到同步队列中,加入到同步状态的获取中
    static final int CONDITION = -2;
    // 表示下一次共享式同步状态获取,将会无条件地传播下去
    static final int PROPAGATE = -3;
    /***********************/

    // 等待状态 
    volatile int waitStatus;

    // 前驱和后继指向
    volatile Node prev;
    volatile Node next;

    // 等待队列中的后续节点。如果当前节点是共享的,那么字段将是一个 SHARED 常量,
    // 也就是说节点类型(独占和共享)和等待队列中的后续节点共用同一个字段
    Node nextWaiter;
    
    // 获取同步状态的线程 
    volatile Thread thread;

    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    Node() { // Used to establish initial head or SHARED marker
    }

    Node(Thread thread, Node mode) { // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }

    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
    
}

(1)入队动作
入队动作主要是 addWaiter() 方法: 先获取当前的 tail, 然后 CAS 对比后设置新的 tail.


// 入队动作
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) {
            // 分支一: 初始化动作放在 enq 的时候: 设置 head = tail = dummyNode
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // 分之二: 真正的入队动作, CAS 加入 tail
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

// 增加新 waiter
// 该方法和 enq 方法差不多
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    // 第一次不跳入循环尝试 CAS 添加, 失败再走完整的 enq 流程
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

(2) 出队
因为队列的先进先出, 所以只要获取当前节点的 next 节点, 作为新的 head 即可.
因为只有获取资源的线程才能释放资源, 因此出队不存在线程安全问题, 无锁设置 head 即可

private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值