(接上文《线程基础:JDK1.5+(8)——线程新特性(上)》)

3、工作在多线程环境下的“计数器”:

从这个小节开始,我们将以一个“赛跑”的例子,讲解JDK1.5环境下一些线程控制工具(包括Semaphore、CountDownLatch和java.util.concurrent.atomic子包),并且复习这个专题讲到的知识点:同步快、锁、线程池、BlockingQueue、Callable等。

3-1、 赛跑比赛的需求

现在您不仅可以通过我们已经介绍的知识点,实现对100米田径比赛的初赛和决赛的模拟,然后公布出比赛的冠亚季军。还可以基于这些知识,模拟机场T1和T2跑道的起降工作。这里我们一起实现前者的需求,首先来看看100米田径比赛的业务需求:

线程基础:JDK1.5+(9)——线程新特性(中)_javad

  1. 选手要参加比赛,首先就要报名。为了使功能足够简单,参赛选手的基本只包括:姓名、起跑指数(最低速度)、参赛号三个信息。

  2. 同一个选手的状态不稳定性。也就是说某一个选手,在初赛阶段的速度可能是A,但是决赛阶段由于发挥失常,可能速度就变成了B。而这一切都是随机进行的

  3. 选手们首先进行“初赛”,所有选手的“初赛”成绩将进行汇总。成绩最好的5名选手,将参加“决赛”。“决赛”成绩最好的三名选手,将分别获得冠亚季军,并公布出来。

  4. 比赛场地只有一个,总共有5条跑道可供使用。所以无论是“初赛”还是“决赛”,同一时间参加比赛的选手都不能超过5名。

3-1-1、基本类:Player选手类

本小节后续的内容中,我们将对跑步比赛的实现代码进行多次更改优化,但是无论实现代码如何变化,有几个基本的模型是不会变化的:选手描述和比赛结果描述。

选手除了名字、参赛编号的描述外,还有一个“最低速度”的描述,这是为了保证无论这个选手跑多少次,其状态都不会太过失常。“最低速度”是在创建选手时,系统随机生成的。

以下是Player选手类的定义代码:

package test.thread.track;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.Semaphore;

/**
 * 这就是一个选手。
 * 为了简单起见,我们只记录这个选手名字、选手编号、最低速度(创建时随机生成)。
 * 当然,最为一名选手,最重要的工作就是“跑步”
 * @author yinwenjie
 */
public class Player implements Callable<Result> , Comparable<Player>{

    /**
     * 选手编号
     */
    private int number;

    /**
     * 选手的名字
     */
    private String name;

    /**
     * 最低速度
     */
    private float minSpeed;

    /**
     * 本次比赛结果
     */
    private Result result;

    /**
     * 跑道
     */
    private Semaphore runway;

    public Player(String name , int number , Semaphore runway) {
        this.name = name;
        this.number = number;
        this.runway = runway;

        // 这个最低速度设置是 8米/秒(否则就真是‘龟速’了)
        this.minSpeed = 8f;
    }

    /* (non-Javadoc)
     * @see java.util.concurrent.Callable#call()
     */
    @Override
    public Result call() throws Exception {
        try {
            // 申请上跑道
            this.runway.acquire();
            return this.doRun();
        } catch(Exception e) {
            e.printStackTrace(System.out);
        } finally {
            // 都要进入初赛结果排序(中途退赛的成绩就为0)
            this.runway.release();
        }

        // 如果执行到这里,说明异常发生了
        this.result = new Result(Float.MAX_VALUE);
        return result;
    }

    /**
     * 开始跑步
     * @return
     * @throws Exception
     */
    private Result doRun()  throws Exception {
        /*
         * 为了表现一个选手每一次跑步都有不同的状态(但是都不会低于其最低状态),
         * 所以每一次跑步,系统都会为这个选手分配一个即时速度。
         * 
         * 这个即时速度不会低于其最小速度,但是也不会高于 14米/秒(否则就是‘超人’咯)
         * */
        // 生成即时速度
        float presentSpeed = 0f;
        presentSpeed = this.minSpeed * (1.0f + new Random().nextFloat());
        if(presentSpeed > 14f) {
            presentSpeed = 14f;
        }

        // 计算跑步结果(BigDecimal的使用可自行查阅资料)
        BigDecimal calculation =  new BigDecimal(100).divide(new BigDecimal(presentSpeed) , 3, RoundingMode.HALF_UP);
        float presentTime = calculation.floatValue();

        // 让线程等待presentSpeed的时间,模拟该选手跑步的过程
        synchronized (this) {
            this.wait((long)(presentTime * 1000f));
        }

        // 返回跑步结果
        this.result = new Result(presentTime);
        return result;
    }

