Thinking in Java---Concurrent包下的新构件学习+赛马游戏仿真

Java5的java.util.concurrent包下引入了大量的用于解决并发问题的新类;相对于前面那些基础的线程同步和通信的方法,这些新类是一种更高层次上的抽象,使用起来还是比较容易的.这篇博客就来学习其中的两个新类:CountDownLatch和CyclicBarrier;并使用CyclicBarrier来模拟一个简单的赛马游戏.

一.CountDownLatch

使用CountDownLatch对象时,我们需要给其设定一个初始的计数值,然后在这个对象上调用await()的任务都会阻塞,直到这个对象的计数值减为0;其它的任务可以在完成自己的工作时调用这个对象的countDown()方法来减少这个对象的计数值。所以这个类可以用于同步一个或多个任务,强制它们等待由其它任务执行的一组操作完成;一个典型的应用场景是将一个程序分解为n个互相独立的可解决任务,并创建值为n的CountDownLatch,当每个任务完成时,就会在这个对象上调用countDown().而那些等待这个问题被解决的任务在这个对象上调用await(),使自己阻塞,直到这个对象计数值减为0;另外值得注意的一点是这个对象并不会协调n个任务执行的先后顺序。下面演示这种技术的一个框架示例:

package lkl;

import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * CountDownLatch用来同步一个或多个任务,强制它们
 * 等待由其它任务执行的一组操作完成
 * */

//预先执行的任务
class TaskPortion implements Runnable{
      private static int counter = 0;
      private final int id= counter++;

      //全局多个线程共享一个Random对象,这里其实牵涉到一个并发问题
      //只是实际上Random.next()本身就是线程安全的,所以不需要我们自己进行同步
      private static Random rand = new Random(47);

      private final CountDownLatch latch;
      public TaskPortion(CountDownLatch latch){
          this.latch = latch;
      }

      public void run(){
          try{
              dowork();
              latch.countDown(); //减少计数值
          }catch(InterruptedException ex){
              System.out.println(this+" 通过中断异常退出");
          }
      }

      //线程睡眠一段时间模拟做些工作
     public void dowork() throws InterruptedException{
         TimeUnit.MILLISECONDS.sleep(rand.nextInt(2000));
         System.out.println(this+" completed");
     }

     public String toString(){
         return String.format("%1$-3d", id);
     }
}

class WaitingTask implements Runnable{
    private static int counter = 0;
    private final CountDownLatch latch;
    private final int id = counter++;
    public WaitingTask(CountDownLatch latch){
        this.latch = latch;
    }
    public void run()  {
        try{
            latch.await();; //在latch上的计数值减为0之前,都会阻塞在这里
            System.out.println(this+" completed");
        }catch(InterruptedException ex){
            System.out.println();
        }
    }
    public String toString(){
        return String.format("WatitingTask %1$-3d", id);
    }
}

public class CountDownLatchDemo {

    static final int SIZE = 100;
    public static void main(String[] args) throws Exception{
        ExecutorService exec = Executors.newCachedThreadPool();

        //所有线程都必须共享一个latch对象
        //设定初始计数值为100,即必须等待100个任务完成以后
        //在这个对象上调用await()的任务才能执行
       CountDownLatch latch = new CountDownLatch(SIZE);

       //开10个等待线程
       for(int i=0; i<10; i++){
           exec.execute(new WaitingTask(latch));
       }

       //开100个预先执行的线程
       for(int i=0; i<SIZE; i++){
           exec.execute(new TaskPortion(latch));
       }
       System.out.println("Latched all takss");
       exec.shutdown();
    }
}

二.CyclicBarrier

CyclicBarrier适用于这种情况:我们希望创建一组任务,它们并行的执行,然后在进行下一个步骤之前等待,直至所有的任务都完成(按正常的线程调度,这是不可能实现的)。它使得所有任务都在栅栏处列队,因此可以一致的向前移动。这看起来和上面的CountDownLatch类似,但是CountDownLatch只能触发一次事件,而CyclicBarrier可以多次重用.更具体的使用可以描述如下:我们创建一个指定了初始计数值为n和Runnable对象为r的CyclicBarrier对象,然后将其提交给n个线程,每个线程在完成当前的任务后就会调用这个对象上的await()减少计数值并且当前线程会阻塞;这样直到最后一个线程调用了await()使得计数值减为0,然后就会调用这个Cyclibarrier对象上r的run()方法,在run()方法执行完成以后,又会重置CyclicBarrier对象的计数值然后重复上面的过程。通过这样的一个过程,就达到了使多个线程一致向前移动的效果。

下面通过使用这个类仿真一个赛马游戏:

