一、AQS是什么
AQS全称是AbstractQueuedSynchronizer
,即抽象队列同步器
。是用来构建锁或者其他同步器组件的基础框架。在编写多线程程序时,常常需要考虑线程同步的问题,我们可以使用Java提供的synchronized
关键字或者是J.U.C包下的各种API(如Reentrantlock
、CountDownLatch
、Semaphore
等)来解决线程同步的问题,前者是JVM层面的锁,通过monitor对象来完成(字节码monitorenter和monitorexit),而后者则是API层面的锁。
下面看一下J.U.C包下几个常用的类的源码:
Reentrantlock
CountDownLatch
Semaphore
J.U.C提供了多个类来解决各种同步问题,这些类几乎都与AQS有关系。可以看到这些类中都有一个抽象内部类Sync
,而Sync
继承了AQS。这些类对外暴露了加锁\解锁等功能,实际上内部是通过AQS提供的功能来实现的。
AQS框架是由Java并发大神——Doug Lee实现的,他提出了统一的规范并简化了锁的实现。
小结:
J.U.C包提供了多个类供我们使用,如Reentrantlock
,我们可以使用它的lock
方法来加锁或者是unlock
方法来解锁,那么这些方法是如何实现加锁\解锁的呢?实际上内部调用了AQS提供的方法来实现这些功能的,因此说AQS是用来构建锁或者其他同步器组件的基础框架,是J.U.C的基石。
二、前置知识
CAS
即比较和交换指令,不了解的同学请自行查阅相关资料。
LockSupport
我们知道,多个线程竞争一把锁的时候,获取到锁的线程执行其相关代码,而没获取到锁的线程需要阻塞等待,直到锁再次可用。这就需要一个阻塞\唤醒
机制。
首先想到的就是Object
类中的wait()
和notify()
方法,调用这两个方法前,必须确保线程已经获取到对象锁(即必须是在synchronized
修饰的方法或代码块内调用)。并且,如果是先执行了notify()
,然后再执行wait()
,那么被阻塞的线程将不会被唤醒,唤醒信号丢失。
其次是使用J.U.C中Condition
类提供的await()
和signal()
方法,同样可以阻塞和唤醒线程,与wait()\notify()
相同,调用await()\signal()
方法之前也需要确保获取到锁(即通过lock()
方法进行加锁),并且,如果是先执行了signal()
,然后再执行await()
,那么被阻塞的线程将不会被唤醒,唤醒信号丢失。
接下来介绍的就是LockSupport
了,同样是用于线程的阻塞和唤醒。其中有两个方法park()
和unpark(Thread thread)
,分别对应阻塞和唤醒线程。LockSupport
使用Permit(许可证)
的概念来阻塞和唤醒线程,每个线程都有一个Permit
,Permit
只有两个取值——0和1,如果Permit
为0,即没有许可证,当前线程调用park()
方法则会被阻塞。而调用unpark(Thread t)
会使线程t的Permit
变为1,t被唤醒。
需要注意的是,即使对一个线程多次调用unpark方法,Permit的值也不会超过1,即Permit不会累加,因此多次调用unpark方法后,如果又多次调用了park方法,线程还是会被阻塞。
LockSupport
与前面两种阻塞\唤醒机制的不同之处就在于:
- 调用
park()
和unpark(Thread t)
之前并不需要获取锁 - 无论是先调用
park()
,后调用unpark(Thread t)
,还是先调用unpark(Thread t)
,后调用park()
,线程都可以被唤醒,唤醒信号不会丢失。
LockSupport
内部是通过调用Unsafe
类的方法来阻塞和唤醒线程的,而Unsafe
类中又是调用了native
方法来实现的。
在AQS中,就使用了LockSupport
来实现线程的阻塞和唤醒的。
三、源码分析
首先来看一下AQS内部的结构
- 静态内部类
Node
,下面详细分析 head
:指向Node的一个指针tail
:指向Node的一个指针- int类型变量
state
:同步状态,初始值为0。线程需要根据state的值来决定是否能获取到锁。
内部类Node
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/** 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;
// 关注这些成员变量
volatile int waitStatus; // 结点的状态
volatile Node prev; // 前驱结点
volatile Node next; // 后继结点
volatile Thread thread;// 封装的线程
}
SHARED
和EXCLUSIVE
标记是共享式还是独占式waitStatus
:结点的状态,默认是0,还有其他四种状态,分别是(CANCELLED
、SIGNAL
、CONDITION
、PROPAGATE
)prev
:当前结点的前驱结点next
:当前结点的后继结点thread
:当前结点内封装的线程
Node类是用来做什么的呢?当多个线程竞争时,未获取到锁的线程需要以某种方式管理起来,在AQS中,获取锁失败的线程,会被封装成一个Node
,多个Node
组成一个双向链表,这个双向链表构成了一个队列,这就是AQS中Q的含义。
AQS的内部结构如下图:
接下来以Reentrantlock
为例,从源码的角度来看Reentrantlock
是如何使用AQS来实现加锁和解锁的。
这里用一段简单的代码来分析Reentrantlock
内部到底做了什么。
public class AQSDemo {
ReentrantLock lock = new ReentrantLock();
public void run(){
new Thread(()->{
try {
lock.lock();
System.out.println("Thread A");
try {
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
lock.unlock();
}
},"A").start();
new Thread(()->{
try {
lock.lock();
System.out.println("Thread B");
}finally {
lock.unlock();
}
},"B").start();
new Thread(()->{
try {
lock.lock();
System.out.println("Thread C");
}finally {
lock.unlock();
}
},"C").start();
}
public static void main(String[] args) {
AQSDemo aqsDemo = new AQSDemo();
aqsDemo.run();
}
}
代码逻辑非常简单,线程A先获取到锁,中间sleep一段时间,这段时间内,由于线程B和C无法获取到锁,因此会阻塞。接下来从源码来看这个过程。
AQS内部的初始状态如下图所示
接下来线程A执行lock.lock()
方法,代码如下:
ReentrantLock#lock
public void lock() {
// sync即继承了AQS的内部类
sync.lock();
}
在Sync
类中,lock
方法是一个抽象方法,有两种实现,分别是基于公平锁的实现和基于非公平锁的实现。上面的代码在创建ReentrantLock
时,并没有传递参数,因此默认是非公平锁,接下来看非公平锁中lock
的实现:
NonfairSync#lock
final void lock() {
// 使用CAS操作将state设置为1,如果成功说明锁被获取
if (compareAndSetState(0, 1))
// 成功获取锁后,在AQS中保存当前获取到锁的线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
由于初始状态中,state值为0,因此CAS操作可以成功,线程A将state修改为1后,直接返回,执行sleep。此时,AQS内部状态如下图:
既然A已经在sleep了,此时考虑线程B,B也执行了lock()
方法,因此线程B最终也会执行下面这段代码:
NonfairSync#lock
final void lock() {
// 使用CAS操作将state设置为1,如果成功说明锁被获取
if (compareAndSetState(0, 1))
// 成功获取锁后,在AQS中保存当前获取到锁的线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);// 线程B执行这个方法
}
但是由于此时state
的值为1,因此线程B执行CAS操作会失败,转而执行acquire(1)
。
AbstractQueuedSynchronizer#acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这里主要有三个方法,这也是最关键的三个方法:
tryAcquire()
addWaiter()
acquireQueued()
tryAcquire()方法
首先来看tryAcquire()
AbstractQueuedSynchronizer#tryAcquire
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
在AQS中并没有这个方法的实现逻辑,直接抛出异常。说明这个方法是需要子类来重写的。上面线程B使用的是ReentrantLock
的非公平锁版本,因此我们查看在NonfairSync
类中该方法的实现:
NonfairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
Sync#nonfairTryAcquire
// 传入的参数值acquires = 1
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程,这里就是线程B
final Thread current = Thread.currentThread();
// 获取state的值,由上面那张AQS状态图可知此时state = 1
int c = getState();
if (c == 0) {// 线程B不执行这里
// 如果state的值为0,那么使用CAS将state修改为acquires,即1
if (compareAndSetState(0, acquires)) {
// 然后把占有锁的线程设置为当前线程
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {// 判断当前线程和持有锁的线程是否相同,当前线程是A,因此也不执行这里
// 如果当前线程就是占有锁的线程,那么计算nextc的值,即state原来的值加1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 设置state的值
setState(nextc);
return true;
}
return false;// 返回false
}
因此在线程B中,执行tryAcquire(1)
会直接返回false。
理解tryAcquire方法:在
ReentrantLock
中,tryAcquire
首先去判断state是否为0,如果是0表示当前线程有机会争夺锁,因此使用CAS修改state的值为1,修改成功表示获取到锁,然后将修改AQS中记录持有锁的线程。如果state不为0,说明锁已经被其他线程持有,那么再判断一下这个持有锁的线程是不是自己,如果是,那么就累加state的值,这里体现了可重入性。如果state既不是0,持有锁的线程也不是自己,则直接返回false,获取锁失败。如这里线程B就会直接失败。
AbstractQueuedSynchronizer#acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
刚刚说完线程B执行完了tryAcquire(1)
方法,直接返回false,因此!tryAcquire(arg)
为true,接下来执行addWaiter()
方法。
addWaiter()方法
AbstractQueuedSynchronizer#addWaiter
private Node addWaiter(Node mode) {
// 将当前线程(即线程B)封装为一个Node结点,模式为独占模式
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 队尾结点,由上面AQS状态图可知,此时队列为空,因此队尾是null
Node pred = tail;
if (pred != null) {// 线程B不执行这里
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);// 线程B直接执行这里
return node;
}
AbstractQueuedSynchronizer#enq
private Node enq(final Node node) {
for (;;) {// 自旋
Node t = tail;
if (t == null) { // 1
if (compareAndSetHead(new Node())) // 2
tail = head;// 3
} else {// 4
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
这是一个自旋操作,因此,线程B刚开始时,由于队列为空,因此队尾为null,所以会执行1,2,3,通过CAS将队列头设置为一个空的Node结点,同时将队尾也设置为这个空的Node结点。如下图:
接下来开始第二次自旋,此时tail已经不为null,因此会执行4的逻辑。即将封装了线程B的Node的prev设置为tail,并使用CAS修改队列的尾结点为Node B,并将Node 1的next设置为Node B。如下图:
到这里,addWaiter()
方法也就完成了,可以看到,addWaiter()
方法是将未获取到锁的线程封装为Node并加入到AQS的队列尾部。
从上面分析也可以看到,AQS的队列中,队列头部实际上是一个哨兵结点,里面并没有封装一个线程。
同样,线程C也会经历线程B所经历的,即首先tryAcquire()
直接会返回false,接下来执行addWaiter()
AbstractQueuedSynchronizer#addWaiter
private Node addWaiter(Node mode) {
// 将当前线程(即线程C)封装为一个Node结点,模式为独占模式
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 队尾结点,由上面AQS状态图可知,此时队列不为空,队尾是Node B
Node pred = tail;
if (pred != null) {// 线程C会先执行这里
node.prev = pred;// 当前结点的prev设置为队尾
if (compareAndSetTail(pred, node)) {// 用CAS修改队尾为当前结点
pred.next = node;// 修改Node B的next指针
return node;
}
}
enq(node);// 如果上面CAS修改队尾失败了,那么继续执行这个方法,自旋修改队尾直到成功
return node;
}
线程C在执行该方法时,由于队列不是空,因此会先用CAS尝试一次将自己加入队尾,如果CAS失败了,则继续使用enq
方法通过不断尝试,将自己加入队尾。线程C执行完addWaiter()
后如下图:
acquireQueued()方法
此时线程B和C都已经封装为一个Node
类型加入到了队列中,虽然此时线程B和C都已经加入队列中了,但是它们此时并非处于阻塞状态,线程B和C仍然是活跃的。再回顾一下上面的代码:
AbstractQueuedSynchronizer#acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
此时线程B应该执行acquireQueued()
方法了。
AbstractQueuedSynchronizer#acquireQueued
// 此时传进来的node就是Node B,arg = 1
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;// 是否被中断标志
for (;;) {// 自旋
final Node p = node.predecessor();// 该方法返回Node的前驱结点,在这里,NodeB的前驱结点就是Node1,即哨兵结点
if (p == head && tryAcquire(arg)) {// 如果p是头结点,就tryAcquire再尝试一次获取锁,假设此时线程A还没有释放锁,因此线程B不会执行下面的逻辑
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 判断线程是否应该进入阻塞状态,下面详细分析
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
在这个方法中,当前线程会根据自身结点在队列中的位置来决定是否需要再尝试一次获取锁,即如果自身结点前一个结点就是head,那么再尝试一次获取锁。根据上面的AQS状态图,虽然此时线程B前一个结点就是head,但是由于A此时还占用着锁,因此tryAcquire
会返回false。接下来线程B会执行shouldParkAfterFailedAcquire()
和parkAndCheckInterrupt()
。
AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire
// 对于线程B,传入的pred就是队列的头结点,即Node1,node就是NodeB
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;// 由上面的AQS状态图可知头结点waitStatus = 0
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
// waitStatus为0,因此使用CAS将pred的waitStatus设置为SIGNAL,即-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;// 返回false
}
执行完这个方法以后,AQS状态图如下:
AbstractQueuedSynchronizer#acquireQueued
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {// 1
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
看上面的代码,由于shouldParkAfterFailedAcquire()
直接返回了false,因此这一次自旋不会执行parkAndCheckInterrupt()
方法了,线程B开始下一轮的自旋。此时注释1处的if仍然不满足,因此又进入了shouldParkAfterFailedAcquire
方法。
AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire
// 对于线程B,传入的pred就是队列的头结点,即Node1,node就是NodeB
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;// 由上面的AQS状态图可知头结点waitStatus = -1,即Node.SIGNAL
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
// 下面代码省略
}
这时该方法直接就返回true。接下来继续执行parkAndCheckInterrupt()
方法
AbstractQueuedSynchronizer#parkAndCheckInterrupt
private final boolean parkAndCheckInterrupt() {
// 阻塞当前线程
LockSupport.park(this);
return Thread.interrupted();
}
可以看到,AQS内部使用了LockSupport
来阻塞线程,此时线程B才被阻塞了。由于acquireQueued()
内部是一个自旋,因此线程B恢复以后还会继续执行自旋的逻辑,直到离开自旋。
同样,线程C执行acquireQueued()
方法,也会经历上面的步骤,这里大家自己思考一下C执行完acquireQueued()
方法后,AQS状态图是什么样的。如下图所示:
此时线程C也进入了阻塞状态。
上面介绍完了加锁的过程,下面我们假设线程A已经完成了任务(sleep结束了),开始释放锁了。接下来我们看lock.unlock()
的逻辑。
ReentrantLock#unlock
public void unlock() {
sync.release(1);
}
同样是调用了内部的Sync
对象来实现的,而Sync
又是继承了AQS。下面来看这个release()
方法,实际上是AQS提供的方法。
AbstractQueuedSynchronizer#release
// 这里arg就是上面传进来的1
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
先来看tryRelease()
方法
AbstractQueuedSynchronizer#tryRelease
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
可以看到,AQS内部这个方法也是没有实现逻辑的,直接抛出异常,这说明需要子类来重写。那我们就来看ReentrantLock
里是怎么实现这个方法的吧。
Sync#tryRelease
// 传入的releases = 1
protected final boolean tryRelease(int releases) {
int c = getState() - releases;// getState()的值是1,因此c = 0
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {// 满足c == 0
free = true;
// 将AQS中占有锁的线程设置为null
setExclusiveOwnerThread(null);
}
// 将state设置为c,即0
setState(c);
return free;// 返回true
}
释放以后,此时AQS状态图如下:
AbstractQueuedSynchronizer#release
// 这里arg就是上面传进来的1
public final boolean release(int arg) {
// 上面分析了tryRelease返回了true
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)// 由AQS状态图可以看到这个条件是满足的
unparkSuccessor(h);// 执行该方法
return true;
}
return false;
}
AbstractQueuedSynchronizer#unparkSuccessor
// node即head结点
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;// ws此时为-1
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);// CAS将node的waitStatus设置为0
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;// head的下一个结点,这里就是NodeB
if (s == null || s.waitStatus > 0) {// mark
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);// 直接唤醒NodeB内的线程,也就是线程B了
}
这里,首先是将传进来的node,即head结点,将其waitStatus设置为0。然后看注释mark处,此时s不为null并且s的waitStatus=-1
,所以这里不会执行。这段代码的意思是,如果s的waitStatus是大于0的,即该结点已经取消了,那么就从队列尾开始查找,找到一个waitStatus<=0的结点,即还没有被取消的结点。
对于线程A来说,这里就直接唤醒线程B了。先看一下此时AQS状态图
还记得上面介绍acquireQueued()
方法吗,这个方法是一直在自旋状态,然后线程B进入了阻塞,现在线程A唤醒了线程B了,线程B则继续执行下一轮的自旋。
AbstractQueuedSynchronizer#acquireQueued
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {// 继续下一轮的自旋
final Node p = node.predecessor();// NodeB的前驱结点,即Node1
if (p == head && tryAcquire(arg)) {// 1
setHead(node);// 下面分析
p.next = null; // help GC
failed = false;
return interrupted;// 直接返回
}
if (shouldParkAfterFailedAcquire(p, node)
parkAndCheckInterrupt())// 此处被阻塞,也是此处被唤醒的
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
线程B被唤醒以后,又开始了自旋,此时注释1处,p == head是满足的,这时tryAcquire()
会成功吗?忘记该方法的去上面再看一下,此时会成功,因为线程A已经释放了锁,这时线程B再去调用tryAcquire()
方法,会将state设置为1,并将占有锁的线程设置为线程B。
接下来看setHead()
方法
AbstractQueuedSynchronizer#setHead
// 这里node就是NodeB
private void setHead(Node node) {
head = node;// 队列头设置为NodeB
node.thread = null;// NodeB内封装的线程设置为null
node.prev = null;// 前驱设置为null
}
由于线程B已经拿到锁了,因此需要移出队列,这里正是通过这个setHead()
方法,将NodeB内的线程设置为null,使得这个NodeB成为了新的哨兵结点。
看一下此时的AQS状态图:
接下来线程B释放锁,线程C获取锁,这里就不分析下去了,和上面步骤差不多,大家可以自行分析一下。
基于ReentrantLock
分析AQS源码就到这里了。大家接下来可以去看看其他API的源码,如CountDownLatch
等,内部也都是基于AQS的。
四、问题
上面再分析线程A解锁的时候,它会从队列头开始,找到下一个没有被取消的结点,并唤醒它,在上面的例子就是先唤醒了线程B。可是,不是说ReentrantLock
默认是一个非公平锁吗?这样先唤醒B后唤醒C的方式不是公平锁的体现吗?那为什么说ReentrantLock
是非公平锁呢?
再来看一下非公平版本的lock
方法
NonfairSync#lock
final void lock() {
// 使用CAS操作将state设置为1,如果成功说明锁被获取
if (compareAndSetState(0, 1))
// 成功获取锁后,在AQS中保存当前获取到锁的线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
假设线程队列里已经有线程B在等待锁了,接下来线程C尝试获取锁,因为加锁的第一步就会先尝试使用CAS占有当前的锁,如果此时线程C成功了,那么B将继续等待。这不就是非公平锁的体现了吗。
而且在公平锁版本中,tryAcquire()
方法会多加一个判断,即判断当前结点有没有前驱结点,即前面还有没有线程在等待,而非公平锁不会做这个判断。