    /**
     * @return the result
     */
    public Result getResult() {
        return result;
    }

    /**
     * @return the number
     */
    public int getNumber() {
        return number;
    }

    /**
     * @return the name
     */
    public String getName() {
        return name;
    }

    /* (non-Javadoc)
     * @see java.lang.Comparable#compareTo(java.lang.Object)
     */
    @Override
    public int compareTo(Player o) {
        /*
         * 两个选手间,还可以通过他们的result进行比较
         * 耗时越小,当然越靠前
         * */
        Result myReslut = this.getResult();
        Result targetReslut = o.getResult();

        // 如果出现了reslut为null或者targetReslut为null,说明比赛结果出现了问题
        // 当然如果真的出现这样的问题,最可能的选手中途退赛了
        if(myReslut == null) {
            return 1;
        }
        if(targetReslut == null) {
            return -1;
        }

        // 耗时越少的选手,当然应该排在“成绩”队列的越前面
        if(myReslut.getTime() < targetReslut.getTime()) {
            return -1;
        } else {
            return 1;
        }
    }
}

为什么Player选手类要实现Comparable接口呢?在实现代码中,我将使用PriorityBlockingQueue队列,将选手依据其比赛成绩进行排序。为了能够保证PriorityBlockingQueue队列能够正常排序,所以需要实现该接口。

当然有的读者会说,实现Comparable接口后,使用普通的List也可以排序。但是List接口的实现类(ArrayList、LinkedList、Vector等等)并不是线程安全的,它们常用的处理场景还是在某一个线程内进行数据线性化处理时使用。

而就目前我们的场景来看,程序员根本就不知道某一个选手什么时候能够跑完100米,并且多个选手跑步的处理结果都将随机的送入队列。所以保证线程安全性是需求实现中重要的一部分

当然,如果您硬是要使用传统的List也行。能可以通过JDK提供的“同步包装器”(Collections.synchronizedList)将它变成线程安全的。但这个问题不是本小节讨论的范围。

另外,做为一个选手来说,最根本的功能就是“跑”这个动作。并且根据需求,很明显我们需要在选手“跑完后”知道“跑”的成绩。所以我们还需要Player类实现Callable接口,以便让选手能够跑起来。

为了模拟跑的过程和选手的状态有关,代码中使用随机数确定本次选手“跑”的速度。但是这个速度不会低于选手的“最低速度”(目前给定的是14秒)。

3-1-2、基本类:Result比赛结果

另外一个不会变动的基本类就是Result成绩:

package test.thread.track;

/**
 * 选手某一次跑步的成绩
 * @author yinwenjie
 *
 */
public class Result {
    /**
     * 记录了本次赛跑的用时情况
     */
    private float time;

    public Result(float time) {
        this.time = time;
    }

    /**
     * @return the time
     */
    public float getTime() {
        return time;
    }

    /**
     * @param time the time to set
     */
    public void setTime(float time) {
        this.time = time;
    }
}

每一次选手“跑”的成绩都是不一样的。成绩中只包括一个属性,就是跑完100米的用时情况。

3-2、Semaphore:信号量

3-2-1、基本使用

Semaphore信号量,是concurrent包的一个重要工具类,它通过申请和回收“证书”,实现多个线程对同一资源的访问控制。具体的做法是,某个线程在访问某个(可能出现资源抢占的)资源的时候,首先向Semaphore对象申请“证书”,如果没有拿到“证书”就一直阻塞;当拿到“证书”后,线程就解除阻塞状态,然后访问资源;在完成资源操作后,再向Semaphore对象归还“证书”;让我们先来看看Semaphore信号的简单示例:

package test.thread.semaphore;

import java.util.concurrent.Semaphore;

public class SemaphoreTest {

    public static void main(String[] args) throws Throwable {
         new SemaphoreTest().doTest();
    }

    public void doTest() {
        Semaphore semp = new Semaphore(5 , false);

        // 我们创建10个线程,并通过0-9的index进行编号
        for(int index = 0 ; index < 10 ; index++) {
            Thread semaphoreThread = new Thread(new SemaphoreRunnableNonfair(semp , index));
            semaphoreThread.start();
        }
    }

    /**
     * 测试Semaphore的非公平模式
     * @author yinwenjie
     */
    private static class SemaphoreRunnableNonfair implements Runnable {