package lkl;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 模拟赛马游戏
 * 下面的程序中使用"*"表示目前马所在的位置,使用"=="表示栅栏
 * 具体的思想是:我们使用一个线程来模拟一匹马,在这个线程中我们每次都会
 * 生成一个随机数表示本次移动这匹马跑了多少步;然后使用CyclicBarrier来同步多个线程
 * 使它们能一致的向前移动,从而产生赛马的效果。我们会在CycylicBarrier的run()方法中
 * 打印出每次移动以后的情况。
 * 可以通过调整控制台的尺寸大小到只有马,看到这个仿真的效果。
 * */

//模拟马的线程
class Horse implements Runnable{
    private static int counter = 1;
    private final int id = counter++;//马的编号
    private int strides = 0; //当前马所走的步数
    private static Random rand = new Random(47);
    private static CyclicBarrier barrier;//所有线程共享一个CyclicBarrier对象

    public Horse(CyclicBarrier b){
        barrier = b;
    }
    //返回当前马所跑的步数
    public synchronized int getStrides(){
        return strides;
    }

    public void run(){
        try
        {
            while(!Thread.interrupted()){
                synchronized(this){
                    //本次移动了多少步
                    strides+=rand.nextInt(3); //0,1,2
                }
            //阻塞直到最后一个线程也调用了这个方法
            //然后就会先执行barrier的run()任务,然后再重置计数值解除线程阻塞,然后调用这些线程
            //这样就可以保证每次都是每次都是每个线程都调度一次,然后输出一次当前情况
            //然后再进行下一轮的调用,这样就可以模拟赛马的情形了。不然因为操作系统的线程
            //调用是不均匀的,是模拟不了的。
            barrier.await();
            }
        }
        catch(InterruptedException ex){
            System.out.println(this+ " 通过中断异常退出");
        }catch(BrokenBarrierException e){//await()引起的异常
            throw new RuntimeException();
        }
    }
    public String toString(){
        return "Horse "+id+" ";
    }
    //使用"*"表示当前马的轨迹
    public String tracks(){
        StringBuilder s = new StringBuilder();
        for(int i=0 ;i<getStrides();i++){
            s.append("*");
        }
        s.append(id);
        return s.toString();
    }
}

public class HorseRace {

      static final int FINISH_LINE=75; //终点线步数
      private List<Horse> horses = new ArrayList<Horse>();
      private ExecutorService exec = Executors.newCachedThreadPool();
      private CyclicBarrier barrier;

      public HorseRace(int nHorse,final int pause){

          //构造CyclicBarrier对象时需要传入一个Runnable对象用来计数值减为0的时候
          //执行,这里使用匿名内部类的形式传入的。在这里这个Runnable对象负责打印
          //出每次所有马都移动一次以后的情况。
          barrier = new CyclicBarrier(nHorse,new Runnable(){
              public void run(){

                  StringBuilder s = new StringBuilder();
                  //表示栅栏
                  for(int i=0 ; i<FINISH_LINE; i++){
                       s.append("=");
                  }
                  System.out.println(s);
                  //打印每匹马当前的位置(“*”+id表示)
                  for(Horse horse : horses){
                      System.out.println(horse.tracks());
                  }

                  //如果有那匹马越过终点线,则打印出该匹马获胜
                  //并结束游戏(结束掉所有的赛马线程)
                  for(Horse horse: horses){
                      if(horse.getStrides()>=FINISH_LINE){
                          System.out.println(horse+" won");
                          exec.shutdownNow();
                          return;
                      }
                  }
                  try{
                      //睡眠一段时间
                      TimeUnit.MILLISECONDS.sleep(pause);
                  }catch(InterruptedException ex){
                      ex.printStackTrace();
                  }
              }
          });

          //产生nHorse匹马赛跑
         for(int i=0; i<nHorse;i++){
             Horse horse = new Horse(barrier);
             horses.add(horse);
             exec.execute(horse);
         }
      }

      public static void main(String[] args){
          //默认7匹马赛跑
          int nHorses = 7;
          int pause = 200;
          new HorseRace(nHorses,pause);
      }
}
时间: 2024-10-09 13:57:06

Thinking in Java---Concurrent包下的新构件学习+赛马游戏仿真的相关文章

java.util.concurrent包下的几个常用类

1.Callable<V> Callable<V>与Runnable类似,理解Callable<V>可以从比较其与Runnable的区别开始: 1)从使用上:实现的Callable<V>的类需要实现call()方法,此方法有返回对象V:而Runnable的子类需要实现run()方法,但没有返回值: 2)如果直接调用Callable<V>的子类的call()方法,代码是同步顺序执行的:而Runnable的子类是线程,是代码异步执行. 3)将Call

