Java多线程Day20-JUC锁之非公平锁

152 阅读4分钟

非公平锁

基本概念

  • 非公平锁和公平锁在获取锁的方法上,流程是一样的,区别在与获取锁的机制不同

    • 非公平锁在每次尝试获取锁时,采用的是非公平策略,无视等待队列,直接尝试获取锁,如果锁是空闲的,即可获取锁的状态,则获取锁
    • 公平锁在每次尝试获取锁时,采用的是公平策略,根据等待队列依次排序等待获取锁

获取非公平锁

lock

  • lock()ReentrantLockNonfairSync类中实现:
		final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
  • lock() 方法首先通过调用compareAndSetState(0, 1) 来判断锁是否为空闲状态

    • 如果是空闲状态,当前线程可以直接获取锁

    • 如果不是空闲状态,则通过调用acquire(1) 获取锁

      • compareAndSet()CAS函数,作用是比较并设置当前锁的状态
  • setExclusiveOwnerThread(Thread.currentThread()) 的作用是设置当前线程为锁的持有者

  • 公平锁的lock()方法和非公平锁lock()方法的比较:

    • 公平锁的lock() 方法中直接调用acquire(1) 方法获取锁
    • 非公平锁的lock() 方法会首先判断当前锁的状态是否为空闲状态,如果是空闲状态,直接获取锁

acquire

  • acquire()AQS中实现:
	public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
  • 当前线程首先通过tryAcquire() 尝试获取锁,如果获取成功,直接返回. 如果获取失败,则进入到等待队列依次排序,然后获取锁

  • 当前线程在尝试失败的情况下,会通过addWaiter(Node.EXCLUSIVE) 将当前线程加入到非阻塞的FIFO队列CLH队列的

  • 调用acquireQueued() 方法获取锁:

    • 当前线程会等待CLH队列中前面的所有线程执行并释放锁之后,才能获取锁并返回
    • 如果当前线程在等待过程中被中断过,会调用selfInterrupt() 自己产生一个中断
  • 公平锁的acquire()方法和非公平锁的acquire()方法的比较:

    • 公平锁和非公平锁只有tryAcquire() 方法的实现不相同,也就是尝试获取锁的机制不同,即获取锁的策略不同

tryAcquire

  • 非公平锁的tryAcquire()ReentrantLock类中的NonfairSync类中实现:
  	 protected final boolean tryAcquire(int acquires) {
          return nonfairTryAcquire(acquires);
      }
  • 公平锁的tryAcquire()方法和非公平锁的tryAcquire()方法的比较:

    • 公平锁的tryAcquire() 方法尝试获取锁时,即使锁没有被任何线程所持有,也会判断当前线程是否在CLH队列的表头,只有当前线程在CLH队列的表头时,才会获取锁
    • 非公平锁的tryAcquire() 方法尝试获取锁时,只要锁没有被任何线程所持有,就会直接获取锁

nonfairTryAcquire

  • nonfairTryAcquire()ReentrantLock类的Sync类中实现:
		final boolean nonfairTryAcquire(int acquires) {
			// 获取当前线程
            final Thread current = Thread.currentThread();
            // 获取锁的状态
            int c = getState();
            if (c == 0) {
            	/* 
            	 * c == 0表示锁没有被任何线程所持有
            	 * 如果锁没有被任何线程锁持有,则通过CAS函数设置锁的状态为acquires
            	 * 同时,设置当前线程为锁的持有者
            	 */
                if (compareAndSetState(0, acquires)) {
                    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);
                return true;
            }
            return false;
        }
  • nonfairTryAcquire() 的作用就是尝试获取锁:

    • 如果锁没有被任何线程所持有,那么就通过CAS函数设置锁的状态为acquires, 同时设置当前线程为锁的持有者,返回true
    • 如果锁的持有者是当前线程,则更新锁的状态,返回true
    • 如果不属于以上两种情况,则尝试获取锁失败,返回false

释放非公平锁

unlock

  • unlock()ReentrantLock中实现:
public void unlock() {
	sync.release(1);
}
  • unlock() 是释放锁函数,是通过AQS中的release() 函数实现的

  • 1的含义: 锁的状态的参数

    • 对于独占锁来说,当锁处于可获取状态时,状态值为0
    • 对于独占锁来说,当锁初次被线程获取到时,状态值为1
  • 因为公平锁是可重入锁,对于同一个线程,每次释放锁,锁的状态就会减1

release

  • release()AQS中实现:
	public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
  • release() 中首先会调用tryRelease() 尝试释放当前线程持有的锁. 如果成功,则唤醒后继等待的线程,并且返回true. 如果失败,则返回false

tryRelease

  • tryRelease() 在ReentrantLock类中的Sync类中实现:
		protected final boolean tryRelease(int releases) {
			// 定义释放锁之后的状态
            int c = getState() - releases;
            // 如果当前线程不是锁的持有者,则抛出异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
            	// 如果当前锁已经被当前线程释放,则设置锁的持有者为null.此时锁是可获取状态
                free = true;
                setExclusiveOwnerThread(null);
            }
            // 设置释放锁之后的当前线程的锁的状态
            setState(c);
            return free;
        }
  • tryRelease() 的作用是尝试释放当前锁

    • 如果当前线程不是锁的持有者,则抛出异常
    • 在当前线程释放锁操作之后,当前线程对锁的拥有状态为0, 此时当前线程彻底释放该锁.设置锁的持有者为null, 这是锁即为可获取状态,同时更新当前线程的锁的状态为0

unparkSuccessor

  • release() 中当前线程释放锁成功之后,会唤醒当前线程的后继线程
  • 根据CLH队列的先入先出FIFO原则,当前线程必然是头结点head, 如果CLH队列非空,则唤醒下一个等待线程
	private void unparkSuccessor(Node node) {
        // 获取当前线程的状态
        int ws = node.waitStatus;
        if (ws < 0)
        	// 如果状态为负,则设置状态值为0
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * 获取当前节点的有效的后继节点
         * 如果是无效的节点,则使用for循环从尾部开始寻找
         * 这里的有效是指后继节点对应的状态的 
         */
        
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        // 唤醒后继节点对应的线程
        if (s != null)
            LockSupport.unpark(s.thread);
    }
  • unparkSuccessor() 的作用的是唤醒当前线程的后继线程
  • 后继线程唤醒之后,就可以继续获取锁并恢复运行

总结

  • 公平锁和非公平锁的区别在于获取锁的机制的不同:

    • 公平锁在尝试获取锁时,只有当前线程在CLH等待队列的表头,才会获取锁
    • 非公平所在尝试获取锁时,只要锁处于空闲状态,未被其余线程所持有,则直接获取锁,不考虑当前线程在CLH等待队列的顺序