        private Semaphore semp;

        /**
         * 编号
         */
        private Integer index;

        public SemaphoreRunnableNonfair(Semaphore semp , Integer index) {
            this.semp = semp;
            this.index = index;
        }

        @Override
        public void run() {
            try {
                System.out.println("线程" + this.index + "等待信号。。。。。。");
                this.semp.acquire();
                // 停止一段时间,模拟业务处理过程
                synchronized(this) {
                    System.out.println("index 为 " + this.index + " 的线程,获得信号,开始处理业务");
                    this.wait(5000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace(System.out);
            } finally {
                // 最后都要释放这个信号/证书
                this.semp.release();
            }
        }
    }
}

以上代码我们创建了10个线程。分别编号为0-9(这里我们没有使用Thread自带的id,主要还是为了读者能够看得清楚)。Semaphore信号量对象中,我们放置了5个“证书”,也就是说最多同时可以有5个线程进行业务处理,处理完成后向线程向Semaphore信号对象归还“证书”。以上代码的处理结果,可能如下图所示(注意,是“可能”):

线程0等待信号。。。。。。
线程2等待信号。。。。。。
index2 的线程,获得信号,开始处理业务
index0 的线程,获得信号,开始处理业务
线程3等待信号。。。。。。
index3 的线程,获得信号,开始处理业务
线程4等待信号。。。。。。
index4 的线程,获得信号,开始处理业务
线程5等待信号。。。。。。
index5 的线程,获得信号,开始处理业务
线程7等待信号。。。。。。
线程8等待信号。。。。。。
线程6等待信号。。。。。。
线程9等待信号。。。。。。
线程1等待信号。。。。。。
index8 的线程,获得信号,开始处理业务
index7 的线程,获得信号,开始处理业务
index6 的线程,获得信号,开始处理业务
index9 的线程,获得信号,开始处理业务
index1 的线程,获得信号,开始处理业务

3-2-2、Semaphore的基本操作方式

为了方便读者查阅,这里我们列举了Semaphore中常用的操作方式

  • 申请/获取证书:

    void acquire():从此信号量获取一个许可,在Semaphore能够提供一个许可前,当前线程将一直阻塞等待。如果在等待过程中,当前线程收到了interrupt信号,那么将抛出InterruptedException异常。

    void acquire(permits):从此信号量获取permits个许可,在Semaphore能够提供permits个许可前,当前线程将一直阻塞等待。如果在等待过程中,当前线程收到了interrupt信号,那么将抛出InterruptedException异常。

    void acquireUninterruptibly():从此信号量获取一个许可,在Semaphore能够提供一个许可前,当前线程将一直阻塞等待。使用这个方法获取许可时,不会受到线程interrupt信号的影响。

    void acquireUninterruptibly(permits):从此信号量获取permits个许可,在Semaphore能够提供permits个许可前,当前线程将一直阻塞等待。使用这个方法获取许可时,不会受到线程interrupt信号的影响。

    boolean tryAcquire():从此信号量获取一个许可,如果无法获取,线程并不会阻塞在这里。如果获取到了许可,则返回true,其他情况返回false。

    boolean tryAcquire(permits):从此信号量获取permits个许可,如果无法获取,线程并不会阻塞在这里。如果获取到了许可,则返回true,其他情况返回false。

    boolean tryAcquire(int permits, long timeout, TimeUnit unit):从此信号量获取permits个许可,如果无法获取,则当前线程等待设定的时间。如果超过等待时间后,还是没有拿到许可,则解除等待继续执行。如果获取到了许可,则返回true,其他情况返回false。

  • 证书状态:

    int availablePermits():返回此信号量中当前可用的许可数。

    int getQueueLength():返回正在等待获取的线程的估计数目。该值仅是估计的数字,因为在此方法遍历内部数据结构的同时,线程的数目可能动态地变化。此方法用于监视系统状态,不用于同步控制。

    boolean hasQueuedThreads():查询是否有线程正在等待获取。注意,因为同时可能发生取消,所以返回 true 并不保证有其他线程等待获取许可。此方法主要用于监视系统状态。

    boolean isFair():如果此信号量的公平设置为 true,则返回 true。

  • 释放/返还证书:

    void release():释放一个许可,将其返回给信号量。最好将这个方法的调用,放置在finally程序块中执行。

    void release(permits):释放给定数目的许可,将其返回到信号量。最好将这个方法的调用,放置在finally程序块中执行。

  • fair:公平与非公平

