`
BrokenDreams
  • 浏览: 254980 次
  • 性别: Icon_minigender_1
  • 来自: 北京
博客专栏
68ec41aa-0ce6-3f83-961b-5aa541d59e48
Java并发包源码解析
浏览量:100519
社区版块
存档分类
最新评论

Jdk1.7 JUC源码增量解析(6)-Phaser

阅读更多

Jdk1.7 JUC源码增量解析(6)-Phaser

作者:大飞

 

功能简介:
  • Phaser是jdk1.7提供的类似于CyclicBarrier和CountDownLatch的同步机制。
  • 它支持更灵活的使用方式:1.使用过程中可以随时注册和注销参与者;2.不同于CyclicBarrier,分离出"到达"和"等待"机制;3.支持结束,默认情况下,当没有参与者的时候Phaser就结束了;4.支持层级Phaser结构;5.提供针对内部状态的监控方法;
源码分析:
  • 先看一下内部结构:
    /**
     * 主状态,分为4部分:
     *
     * 未到达计数  -- 还没有到达栅栏的参与者计数。  (bits  0-15)
     * parties     -- 栅栏全部参与者的计数。        (bits 16-31)
     * phase       -- 栅栏当前所处的阶段            (bits 32-62)
     * terminated  -- 栅栏的结束标记                (bit  63 / sign)
     *
     * 一个没有注册参与者的phaser的主状态中会有0个参与者计数
     * 和1个未到达计数。
     */
    private volatile long state;
    private static final int  MAX_PARTIES     = 0xffff;
    private static final int  MAX_PHASE       = Integer.MAX_VALUE;
    private static final int  PARTIES_SHIFT   = 16;
    private static final int  PHASE_SHIFT     = 32;
    private static final int  UNARRIVED_MASK  = 0xffff;      // to mask ints
    private static final long PARTIES_MASK    = 0xffff0000L; // to mask longs
    private static final long TERMINATION_BIT = 1L << 63;
    // some special values
    private static final int  ONE_ARRIVAL     = 1;
    private static final int  ONE_PARTY       = 1 << PARTIES_SHIFT;
    private static final int  EMPTY           = 1;
       主状态是Phaser中的一个重要的域,它是一个long型值,内部包含了4部分内容:未到达栅栏的参与者的计数、栅栏的全部参与者计数、当前栅栏处于的阶段(和CyclicBarrier每次用完后会产生一个新的generation的行为类似)、结束标记。

       主状态这样设计(将状态封装到一个原子的long域)可以从两方面提高性能,一个是对状态的编解码简单高效、另一个是可以减小竞争窗口(空间)。 

 

    /**
     * 当前phaser的父phaser, 如果没有父phaser,这个域为null。
     */
    private final Phaser parent;
    /**
     * phaser树的根节点. 如果当前phaser不在一棵树内,这个域等于自身。
     */
    private final Phaser root;

 

    private final AtomicReference<QNode> evenQ;
    private final AtomicReference<QNode> oddQ;
    private AtomicReference<QNode> queueFor(int phase) {
        return ((phase & 1) == 0) ? evenQ : oddQ;
    }
       Phaser中使用Treiber Stack结构来保存等待线程,为了在一些情况下避免竞争,Phaser内部使用了2个Treiber Stack,evenQ和addQ,分别在内部phase为偶数和奇数下交替使用。
 

       再看下QNode这个类,先看下结构:

    static final class QNode implements ForkJoinPool.ManagedBlocker {
        final Phaser phaser;
        final int phase;
        final boolean interruptible;
        final boolean timed;
        boolean wasInterrupted;
        long nanos;
        long lastTime;
        volatile Thread thread; // nulled to cancel wait
        QNode next;
        QNode(Phaser phaser, int phase, boolean interruptible,
              boolean timed, long nanos) {
            this.phaser = phaser;
            this.phase = phase;
            this.interruptible = interruptible;
            this.nanos = nanos;
            this.timed = timed;
            this.lastTime = timed ? System.nanoTime() : 0L;
            thread = Thread.currentThread();
        }
       QNode内部结构很简单,就是保存了一些线程等待的相关信息,还有指向下一个QNode的域。这里要注意的是QNode实现了ForkJoinPool.ManagedBlocker,作用就是当包含ForkJoinWorkerThread的QNode阻塞的时候,ForkJoinPool内部会增加一个工作线程来保证并行度(具体的内容可以回头看一下ForkJoin框架的分析文章)。

      继续看下QNode中的方法,首先是isReleasable方法:

        public boolean isReleasable() {
            if (thread == null)
                return true;
            if (phaser.getPhase() != phase) {
                thread = null;
                return true;
            }
            if (Thread.interrupted())
                wasInterrupted = true;
            if (wasInterrupted && interruptible) {
                thread = null;
                return true;
            }
            if (timed) {
                if (nanos > 0L) {
                    long now = System.nanoTime();
                    nanos -= now - lastTime;
                    lastTime = now;
                }
                if (nanos <= 0L) {
                    thread = null;
                    return true;
                }
            }
            return false;
        }
       isReleasable方法中的逻辑比较简单:当QNode中的thread为null、或者和phaser的阶段值不相等、或者被中断、或者等待超时,方法都返回true。

       再看下block方法:

        public boolean block() {
            if (isReleasable())
                return true;
            else if (!timed)
                LockSupport.park(this);
            else if (nanos > 0)
                LockSupport.parkNanos(this, nanos);
            return isReleasable();
        }
       block方法就是一个阻塞的过程。
 
 
  • 通过一些示例来分析下主要功能源码。

       首先看一个示例,使用方式类似于Count为1的CountDownLatch:

	public static void main(String[] args) {
		
		final Phaser phaser = new Phaser(1);
		for(int i=0;i<10;i++){
			phaser.register();
			new Thread(new Runnable() {
				@Override
				public void run() {
					phaser.arriveAndAwaitAdvance();
					System.out.println(Thread.currentThread() +" start!!!!");
				}
			}).start();
		}
		try {
			TimeUnit.SECONDS.sleep(5);
			phaser.arriveAndDeregister();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
	}
       上面的示例会在main线程启动后,sleep5秒,然后上面启动的10个线程才会打印。
 

       示例中首先构造了一个Phaser,我们先看下构造方法:

    /**
     * 创建一个没有初始参与者的phaser,默认没有父级phaser,初始
     * phase值为0。如果有任何线程想要使用这个phaser,都必须先
     * 注册这个phaser。
     */
    public Phaser() {
        this(null, 0);
    }
    /**
     * 创建一个有初始参与者(未到达)数量的phaser,默认没有父级phaser,初始
     * phase值为0。
     */
    public Phaser(int parties) {
        this(null, parties);
    }

    public Phaser(Phaser parent) {
        this(parent, 0);
    }
    /**
     * 创建一个有给定父级phaser和初始参与者(未到达)数量的phaser, 
     * 如果给定的父级phaser不为null,并且给定的参与者数量大于0, 
     * 当前的子phaser相当于注册了父phaser。
     */
    public Phaser(Phaser parent, int parties) {
        //parties不能超过65535
        if (parties >>> PARTIES_SHIFT != 0)
            throw new IllegalArgumentException("Illegal number of parties");
        int phase = 0;
        this.parent = parent;
        if (parent != null) {
            //如果父级Phaser不为空。
            final Phaser root = parent.root;
            this.root = root;
            //共享父级的线程等待队列。
            this.evenQ = root.evenQ;
            this.oddQ = root.oddQ;
            if (parties != 0)
                //如果当前phaser的参与者不为0,那么注册一个参与者到父级,注意这里是一个。
                phase = parent.doRegister(1);
        }
        else {
            //如果父级为空。
            //root就是自身,
            this.root = this;
            this.evenQ = new AtomicReference<QNode>();
            this.oddQ = new AtomicReference<QNode>();
        }
        this.state = (parties == 0) ? (long)EMPTY :
            ((long)phase << PHASE_SHIFT) |
            ((long)parties << PARTIES_SHIFT) |
            ((long)parties);
    }
       构造方法很简单,只要注意两点:1.当有父级Phaser的时候,子Phaser会共享父Phaser的线程等待队列。如果子Phaser的参与者不为0,那么会以一个参与者的身份注册到父Phaser上,注意是一个!!;2.最后初始化state的时候,如果没有参与者,state就是EMPTY,等于1。
 

       示例中接下来会在每次新建线程之前调用register方法来注册参与者,看下这个方法:

    /**
     * 添加一个新的未到达的参与者到当前phaser。如果当前正在onAdvance方法, 
     * 的执行过程中,这个方法会等待其完成再返回。如果当前phaser有父phaser,
     * 并且当前phaser之前没有注册的参与者,phaser会注册到父phaser上。 
     * 如果当前phaser结束了,那么方法不会产生任何作用,并返回一个负数。 
     */
    public int register() {
        return doRegister(1);
    }

       register内部调用的是doRegister,看下这个方法: 

    private int doRegister(int registrations) {
        //调整主状态,将给定的数值加到总参与者和未到达参数者数量上。
        long adj = ((long)registrations << PARTIES_SHIFT) | registrations;
        final Phaser parent = this.parent;
        int phase;
        for (;;) {
            long s = state;
            int counts = (int)s;
            int parties = counts >>> PARTIES_SHIFT;
            int unarrived = counts & UNARRIVED_MASK;
            if (registrations > MAX_PARTIES - parties)
                //注册的参与者数量和已存在的参与者数量加起来不能超过最大参与者数量。
                throw new IllegalStateException(badRegister(s));
            else if ((phase = (int)(s >>> PHASE_SHIFT)) < 0)
                //如果phaser已经结束,那么直接退出循环。
                break;
            else if (counts != EMPTY) {
                //如果不是第一个注册。
                if (parent == null || reconcileState() == s) {
                    if (unarrived == 0)
                        // 如果当前未到达数量为0,说明需要进入下一阶段了,这里要等待一下root进入下一阶段。             
                        root.internalAwaitAdvance(phase, null);
                    //否则原子更新主状态。
                    else if (UNSAFE.compareAndSwapLong(this, stateOffset,
                                                       s, s + adj))
                        break;
                }
            }
            else if (parent == null) {
                // 第一个root注册(没有父级)。
                // 算出下一个主状态。
                long next = ((long)phase << PHASE_SHIFT) | adj;
                // 原子更新主状态。
                if (UNSAFE.compareAndSwapLong(this, stateOffset, s, next))
                    break;
            }
            else {
                //第一个子phaser的注册,需要加锁。
                synchronized (this) {               
                    if (state == s) {               //检测一下状态有没有变化。
                        parent.doRegister(1);       //由于是第一次注册,所以需要向父类注册一下。
                        do {                        //更新到下一个主状态。
                            phase = (int)(root.state >>> PHASE_SHIFT);
                            // assert phase < 0 || (int)state == EMPTY;
                        } while (!UNSAFE.compareAndSwapLong
                                 (this, stateOffset, state,
                                  ((long)phase << PHASE_SHIFT) | adj));
                        break;
                    }
                }
            }
        }
        return phase;
    }
       总结一下doRegister方法:
              1.当前不是第一个注册者(参与者)。如果当前主状态中未到达数量为0,说明参与者已经全部到达栅栏,当前Phaser正在进入下一阶段过程中,需要等待这一过程完成(可能会阻塞);否则会原子更新当前的主状态,加一个总参与者数量和一个未到达参与者数量。(过程中如果parent不为null,需要调用reconcileState调整一下当前主状态,和root的主状态保持一致)
              2.当前是第一个注册者且当前Phaser没有父级Phaser。直接原子更新当前主状态,加一个总参与者数量和一个未到达参与者数量。
              3.当前是第一个注册者且当前Phaser有父级Phaser。需要加锁操作,首先向父级Phaser注册一个参与者,然后原子更新主状态,加一个总参与者数量和一个未到达参与者数量。
 

       doRegister方法中会调用reconcileState来调整状态,看下这个方法实现:

    private long reconcileState() {
        final Phaser root = this.root;
        long s = state;
        if (root != this) {
            int phase, u, p;
            // CAS root phase with current parties; possibly trip unarrived
            while ((phase = (int)(root.state >>> PHASE_SHIFT)) !=
                   (int)(s >>> PHASE_SHIFT) &&
                   !UNSAFE.compareAndSwapLong
                   (this, stateOffset, s,
                    s = (((long)phase << PHASE_SHIFT) |
                         (s & PARTIES_MASK) |
                         ((p = (int)s >>> PARTIES_SHIFT) == 0 ? EMPTY :
                          (u = (int)s & UNARRIVED_MASK) == 0 ? p : u))))
                s = state;
        }
        return s;
    }
       可见,这个方法要做的事情就是将当前Phaser和root Phaser的phase值调整为一致的。
 

       doRegister方法中,如果当前Phaser正在进入下一阶段过程中,需要等待这个过程完成,会调用internalAwaitAdvance方法,看下这个方法:

    /** cpu核数 */
    private static final int NCPU = Runtime.getRuntime().availableProcessors();
    /**
     * 单个参与者阻塞等待栅栏进入下一个阶段之前的自旋次数。
     * 在多核处理器下,一次性完全的阻塞和唤醒一大批线程通常比较慢,
     * 所以我们这里使用了一个可调整的自旋次数值在避免这种情况。
     * 当一个参与者线程在internalAwaitAdvance方法中阻塞之前发现了
     * 其他到达的线程,并且有cpu资源可用,那么这个参与者线程会在阻塞 
     * 之前自旋SPINS_PER_ARRIVAL或者更多次。
     */
    static final int SPINS_PER_ARRIVAL = (NCPU < 2) ? 1 : 1 << 8;

    private int internalAwaitAdvance(int phase, QNode node) {
        releaseWaiters(phase-1);          // 清空不用的等待线程队列(Treiber Stack)。
        boolean queued = false;           // 入队标识。
        int lastUnarrived = 0;            // 用于在发生变化时增加自旋次数。
        int spins = SPINS_PER_ARRIVAL;
        long s;
        int p;
        while ((p = (int)((s = state) >>> PHASE_SHIFT)) == phase) {
            if (node == null) {          
                int unarrived = (int)s & UNARRIVED_MASK;
                //如果未到达参与者数量发生了变化,且变化后的未到达数量小于cpu核数,需要增加自旋次数。
                if (unarrived != lastUnarrived &&
                    (lastUnarrived = unarrived) < NCPU)
                    spins += SPINS_PER_ARRIVAL;
                //获取并清除当前线程中断标记。
                boolean interrupted = Thread.interrupted();
                if (interrupted || --spins < 0) { 
                    //如果当前线程被中断,或者自旋次数用完。创建一个(不可中断的)节点。
                    node = new QNode(this, phase, false, false, 0L);
                    node.wasInterrupted = interrupted;
                }
            }
            else if (node.isReleasable()) // done or aborted
                break;
            else if (!queued) {           // 将节点加入队列首部。
                AtomicReference<QNode> head = (phase & 1) == 0 ? evenQ : oddQ;
                QNode q = node.next = head.get();
                if ((q == null || q.phase == phase) &&
                    (int)(state >>> PHASE_SHIFT) == phase) // avoid stale enq
                    queued = head.compareAndSet(q, node);
            }
            else {
                try {
                    //阻塞等待。
                    ForkJoinPool.managedBlock(node);
                } catch (InterruptedException ie) {
                    node.wasInterrupted = true;
                }
            }
        }
        if (node != null) {
            if (node.thread != null)
                node.thread = null;       // avoid need for unpark()
            if (node.wasInterrupted && !node.interruptible)
                //不可中断模式下要传递中断。
                Thread.currentThread().interrupt();
            if (p == phase && (p = (int)(state >>> PHASE_SHIFT)) == phase)
                return abortWait(phase); // possibly clean up on abort
        }
        releaseWaiters(phase);
        return p;
    }
       internalAwaitAdvance中的主要逻辑过程就是当前(参与者)线程等待Phaser进入下一个阶段(就是phase值变化)。有些细节需要注意一下。
       如果传入的node为null,过程如下:
       第1步,等待分两个阶段,自旋等待和阻塞等待,首先自旋给定的次数,如果自旋过程中未到达参与者数量发生变化,且变化后的为未到达参与者数量小于CPU处理器核数,那么自选次数会增加SPINS_PER_ARRIVAL次。如果自旋次数用完后或者当前线程被中断了,那么会创建一个不可中断模式的节点,节点中保存当前线程及其他信息。
       第2步,将上面创建的节点加入线程等待队列的首部(类似于压栈,因为线程等待队列就是Treiber Stack)。
       第3步,当前线程开始阻塞等待。
       第4步,当前线程被唤醒后,如果是不可中断模式的节点,需要向上层传递中断状态;如果当前phaser还是没有进入下一阶段,那么调用abortWait,做放弃等待操作。
 
       如果传入的node不为null,过程和上面类似,只是没有第1步。 

       注意整个internalAwaitAdvance过程的前后都会清空一下当前不用的等待线程队列(两个奇偶队列交替使用),看下这个方法:

    private void releaseWaiters(int phase) {
        QNode q;   // first element of queue
        Thread t;  // its thread
        AtomicReference<QNode> head = (phase & 1) == 0 ? evenQ : oddQ;
        while ((q = head.get()) != null &&
               q.phase != (int)(root.state >>> PHASE_SHIFT)) {
            if (head.compareAndSet(q, q.next) &&
                (t = q.thread) != null) {
                q.thread = null;
                LockSupport.unpark(t);
            }
        }
    }
       逻辑很简单,就是把等待线程队列里面的节点都移除了,如果节点有线程的话,将线程唤醒。
 

       internalAwaitAdvance中还可能会调用一个放弃等待的abortWait方法,看下:

    private int abortWait(int phase) {
        AtomicReference<QNode> head = (phase & 1) == 0 ? evenQ : oddQ;
        for (;;) {
            Thread t;
            QNode q = head.get();
            int p = (int)(root.state >>> PHASE_SHIFT);
            if (q == null || ((t = q.thread) != null && q.phase == p))
                return p;
            if (head.compareAndSet(q, q.next) && t != null) {
                q.thread = null;
                LockSupport.unpark(t);
            }
        }
    }
       这个方法和releaseWaiters方法有些区别:
       releaseWaiters方法是用来清空当前不适用的等待线程队列的;abortWait方法是将当前正在使用的队列中由于超时或者中断不在等待当前phaser的下一阶段的节点移除。
 
 

       再回头看我们的示例,for循环中新建的线程在运行时会先调用arriveAndAwaitAdvance方法,然后会在这个方法上等待,直到主线程调用了arriveAndDeregister。看下arriveAndAwaitAdvance这个方法:

    public int arriveAndAwaitAdvance() {
        final Phaser root = this.root;
        for (;;) {
            //获取主状态
            long s = (root == this) ? state : reconcileState();
            //获取phase值
            int phase = (int)(s >>> PHASE_SHIFT);
            int counts = (int)s;
            //获取当前未到达参与者计数,就是之前的未到达计数减1。
            int unarrived = (counts & UNARRIVED_MASK) - 1;
            if (phase < 0)
                return phase; //如果当前phaser已经结束,退出。
            else if (counts == EMPTY || unarrived < 0) {
                if (reconcileState() == s)
                    throw new IllegalStateException(badArrive(s)); //非法状态。
            }
            //主状态中未到达参与者的计数减1。
            else if (UNSAFE.compareAndSwapLong(this, stateOffset, s,
                                               s -= ONE_ARRIVAL)) {
                if (unarrived != 0)
                    //如果还有未到达的参与者,等待。
                    return root.internalAwaitAdvance(phase, null);
                if (root != this)
                    //如果当前是子级Phaser,要等待父级进入下一阶段。
                    return parent.arriveAndAwaitAdvance();
                long n = s & PARTIES_MASK;  // base of next state
                //这里算出来的是总参与者的数量。
                int nextUnarrived = (int)n >>> PARTIES_SHIFT;
                //调用onAdvance方法。
                if (onAdvance(phase, nextUnarrived))
                    //如果onAdvance方法返回true,给主状态中设置结束标记。
                    n |= TERMINATION_BIT;
                else if (nextUnarrived == 0)
                    //如果总参与者数量变为0,那么将主状态设置为没有参与者的特殊状态。
                    n |= EMPTY;
                else
                    //否则,重置未到达参与者数量。
                    n |= nextUnarrived;
                //算出下一个phase值。
                int nextPhase = (phase + 1) & MAX_PHASE;
                //设置到主状态上。
                n |= (long)nextPhase << PHASE_SHIFT;
                //原子更新主状态。
                if (!UNSAFE.compareAndSwapLong(this, stateOffset, s, n))
                    //如果发生竞争,返回phase值,如果当前phaser结束,返回负数。
                    return (int)(state >>> PHASE_SHIFT); // terminated
                //清空上一阶段使用的线程等待队列。
                releaseWaiters(phase);
                //最后返回上面算出来的nextPhase值。
                return nextPhase;
            }
        }
    }
       arriveAndAwaitAdvance方法中的主要逻辑如下:
              首先将主状态中的未到达参与者数量减1,然后判断未到达参与者数量是否为0。如果不为0,当前线程会等待其他参与者到来;如果为0,说明当前(线程)是最后一个参与者,那么会继续算出下一个阶段的主状态,然后更新到Phaser中。计算过程中会通过调用onAdvance方法,判断当前Phaser是否结束,还会重置未到达参与者数量等。
 

       看一下arriveAndAwaitAdvance方法中调用的onAdvance方法:

    protected boolean onAdvance(int phase, int registeredParties) {
        return registeredParties == 0;
    }
       onAdvance方法的作用就是来控制Phaser是否结束,默认行为是当总参与者数量为0时,Phaser就结束了。具体使用时可以覆盖这个方法,实现合适的策略来控制Phaser的结束时机。
 

       回到示例的最后,主线程休眠5秒中,然后调用了arriveAndDeregister方法,看下这个方法:

    public int arriveAndDeregister() {
        return doArrive(true);
    }

       这个方法表示一个参与者到达栅栏,并且将自己从phaser上注销。内部调用了doArrive方法:

    private int doArrive(boolean deregister) {
        int adj = deregister ? ONE_ARRIVAL|ONE_PARTY : ONE_ARRIVAL;
        final Phaser root = this.root;
        for (;;) {
            long s = (root == this) ? state : reconcileState();
            int phase = (int)(s >>> PHASE_SHIFT);
            int counts = (int)s;
            int unarrived = (counts & UNARRIVED_MASK) - 1;
            if (phase < 0)
                return phase;
            else if (counts == EMPTY || unarrived < 0) {
                if (root == this || reconcileState() == s)
                    throw new IllegalStateException(badArrive(s));
            }
            else if (UNSAFE.compareAndSwapLong(this, stateOffset, s, s-=adj)) {
                if (unarrived == 0) {
                    long n = s & PARTIES_MASK;  // base of next state
                    int nextUnarrived = (int)n >>> PARTIES_SHIFT;
                    if (root != this)
                        //这里注意下:如果当前子phaser中没有参与者了,就要于从父phaser中将当前子phaser注销。
                        return parent.doArrive(nextUnarrived == 0);
                    if (onAdvance(phase, nextUnarrived))
                        n |= TERMINATION_BIT;
                    else if (nextUnarrived == 0)
                        n |= EMPTY;
                    else
                        n |= nextUnarrived;
                    n |= (long)((phase + 1) & MAX_PHASE) << PHASE_SHIFT;
                    UNSAFE.compareAndSwapLong(this, stateOffset, s, n);
                    releaseWaiters(phase);
                }
                return phase;
            }
        }
    }
       可见,这个方法中的逻辑和arriveAndAwaitAdvance方法类似,区别是如果参数deregister为true,会在主状态中减去一个参与者计数。
 
       示例中使用到的方法都分析过了,再回头看示例,会发现逻辑细节也清晰了:
       首先,构造了有1个参与者的Phaser。
       其次,开启10个线程,每个线程作为1个参与者注册到上面创建的Phaser上。然后启动线程,在线程执行具体逻辑前等待其他参与者到达栅栏(arriveAndAwaitAdvance)。
       最后,主线程到达栅栏并将自己注销。这时,所有的参与者(一共11个)都已经到达栅栏,上面的10个参与者(线程)就可以通过栅栏,执行具体逻辑了。
 

       再看一个示例,使用方式类似于Count为N的CountDownLatch:

	public static void main(String[] args) {
		final Phaser phaser = new Phaser();
		for(int i=0;i<10;i++){
			phaser.register();
			new Thread(new Runnable() {
				@Override
				public void run() {
					try {
						TimeUnit.SECONDS.sleep(new Random().nextInt(5));
						System.out.println(Thread.currentThread() +" is ready!");
						phaser.arriveAndAwaitAdvance();
						System.out.println(Thread.currentThread() +" start!!!!");
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}).start();
		}
	}
       这个示例要更简单一些,就是等待所有的线程都ready后,然后一起start。涉及到的Phaser方法都分析过了。
 

       继续看一个示例,和第一个示例差不多,但有一点区别:第一个示例中线程只执行了一次逻辑,现在要求线程执行若干次逻辑:

	public static void main(String[] args) {
		final Phaser phaser = new Phaser(1){
			protected boolean onAdvance(int phase, int registeredParties) {
				System.out.println("now phase is " + phase);
				return phase >= 5 || registeredParties == 0;
			}
		};
		for(int i=0;i<5;i++){
			phaser.register();
			final int time = i;
			new Thread(new Runnable() {
				@Override
				public void run() {
					do{
						phaser.arriveAndAwaitAdvance();
						System.out.println(Thread.currentThread() +" start-"+time+"!!!!");
					}while(!phaser.isTerminated());
				}
			}).start();
		}
		try {
			TimeUnit.SECONDS.sleep(5);
			System.out.println("arriveAndDeregister...");
			phaser.arriveAndDeregister();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
       本示例和第一个示例区别只是覆盖了onAdvance方法,加上了阶段限制逻辑:当phase大于等于5时,Phaser就结束了,也就是说,线程里面的逻辑会执行6次(phase从0开始)。
       前面提高过,第一个示例对Phaser的使用方式类似于Count为1的CountDownLatch;而本示例中的Phaser在第一次使用时相当于Count为1的CountDownLatch,当主线程到达并注销自己(相当于countDown),前面启动的子线程就会(通过栅栏)执行逻辑了。但随后的使用又相当于是一个CyclicBarrier,示例中第一次的参与者包括主线程,一共是6个。主线程在Phaser中注销后,后面就只有5个参与者,所以每当这个5个线程都达到栅栏时,它们会一起通过,所以相当于一个CyclicBarrier了。同时我们可以看到,onAdvance中也可以定制类似于CyclicBarrier的barrierAction的逻辑,我们这里是打印了一个语句。

       示例输出如下:

arriveAndDeregister...
now phase is 0
Thread[Thread-4,5,main] start-4!!!!
Thread[Thread-1,5,main] start-1!!!!
Thread[Thread-0,5,main] start-0!!!!
Thread[Thread-2,5,main] start-2!!!!
Thread[Thread-3,5,main] start-3!!!!
now phase is 1
Thread[Thread-0,5,main] start-0!!!!
Thread[Thread-4,5,main] start-4!!!!
Thread[Thread-2,5,main] start-2!!!!
Thread[Thread-3,5,main] start-3!!!!
Thread[Thread-1,5,main] start-1!!!!
now phase is 2
Thread[Thread-1,5,main] start-1!!!!
Thread[Thread-3,5,main] start-3!!!!
Thread[Thread-2,5,main] start-2!!!!
Thread[Thread-4,5,main] start-4!!!!
Thread[Thread-0,5,main] start-0!!!!
now phase is 3
Thread[Thread-0,5,main] start-0!!!!
Thread[Thread-3,5,main] start-3!!!!
Thread[Thread-1,5,main] start-1!!!!
Thread[Thread-2,5,main] start-2!!!!
Thread[Thread-4,5,main] start-4!!!!
now phase is 4
Thread[Thread-4,5,main] start-4!!!!
Thread[Thread-0,5,main] start-0!!!!
Thread[Thread-1,5,main] start-1!!!!
Thread[Thread-3,5,main] start-3!!!!
Thread[Thread-2,5,main] start-2!!!!
now phase is 5
Thread[Thread-2,5,main] start-2!!!!
Thread[Thread-3,5,main] start-3!!!!
Thread[Thread-4,5,main] start-4!!!!
Thread[Thread-0,5,main] start-0!!!!
Thread[Thread-1,5,main] start-1!!!!

                  

 

 

  • 最后,看一下示例中未涉及到的代码。
    private static int unarrivedOf(long s) {
        int counts = (int)s;
        return (counts == EMPTY) ? 0 : counts & UNARRIVED_MASK;
    }
    private static int partiesOf(long s) {
        return (int)s >>> PARTIES_SHIFT;
    }
    private static int phaseOf(long s) {
        return (int)(s >>> PHASE_SHIFT);
    }
    private static int arrivedOf(long s) {
        int counts = (int)s;
        return (counts == EMPTY) ? 0 :
            (counts >>> PARTIES_SHIFT) - (counts & UNARRIVED_MASK);
    }

       这组方法可以从主状态中获取各个状态,是一组"拆包"方法。在具体功能方法内部可能不会调用这些方法,而是手工内联进去。

 

 

    private String badArrive(long s) {
        return "Attempted arrival of unregistered party for " +
            stateToString(s);
    }

    private String badRegister(long s) {
        return "Attempt to register more than " +
            MAX_PARTIES + " parties for " + stateToString(s);
    }
    private String stateToString(long s) {
        return super.toString() +
            "[phase = " + phaseOf(s) +
            " parties = " + partiesOf(s) +
            " arrived = " + arrivedOf(s) + "]";
    }

       这badArrice和badRegister方法用于在逻辑中出现非法参与注册情况时,提供提示消息。stateToString方法将state转化为易懂的字符串形式,用于支持前面两个方法和toString方法。 

 

 

    public int bulkRegister(int parties) {
        if (parties < 0)
            throw new IllegalArgumentException();
        if (parties == 0)
            return getPhase();
        return doRegister(parties);
    }
    public final int getPhase() {
        return (int)(root.state >>> PHASE_SHIFT);
    }

       相对于register的批量注册方法。 

 

 

    public int arrive() {
        return doArrive(false);
    }

       和arriveAndDeregister类似,只是不会注销当前参与者。 

 

 

    public int awaitAdvance(int phase) {
        final Phaser root = this.root;
        long s = (root == this) ? state : reconcileState();
        int p = (int)(s >>> PHASE_SHIFT);
        if (phase < 0)
            return phase;
        if (p == phase)
            return root.internalAwaitAdvance(phase, null);
        return p;
    }

       等待phase值表示的阶段,如果当前phase和给定的phase不一致,直接返回当前的phase值。 

 

 

    public int awaitAdvanceInterruptibly(int phase)
        throws InterruptedException {
        final Phaser root = this.root;
        long s = (root == this) ? state : reconcileState();
        int p = (int)(s >>> PHASE_SHIFT);
        if (phase < 0)
            return phase;
        if (p == phase) {
            QNode node = new QNode(this, phase, true, false, 0L);
            p = root.internalAwaitAdvance(phase, node);
            if (node.wasInterrupted)
                throw new InterruptedException();
        }
        return p;
    }

       awaitAdvance方法逻辑一样,但支持中断。 

 

 

    public int awaitAdvanceInterruptibly(int phase,
                                         long timeout, TimeUnit unit)
        throws InterruptedException, TimeoutException {
        long nanos = unit.toNanos(timeout);
        final Phaser root = this.root;
        long s = (root == this) ? state : reconcileState();
        int p = (int)(s >>> PHASE_SHIFT);
        if (phase < 0)
            return phase;
        if (p == phase) {
            QNode node = new QNode(this, phase, true, true, nanos);
            p = root.internalAwaitAdvance(phase, node);
            if (node.wasInterrupted)
                throw new InterruptedException();
            else if (p == phase)
                throw new TimeoutException();
        }
        return p;
    }

       awaitAdvance方法逻辑一样,但支持中断和超时。 

 

 

    public void forceTermination() {
        // Only need to change root state
        final Phaser root = this.root;
        long s;
        while ((s = root.state) >= 0) {
            //给主状态中添加结束标记。
            if (UNSAFE.compareAndSwapLong(root, stateOffset,
                                          s, s | TERMINATION_BIT)) {
                //清空奇偶线程等待队列。
                releaseWaiters(0);
                releaseWaiters(1);
                return;
            }
        }
    }
       强制结束Phaser。
 
 

       最后看一些支持监控的方法:

    public final int getPhase() {
        return (int)(root.state >>> PHASE_SHIFT);
    }

    public int getRegisteredParties() {
        return partiesOf(state);
    }

    public int getArrivedParties() {
        return arrivedOf(reconcileState());
    }

    public int getUnarrivedParties() {
        return unarrivedOf(reconcileState());
    }

    public Phaser getParent() {
        return parent;
    }

    public Phaser getRoot() {
        return root;
    }

    public boolean isTerminated() {
        return root.state < 0L;
    }
       方法都非常简单,不啰嗦了。
 

       OK,JDK1.7 Phaser的代码解析完毕!  

 

 

       参见:Jdk1.6 JUC源码解析(9)-CountDownLatch

       参见:Jdk1.6 JUC源码解析(11)-CyclicBarrier

       参见:Jdk1.7 JUC源码增量解析(5)-ForkJoin-ForkJoin框架其他过程及方法

 

 

 

分享到:
评论

相关推荐

    星之语明星周边产品销售网站的设计与实现-springboot毕业项目,适合计算机毕-设、实训项目、大作业学习.zip

    Spring Boot是Spring框架的一个模块,它简化了基于Spring应用程序的创建和部署过程。Spring Boot提供了快速启动Spring应用程序的能力,通过自动配置、微服务支持和独立运行的特性,使得开发者能够专注于业务逻辑,而不是配置细节。Spring Boot的核心思想是约定优于配置,它通过自动配置机制,根据项目中添加的依赖自动配置Spring应用。这大大减少了配置文件的编写,提高了开发效率。Spring Boot还支持嵌入式服务器,如Tomcat、Jetty和Undertow,使得开发者无需部署WAR文件到外部服务器即可运行Spring应用。 Java是一种广泛使用的高级编程语言,由Sun Microsystems公司(现为Oracle公司的一部分)在1995年首次发布。Java以其“编写一次,到处运行”(WORA)的特性而闻名,这一特性得益于Java虚拟机(JVM)的使用,它允许Java程序在任何安装了相应JVM的平台上运行,而无需重新编译。Java语言设计之初就是为了跨平台,同时具备面向对象、并发、安全和健壮性等特点。 Java语言广泛应用于企业级应用、移动应用、桌面应用、游戏开发、云计算和物联网等领域。它的语法结构清晰,易于学习和使用,同时提供了丰富的API库,支持多种编程范式,包括面向对象、命令式、函数式和并发编程。Java的强类型系统和自动内存管理减少了程序错误和内存泄漏的风险。随着Java的不断更新和发展,它已经成为一个成熟的生态系统,拥有庞大的开发者社区和持续的技术创新。Java 8引入了Lambda表达式,进一步简化了并发编程和函数式编程的实现。Java 9及以后的版本继续在模块化、性能和安全性方面进行改进,确保Java语言能够适应不断变化的技术需求和市场趋势。 MySQL是一个关系型数据库管理系统(RDBMS),它基于结构化查询语言(SQL)来管理和存储数据。MySQL由瑞典MySQL AB公司开发,并于2008年被Sun Microsystems收购,随后在2010年,Oracle公司收购了Sun Microsystems,从而获得了MySQL的所有权。MySQL以其高性能、可靠性和易用性而闻名,它提供了多种特性来满足不同规模应用程序的需求。作为一个开源解决方案,MySQL拥有一个活跃的社区,不断为其发展和改进做出贡献。它的多线程功能允许同时处理多个查询,而其优化器则可以高效地执行复杂的查询操作。 随着互联网和Web应用的快速发展,MySQL已成为许多开发者和公司的首选数据库之一。它的可扩展性和灵活性使其能够处理从小规模应用到大规模企业级应用的各种需求。通过各种存储引擎,MySQL能够适应不同的数据存储和检索需求,从而为用户提供了高度的定制性和性能优化的可能性。

    精选毕设项目-新浪读书.zip

    精选毕设项目-新浪读书

    智慧农业平台解决方案.pptx

    智慧农业平台解决方案

    精选毕设项目-小程序地图Demo.zip

    精选毕设项目-小程序地图Demo

    操作系统课程设计: 并发与调度

    实验目的 在本实验中,通过对事件和互斥体对象的了解,来加深对 Windows Server 2016 线程同步的理解。 1)回顾系统进程、线程的有关概念,加深对 Windows Server 2016 线程的理解; 2)了解事件和互斥体对象; 3)通过分析实验程序,了解管理事件对象的API; 4)了解在进程中如何使用事件对象; 5)了解在进程中如何使用互斥体对象; 6)了解父进程创建子进程的程序设计方法。 程序清单 清单2-1 1.// event 项目   2.#include <windows.h>   3.#include <iostream>   4.using namespace std;   5.   6.// 以下是句柄事件。实际中很可能使用共享的包含文件来进行通讯   7.static LPCTSTR g_szContinueEvent = "w2kdg.EventDemo.event.Continue";   8.   9.// 本方法只是创建了一个进程的副本,以子进程模式 (由命令行指定) 工作    10.BOOL CreateChild()   11.{  

    三相VIENNA整流,维也纳整流器simulink仿真 输入电压220v有效值 输出电压800v纹波在1%以内 0.1s后系统稳定 功率因数>0.95 电流THD<5% 开关频率20k 图一为拓扑,可

    三相VIENNA整流,维也纳整流器simulink仿真 输入电压220v有效值 输出电压800v纹波在1%以内 0.1s后系统稳定 功率因数>0.95 电流THD<5% 开关频率20k 图一为拓扑,可以看到功率因数和THD以及输出电压 图二为直流输出电压 图三四为a相电压电流 图五为控制等计算的总体框图 图六为svpwm调制框图 图七为双闭环控制图八为输出调制波 可作为电力电子方向入门学习~~

    chromedriver-linux64_122.0.6251.0.zip

    chromedriver-linux64_122.0.6251.0

    操作系统课程设计-进程控制描述与控制

    一、实验目的 实验1.1 Windows“任务管理器”的进程管理 通过在Windows任务管理器中对程序进程进行响应的管理操作,熟悉操作系统进程管理的概念,学习观察操作系统运行的动态性能。 实验1.2 Windows Server 2016进程的“一生” 1)通过创建进程、观察正在运行的进程和终止进程的程序设计和调试操作,进一步熟悉 操作系统的进程概念,理解Windows Server 2016进程的“一生”; 2)通过阅读和分析实验程序,学习创建进程、观察进程和终止进程的程序设计方法。 1.// proccreate项目   2.#include <windows.h>   3.#include <iostream>   4.#include <stdio.h>   5.using namespace std;   6.   7.// 创建传递过来的进程的克隆过程并赋与其ID值   8.void StartClone(int nCloneID) {   9.    // 提取用于当前可执行文件的文件名   10.    TCHAR szFilename[MAX_PATH];   11

    MATLAB环境下一种基于稀疏优化的瞬态伪影消除算法 程序运行环境为MATLAB R2018A,执行一种基于稀疏优化的瞬态伪影消除算法 GRAY = 1 1 1 * 0.7; subplot(4

    MATLAB环境下一种基于稀疏优化的瞬态伪影消除算法 程序运行环境为MATLAB R2018A,执行一种基于稀疏优化的瞬态伪影消除算法。 GRAY = [1 1 1] * 0.7; subplot(4, 1, 4) line(n, y, 'color', GRAY, 'lineWidth', 1) line(n, y - x, 'color', 'black'); legend('Raw data', 'Corrected data') xlim([0 N]) xlabel('Time (n)') 压缩包=数据+程序+参考。

    多机系统的暂态稳定仿真 MATLAB编程 针对多机电力系统,通过编程,计算当发生故障时,多台发电机的功角曲线(pv节点发电机与平衡节点发电机的功角差),通过功角曲线来分析判断多机系统的

    多机系统的暂态稳定仿真 MATLAB编程 针对多机电力系统,通过编程,计算当发生故障时,多台发电机的功角曲线(pv节点发电机与平衡节点发电机的功角差),通过功角曲线来分析判断多机系统的暂态稳定性。 注: 可指定故障发生位置及故障清除时间 下面以IEEE30节点系统为例

    中药实验管理系统设计与实现-springboot毕业项目,适合计算机毕-设、实训项目、大作业学习.zip

    Spring Boot是Spring框架的一个模块,它简化了基于Spring应用程序的创建和部署过程。Spring Boot提供了快速启动Spring应用程序的能力,通过自动配置、微服务支持和独立运行的特性,使得开发者能够专注于业务逻辑,而不是配置细节。Spring Boot的核心思想是约定优于配置,它通过自动配置机制,根据项目中添加的依赖自动配置Spring应用。这大大减少了配置文件的编写,提高了开发效率。Spring Boot还支持嵌入式服务器,如Tomcat、Jetty和Undertow,使得开发者无需部署WAR文件到外部服务器即可运行Spring应用。 Java是一种广泛使用的高级编程语言,由Sun Microsystems公司(现为Oracle公司的一部分)在1995年首次发布。Java以其“编写一次,到处运行”(WORA)的特性而闻名,这一特性得益于Java虚拟机(JVM)的使用,它允许Java程序在任何安装了相应JVM的平台上运行,而无需重新编译。Java语言设计之初就是为了跨平台,同时具备面向对象、并发、安全和健壮性等特点。 Java语言广泛应用于企业级应用、移动应用、桌面应用、游戏开发、云计算和物联网等领域。它的语法结构清晰,易于学习和使用,同时提供了丰富的API库,支持多种编程范式,包括面向对象、命令式、函数式和并发编程。Java的强类型系统和自动内存管理减少了程序错误和内存泄漏的风险。随着Java的不断更新和发展,它已经成为一个成熟的生态系统,拥有庞大的开发者社区和持续的技术创新。Java 8引入了Lambda表达式,进一步简化了并发编程和函数式编程的实现。Java 9及以后的版本继续在模块化、性能和安全性方面进行改进,确保Java语言能够适应不断变化的技术需求和市场趋势。 MySQL是一个关系型数据库管理系统(RDBMS),它基于结构化查询语言(SQL)来管理和存储数据。MySQL由瑞典MySQL AB公司开发,并于2008年被Sun Microsystems收购,随后在2010年,Oracle公司收购了Sun Microsystems,从而获得了MySQL的所有权。MySQL以其高性能、可靠性和易用性而闻名,它提供了多种特性来满足不同规模应用程序的需求。作为一个开源解决方案,MySQL拥有一个活跃的社区,不断为其发展和改进做出贡献。它的多线程功能允许同时处理多个查询,而其优化器则可以高效地执行复杂的查询操作。 随着互联网和Web应用的快速发展,MySQL已成为许多开发者和公司的首选数据库之一。它的可扩展性和灵活性使其能够处理从小规模应用到大规模企业级应用的各种需求。通过各种存储引擎,MySQL能够适应不同的数据存储和检索需求,从而为用户提供了高度的定制性和性能优化的可能性。

    精选毕设项目-鱼缸表盘系统小程序.zip

    精选毕设项目-鱼缸表盘系统小程序

    法院安防系统解决方案Word(77页).docx

    在科技与司法的交响曲中,智慧法院应运而生,成为新时代司法服务的新篇章。它不仅仅是一个概念,更是对法院传统工作模式的一次深刻变革。智慧法院通过移动信息化技术,为法院系统注入了强大的生命力,有效缓解了案多人少的矛盾,让司法服务更加高效、便捷。 立案、调解、审判,每一个阶段都融入了科技的智慧。在立案阶段,智慧法院利用区块链技术实现可信存证,确保了电子合同的合法性和安全性,让交易双方的身份真实性、交易安全性得到了有力见证。这不仅极大地缩短了立案时间,还为后续审判工作奠定了坚实的基础。在调解阶段,多元调解服务平台借助人工智能、自然语言处理等前沿技术,实现了矛盾纠纷的快速化解。无论是矛盾类型的多元化,还是化解主体的多元化,智慧法院都能提供一站式、全方位的服务,让纠纷解决更加高效、和谐。而在审判阶段,智能立案、智能送达、智能庭审、智能判决等一系列智能化手段的应用,更是让审判活动变得更加智能化、集约化。这不仅提高了审判效率,还确保了审判质量的稳步提升。 更为引人注目的是,智慧法院还构建了一套完善的执行体系。移动执行指挥云平台的建设,让执行工作变得更加精准、高效。执行指挥中心和信息管理中心的一体化应用,实现了信息的实时传输和交换,为执行工作提供了强有力的支撑。而执行指挥车的配备,更是让执行现场通讯信号得到了有力保障,应急通讯能力得到了显著提升。这一系列创新举措的实施,不仅让执行难问题得到了有效解决,还为构建诚信社会、保障金融法治化营商环境提供了有力支撑。智慧法院的出现,让司法服务更加贴近民心,让公平正义的阳光更加温暖人心。

    计算机网络复习要点:OSI模型、TCP/IP协议、IP地址、路由算法及网络安全

    内容概要:本文针对计算机网络这门课程的期末复习,全面介绍了多个关键点和重要概念。主要内容涵盖了计算机网络的基本概念、OSI七层模型及其每一层的具体职责和协议、详细的TCP/IP协议介绍,尤其是三次握手和四次挥手机制、IP地址(IPv4 和 IPv6)的概念和子网划分的技术、静态路由和动态路由的区别及其路由选择算法、TCP和UDP作为两种主要传输层协议的功能区别、各种常用的应用层协议如HTTP、HTTPS、FTP、SMTP等,此外还包括了一些关于网络性能优化的关键参数以及常见的网络安全措施。所有理论均配有相应的案例分析帮助深入理解和巩固知识点。 适合人群:正在准备计算机网络相关考试的学生,或希望深入理解计算机网络架构和原理的人群。 使用场景及目标:为用户提供详尽的期末复习指南,助力理解复杂的技术概念并提高解决具体应用问题的能力,同时通过实例演示使学习变得更加直观。 其他说明:强调不仅要记住公式和定义,更要关注概念背后的运作逻辑及实际应用情况来达到良好的复习效果。

    精选毕设项目-移动端商城.zip

    精选毕设项目-移动端商城

    基于Python的B站视频数据分析可视化系统论文

    本文介绍了基于Python的B站视频的数据分析可视化系统设计与实现。该系统帮助用户深入了解B站视频的趋势,并通过数据分析和可视化技术展示相关信息。利用Python的网络爬虫技术获取B站上的视频数据,包括视频标题、上传者、播放量、点赞数等信息。借助数据分析库Pandas对获取的数据进行处理和分析,例如计算了不同用户视频发布个数、粉丝量、视频长度、视频观阅人数,还分析了不同视频的舆情分布和流行趋势。接着,利用可视化库Echarts将分析结果呈现为图表,例如柱状图、饼图、折线图等,以便用户直观地理解数据。为了提供更加个性化的服务,系统还集成了协同过滤算法推荐功能,根据用户的历史观看记录和偏好,推荐可能感兴趣的视频。最后,设计并实现了一个交互式的用户界面,用户可以通过界面选择感兴趣的话题和日期范围,系统将动态展示相关视频的数据分析结果。通过本系统,用户可以更好地了解B站视频的特点和趋势,同时享受到个性化的视频推荐服务,为用户提供了一个便捷而全面的数据分析工具。 感兴趣自行下载学习!

    MPU6050.zip

    标题 "MPU6050.zip" 暗示了这个压缩包可能包含了与MPU6050陀螺仪和加速度传感器相关的资源。MPU6050是一款广泛应用的惯性测量单元(IMU),它能检测设备在三个轴上的角速度和线性加速度,常用于运动控制、姿态估算、导航等领域。 描述中只提到了"MPU6050.zip",没有提供额外信息,但我们可以通过标签 "stm32cubemx" 来推测,这个压缩包里的内容可能与STM32系列微控制器以及使用STM32CubeMX配置工具有关。STM32CubeMX是一款强大的配置工具,用户可以利用它来初始化STM32微控制器的外设,生成相应的初始化代码。 在压缩包的文件名列表中,我们看到以下几个文件: 1. mpu6050.c:这是一个C源文件,通常包含了与MPU6050交互的驱动程序代码。在这个文件里,开发者可能会定义函数来初始化传感器、读取数据、处理中断等。 2. mpu6050.h:这是对应的头文件,包含了函数声明、常量定义和结构体等,供其他模块调用时包含,以实现对MPU60。内容来源于网络分享,如有侵权请联系我删除。另外如果没有积分的同学需要下载,请私信我。

    IPSO-SVR 改进粒子群算法优化支持向量机的多变量回归预测 Matlab语言 1.多变量单输出,通过非线性权重递减方式对粒子群算法进行改进,优化SVR中的两个参数,评价指标包括R2、MAE、MSE

    IPSO-SVR 改进粒子群算法优化支持向量机的多变量回归预测 Matlab语言 1.多变量单输出,通过非线性权重递减方式对粒子群算法进行改进,优化SVR中的两个参数,评价指标包括R2、MAE、MSE、MAPE,效果如图所示,可完全满足您的需求~ 2.直接替Excel数据即可用,注释清晰,适合新手小白[火] 3.附赠测试数据,输入格式如图3所示,可直接运行 4.仅包含模型代码 5.模型只是提供一个衡量数据集精度的方法,因此无法保证替数据就一定得到您满意的结果~

    精选项目-天气预报带后端.zip

    精选项目-天气预报带后端

    精选毕设项目-自助查勘.zip

    精选毕设项目-自助查勘

Global site tag (gtag.js) - Google Analytics