concurrent包下常用的类

转自 http://www.importnew.com/21889.html 在java 1.5中,提供了一些非常有用的辅助类来帮助我们进行并发编程,比如CountDownLatch,CyclicBarrier和Semaphore,今天我们就来学习一下这三个辅助类的用法. 以下是本文目录大纲: 一.CountDownLatch用法 二.CyclicBarrier用法 三.Semaphore用法 一.CountDownLatch用法 CountDownLatch类位于java.util.concu

Java日期时间API系列19-----Jdk8中java.time包中的新的日期时间API类,ZonedDateTime与ZoneId和LocalDateTime的关系,ZonedDateTime格式化和时区转换等。

通过Java日期时间API系列6-----Jdk8中java.time包中的新的日期时间API类中时间范围示意图:可以很清晰的看出ZonedDateTime相当于LocalDateTime+ZoneId. ZonedDateTime是用来处理时区相关的时间,它的各种计算都离不开ZoneId.先看ZoneId. 1. ZoneId 为时区ID,比如Europe/Paris,表示欧洲巴黎时区 1.1 时区相关知识,时区,UTC时间,GMT时间,Unix时间戳 时区 地球自西向东旋转,东边比西边先看到

Java日期时间API系列8-----Jdk8中java.time包中的新的日期时间API类的LocalDate源码分析

目录 0.前言 1.TemporalAccessor源码 2.Temporal源码 3.TemporalAdjuster源码 4.ChronoLocalDate源码 5.LocalDate源码 6.总结 0.前言 通过前面Java日期时间API系列6-----Jdk8中java.time包中的新的日期时间API类中主要的类关系简图如下: 可以看出主要的LocalDate, LocalTime, LocalDateTime, Instant都是实现相同的接口,这里以LocalDate为例分析jav

Java日期时间API系列11-----Jdk8中java.time包中的新的日期时间API类,使用java8日期时间API重写农历LunarDate

通过Java日期时间API系列7-----Jdk8中java.time包中的新的日期时间API类的优点,java8具有很多优点,现在网上查到的农历转换工具类都是基于jdk7及以前的类写的,下面使用java新的日期时间API重写农历LunarDate. package com.xkzhangsan.time; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import ja

Java日期时间API系列13-----Jdk8中java.time包中的新的日期时间API类,时间类转换,Date转LocalDateTime,LocalDateTime转Date

从前面的系列博客中可以看出Jdk8中java.time包中的新的日期时间API类设计的很好,但Date由于使用仍非常广泛,这就涉及到Date转LocalDateTime,LocalDateTime转Date.下面是时间类互相转换大全,包含Instant.LocalDate.LocalDateTime.LocalTime和Date的相互转换,下面是一个工具类,仅供参考: package com.xkzhangsan.time.converter; import java.time.Instant;

Java日期时间API系列17-----Jdk8中java.time包中的新的日期时间API类,java日期计算4,2个日期对比,获取相差年月日部分属性和相差总的天时分秒毫秒纳秒等

通过Java日期时间API系列9-----Jdk8中java.time包中的新的日期时间API类的Period和Duration的区别 ,可以看出java8设计非常好,新增了Period和Duration类,专用于对比2个时间场景: Period,可以获取2个时间相差的年月日的属性. Duration,可以获取2个时间相差总的天时分秒毫秒纳秒. 下面应用: /** * 获取2个日期的相差年月天的年数部分 * @param startInclusive * @param endExclusive

Java日期时间API系列20-----Jdk8中java.time包中的新的日期时间API类,ZoneId时区ID大全等。

Java日期时间API系列19-----Jdk8中java.time包中的新的日期时间API类,ZonedDateTime与ZoneId和LocalDateTime的关系,ZonedDateTime格式化和时区转换等.中已经对ZoneId说明,并列出了常用时区ID信息. 并且通过 java.time.ZoneId.getAvailableZoneIds()获取到所有可用时区ID. 1.测试代码 /** * 获取可用时区ID */ @Test public void getAvailableZon

利用java concurrent 包实现日志写数据库的并发处理

一.概述 在很多系统中,往往需要将各种操作写入数据库(比如客户端发起的操作). 最简单的做法是,封装一个公共的写日志的api,各个操作中调用该api完成自己操作日志的入库.但因为入数据库效率比较低,如果每个操作自己入库,则会影响响应速度.而且当操作并发度很高时,往往同时有多个线程在写数据库,也会对系统有影响. 考虑的解决方案是,这个api并不实际完成入库,而是将每个操作日志信息写到一个公共的缓存中,然后应用系统起了一个独立的线程(一直运行)在后台进行入库.如果当前缓存中有记录,就写库,没有记录,