    Semaphore一共有两个构造函数,分别是:Semaphore(int permits)和Semaphore(int permits, boolean fair);permits是指由Semaphore信号量控制的“证书”数量。fair参数是设置这个信号量对象的工作方式。

当fair参数为true时,信号量将以“公平方式”运行。即首先申请证书,并进入阻塞状态的线程,将有权利首先获取到证书;当fair参数为false时,信号量对象将不会保证“先来先得”。默认情况下,Semaphore采用“非公平”模式运行。

3-2-3、实现比赛场景

在介绍了Semaphore的使用方式后,现在我们就要将Semaphore加入“赛跑比赛”的代码实现中。

很显然Semaphore在我们需求中的应用任务是:给选手使用“跑道”的证书/权利,以便让选手“跑步”,并且在选手使用完跑道后,回收跑道的使用证书/权利,给下一位选手。

......
// 这就是跑道,需求上说了只有5条跑道,所以只有5个permits。
Semaphore runway = new Semaphore(5);
......

这个代码片段控制着所有选手的跑步动作:只有在获得跑道的使用权限后,才能执行“跑步”动作。

3-2-4、关键的一个问题

  • 什么情况下视为“初赛”、“决赛”完成?

    那么最直观的描述就是:所有报名的选手都完成了跑步过程(中途退赛也算),才能算“初赛”完成;“初赛”排名最靠前的前5名选手都完成了跑步过程(中途退赛也算)才算是“决赛”完成。

    如果没有完成“初赛”,那么比赛进程就必须停在那里,直到“初赛”过程完成;如果没有完成“决赛”过程,比赛进程就必须停在那里,知道“决赛”完成:

......
//! 只有当PLAYERNAMES.length位选手的成绩都产生了,才能进入决赛,这很重要
synchronized (this.preliminaries) {
    while(this.preliminaries.size() < OneTrack.PLAYERNAMES.length) {
        try {
            this.preliminaries.wait();
        } catch(InterruptedException e) {
            e.printStackTrace(System.out);
        }
    }
}
......
//! 只有当5位选手的决赛成绩都产生了,才能到下一步:公布成绩
synchronized (this.finals) {
    while(this.finals.size() < 5) {
        try {
            this.finals.wait();
        } catch(InterruptedException e) {
            e.printStackTrace(System.out);
        }
    }
}
......
  • 怎么监控某一个选手,是否完成了跑步过程?

在我们定义的Player选手类中,已经实现了Callable接口,并且将会在运行完成后,返回Result结果信息。所以看选手是否完成了跑步过程,只需要监控Player的Future就可以了。

但是监控Player的Future可不能在100米比赛的主线程上进行,否则就会出现上一个选手没有跑完就不能启动下一个选手的跑步线程的情况。所以我们需要为每一个选手都创建一个“监控线程”FutureThread:

/**
 * 这是计分线程,是为了保证产生比赛结果后,在计入PriorityBlockingQueue
 * 这样才有排列成绩的依据
 * @author yinwenjie
 *
 */
private class FutureThread extends Thread {

    /**
     * 选手跑步任务(Player)的执行状态对象
     */
    private Future<Result> future;

    /**
     * 跑步成绩出来后,需要操作的队列
     * (要将对应的选手加入到队列,以便依据成绩进行排序)
     */
    private PriorityBlockingQueue<Player> achievementQueue;

    /**
     * 当前进行跑步的选手
     */
    private Player player;

    public FutureThread(Future<Result> future , Player player , PriorityBlockingQueue<Player> achievementQueue) {
        this.future = future;
        this.player = player;
        this.achievementQueue = achievementQueue;
    }

    /* (non-Javadoc)
     * @see java.lang.Thread#run()
     */
    @Override
    public void run() {
        // 如果条件成立,最有可能的就是选手在比赛过程中,
        // 由于某种原因退赛了!
        if(this.future == null) {
            System.out.println("选手退赛,计分为0");
        } else {
            try {
                // 如果选手没有跑完,FutureThread将阻塞在这里
                // 当然出现跑步过程中退赛,就会抛出异常
                this.future.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }

        // 运行到这里,就说明这个选手跑完了(或者退赛了)
        // 无论什么情况,都计入队列,然后通知主线程
        this.achievementQueue.put(this.player);
        synchronized (this.achievementQueue) {
            this.achievementQueue.notify();
        }
    }
}

这样,每个选手在跑步过程中,就会有两个线程:一个用来跑步的线程:Player-Callable;另一个用来监控跑步情况,并操作成绩队列的线程:FutureThread。

3-3、完整的比赛代码

实现代码中主要的问题都解决了,现在我们可以给出完成的实现代码了(注意,之前已经给出的代码,就不在赘述了):

package test.thread.track;

import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.Semaphore;


/**
 * 这是第一个比赛程序。
 * @author yinwenjie
 *
 */
public class OneTrack {

    private static final String[] PLAYERNAMES = new String[]{"白银圣斗士","黄金圣斗士"
        ,"青铜圣斗士","神斗士","冥斗士","哈迪斯","龟仙人","孙悟空","孙悟饭","贝吉塔","孙悟天"};

    /**
     * 报名队列(非线程安全)
     */
    private List<Player> signupPlayers = new LinkedList<Player>();

    /**
     * 初赛结果队列(有排序功能,且线程安全)
     */
    private PriorityBlockingQueue<Player> preliminaries = new PriorityBlockingQueue<Player>();

    /**
     * 决赛结果队列(有排序功能,且线程安全)
     */
    private PriorityBlockingQueue<Player> finals = new PriorityBlockingQueue<Player>();

    public void track() {
        /*
         * 赛跑分为以下几个阶段进行;
         * 
         * 1、报名
         * 2、初赛,10名选手,分成两组,每组5名选手。
         * 分两次进行初赛(因为场地只有5条赛道,只有拿到进场许可的才能使用赛道,进行比赛)
         * 
         * 3、决赛:初赛结果将被写入到一个队列中进行排序,只有成绩最好的前五名选手,可以参加决赛。
         * 
         * 4、决赛结果的前三名将分别作为冠亚季军被公布出来
         * */

        //1、================报名
        // 这就是跑道,需求上说了只有5条跑道,所以只有5个permits。
        Semaphore runway = new Semaphore(5);
        this.signupPlayers.clear();
        for(int index = 0 ; index < OneTrack.PLAYERNAMES.length ; ) {
            Player player = new Player(OneTrack.PLAYERNAMES[index], ++index , runway);
            this.signupPlayers.add(player);
        }

        //2、================进行初赛
        // 这是裁判
        ExecutorService refereeService = Executors.newFixedThreadPool(5);
        for (final Player player : this.signupPlayers) {
            Future<Result> future = null;
            future = refereeService.submit(player);
            new FutureThread(future, player, this.preliminaries).start();
        }
        //! 只有当PLAYERNAMES.length位选手的成绩都产生了,才能进入决赛,这很重要
        synchronized (this.preliminaries) {
            while(this.preliminaries.size() < OneTrack.PLAYERNAMES.length) {
                try {
                    this.preliminaries.wait();
                } catch(InterruptedException e) {
                    e.printStackTrace(System.out);
                }
            }
        }

        // 3、============决赛(只有初赛结果的前5名可以参见)
        for(int index = 0 ; index < 5 ; index++) {
            Player player = this.preliminaries.poll();
            Future<Result> future = null;
            future = refereeService.submit(player);
            new FutureThread(future, player, this.finals).start();
        }
        //! 只有当5位选手的决赛成绩都产生了,才能到下一步:公布成绩
        synchronized (this.finals) {
            while(this.finals.size() < 5) {
                try {
                    this.finals.wait();
                } catch(InterruptedException e) {
                    e.printStackTrace(System.out);
                }
            }
        }

        // 4、============公布决赛成绩(前三名)
        for(int index = 0 ; index < 3 ; index++) {
            Player player = this.finals.poll();
            switch (index) {
            case 0:
                System.out.println("第一名:"  + player.getName() + "[" + player.getNumber() + "],成绩:" + player.getResult().getTime() + "秒");
                break;
            case 1:
                System.out.println("第二名:"  + player.getName() + "[" + player.getNumber() + "],成绩:" + player.getResult().getTime() + "秒");
                break;
            case 2:
                System.out.println("第三名:"  + player.getName() + "[" + player.getNumber() + "],成绩:" + player.getResult().getTime() + "秒");
                break;
            default:
                break;
            }
        }
    }

    public static void main(String[] args) throws RuntimeException {
        new OneTrack().track();
    }

    //......这里是FutureThread的代码,上面已给出了
}

以下是可能的执行结果。“可能的执行结果”那是因为结果完全是随机的,您的执行结果可能和我给出的不一样:

第一名:龟仙人[7],成绩:7.143秒
第二名:白银圣斗士[1],成绩:7.477秒
第三名:哈迪斯[6],成绩:7.531

(接下文:CountDownLatch同步器、java.util.concurrent.atomic子包)