初阶并发编辑之——基本的线程机制

定义任务Runnable接口与Thread类

  接口Runnable只有一个方法run(),其签名为:public void run()。可以发现方法run无参数,不返回任何值。一般用Runnable的实现类去描述任务,如下实例:

//from Thinking in Java
class LiftOff implements Runnable{
    protected int countDown = 10;
    private static int taskCount = 0;
    private final int id = taskCount++;
    public LiftOff(){}
    public LiftOff(int countDown){
        this.countDown = countDown;
    }
    public String status(){
        return "#"+id+"("+(countDown > 0 ? countDown : "LiftOff!") +"),";
    }
    @Override
    public void run() {
        while(countDown-- >0){
            System.out.println(status());
            Thread.yield();    //让步语句,表示暗示当前线程切换下一个线程
        }
    }
}

  该任务描述一个10秒倒计时的事例。现在创建一个线程类Thread的实例去驱动该任务。

Thread t = new Thread(new LiftOff());
       t.start();、

输出结果:

#0(9),#0(8),#0(7),#0(6),#0(5),#0(4),
#0(3),#0(2),#0(1),#0(LiftOff!),

  Thread类只需要一个Runnable对象来构造,方法start()线程执行的初始化操作,并执行对应Runnable对象的run()方法,静态方法yield()表示切换当前线程让下一个线程运行,后面会详细介绍。当你构造多个线程去执行这个任务时会发现任务之间时混乱无序的。比如:

for(int i= 0;i<4;i++){
           new Thread(new LiftOff()).start();
           }

输出结果:

#0(9),#2(9),#3(9),#1(9),#2(8),#1(8),
#0(8),#3(8),#0(7),#2(7),#1(7),#3(7),
#0(6),#3(6),#1(6),#2(6),#3(5),#0(5),
#2(5),#1(5),#2(4),#1(4),#3(4),#0(4),
#0(3),#3(3),#2(3),#1(3),#2(2),#3(2),
#0(2),#1(2),#1(1),#2(1),#0(1),#3(1),
#0(LiftOff!),#1(LiftOff!),#3(LiftOff!),
#2(LiftOff!),

  输出结果说明不同任务的执行在线程被换进换出时是混在一起的,有时候cpu被线程1占有,然后到线程2,但是下一轮又不会按1到2的顺序,个线程随机抢占cpu。


使用Executor执行器

  执行器Executor将为你管理Thread对象,可以代替线程去执行任务。ExecutorService(具有服务生命周期的Executor)知道如何构建恰当的上下文来执行Runnable对象。ExecutorService对象由Executor的静态方法创建,一般分为四种:CachedThreadPool,FixedThreadPool,ScheduledThreadPool,SingleThreadPool。他们各自运用在不同的场景。


1)CachedThreadPool

//CachedThreadPool
ExecutorService exec = Executors.newCachedThreadPool();    
       for(int i= 0;i<5;i++){
          exec.execute(new LiftOff());
       }
       exec.shutdown();


  线程池CachedThreadPool为每个任务创建一个线程,方法shutdown()防止新任务被提交给这个Executor,当前线程即main()方法将继续运行shutdown()被调用之前提交的任务。public static ExecutorService newCachedThreadPool()创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。注意,可以使用 ThreadPoolExecutor 构造方法创建具有类似属性但细节不同(例如超时参数)的线程池。


2)FixedThreadPool

  FixedThreadPool创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

 ExecutorService exec = Executors.newFixedThreadPool(5);    
       for(int i= 0;i<5;i++){
          exec.execute(new LiftOff());
       }
       exec.shutdown();



3)ScheduledThreadPool

  ScheduledThreadPool创建一个定长线程池,支持定时及周期性任务执行。延迟执行示例代码如下:

scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
       @Override
       public void run() {
       System.out.println("delay 1 seconds, and excute every 3 seconds");
      }
}, 1, 3, TimeUnit.SECONDS);


  实例表示创建一个延迟一秒后每三秒执行一次任务


4)SingleThreadPool

  SingleThreadPool创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行.

      ExecutorService exec = Executors.newSingleThreadExecutor();    
       for(int i= 0;i<5;i++){
          exec.execute(new LiftOff());
       }
       exec.shutdown();


使用Callable从任务中返回结果

  我们知道Runnable创建一个没有返回值的任务,当我们希望任务有返回值时就要用接口Callable。

//实现接口Callable
class TaskWithResult implements Callable<String>{
    private int id;
    public TaskWithResult(int id){
        this.id = id;
    }
    @Override
    public String call() throws Exception {
        // TODO Auto-generated method stub
        return "result:"+id;
    }
}
//测试方法
public void test(){
        ExecutorService  exec = Executors.newCachedThreadPool();
        ArrayList<Future<String>> results = new ArrayList<Future<String>>();
        for(int i=0;i<10;i++){
            results.add(exec.submit(new TaskWithResult(i)));
        }
        for(Future<String> fs:results){
            try {
                System.out.println(fs.get());
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (ExecutionException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }finally{
                exec.shutdown();
            }
        }
    }

返回结果:

result:0 result:1 result:2 result:3 result:4 
result:5 result:6 result:7 result:8 result:9


  Futrue可以监视目标线程调用call的情况,当你调用Future的get()方法以获得结果时,当前线程就开始阻塞,直接call方法结束返回结果。而方法submit()执行call()方法并将结果给Future对象,并返回该Future对象,


休眠与优先级

  Thread的静态方法sleep()将当前线程强制进入睡眠状态,当休眠时间过去后该线程接入就绪状态,cpu将切换下一个处于就绪状态中优先级较高的线程。可以用较后的jdk版本中的TimeUnit.MILLISECONDS.sleep()代替,默认参数为毫秒,即参数为100将线程睡眠100毫秒,将run()方法改为:

public void run() {
        while(countDown-- >0){
            System.out.println(status());
            //Thread.yield();    让步语句,表示暗示当前线程切换下一个线程
            try {
                //Thread.sleep(100);
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
                System.out.println("Interrupted");
            }
        }
    }

  线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的并非没机会执行。线程的优先级用1-10之间的整数表示,数值越大优先级越高,默认的优先级为5。Thread中公有方法setPriority()用来设定线程的优先级。Thread.MIN_PRIORITY表示最低优先级1,Thread.MAX_PRIORITY表示最高优先级10。Thread中公有方法getPriority()方法用来查看this线程的优先级。


方法yield()

  Thread静态方法yiely()使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。cpu会从众多的可执行态里选择,也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的,并不是说一定会执行其他线程而该线程在下一次中不会执行到了。与sleep()不同的是当前线程退出运行状态不是进入休眠而是变成就绪状态,他有可能还会立即进入运行状态。

后台线程

  所谓后台线程是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程不属于程序中不可或缺的部分。因此,当所有的非后台线程结束时,程序也终止了,同时杀死进程中的所有后台线程。我们看一个例子:

class SimpleDaemon implements Runnable{

    @Override
    public void run() {
        // TODO Auto-generated method stub
        while(true){
            try {
                TimeUnit.MILLISECONDS.sleep(100);
                System.out.println(Thread.currentThread() +"" +this);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}
public class DaemonTest {
  public static void main(String args[]) throws InterruptedException{
      for(int i=0;i<10;i++){
          Thread daemon = new Thread(new SimpleDaemon());
          daemon.setDaemon(true);
          daemon.start();
      }
      System.out.println("Started");
      TimeUnit.MILLISECONDS.sleep(150);
  }
}

输出结果:

Started
Thread[Thread-1,5,main]Concurrent.SimpleDaemon@793d2cfa
Thread[Thread-2,5,main]Concurrent.SimpleDaemon@406866f9
Thread[Thread-9,5,main]Concurrent.SimpleDaemon@10b7ce35
Thread[Thread-8,5,main]Concurrent.SimpleDaemon@46cf8b7b
Thread[Thread-7,5,main]Concurrent.SimpleDaemon@6426ac69
Thread[Thread-4,5,main]Concurrent.SimpleDaemon@44712366
Thread[Thread-6,5,main]Concurrent.SimpleDaemon@4e31adfb
Thread[Thread-3,5,main]Concurrent.SimpleDaemon@25eb939e
Thread[Thread-5,5,main]Concurrent.SimpleDaemon@18a5eea9
Thread[Thread-0,5,main]Concurrent.SimpleDaemon@20b708e4

  当我们将main()中的休眠时间改为100时在输出时输出结果如下:

Started
Thread[Thread-0,5,main]Concurrent.SimpleDaemon@20b708e4
Thread[Thread-8,5,main]Concurrent.SimpleDaemon@46cf8b7b

  通过结果可以发现,当将非后台线程main暂时休眠终止时,后台线程依然存活,当缩短休眠时间时就会发现一旦程序结束,就会杀死那些未来得及的线程。方法setDaemon(Boolean)设置线程是否未后台,线程默认为非后台。


方法join()
  Thread类中的join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。简单理解就是你在主线程main()方法中有两个线程,在第一个线程t1运行后,调用t1的join()方法,那么主线程就不会异步下去执行线程t2的start()方法了,实现了线程t1与主线程的同步。我们看上面的LiftOff类:

Thread t1 = new Thread(new LiftOff());
         Thread t2 = new Thread(new LiftOff());
         t1.start();
         t1.join();
         t2.start();

结果如下:

#0(9),#0(8),#0(7),#0(6),#0(5),#0(4),#0(3),#0(2),#0(1),#0(LiftOff!),
#1(9),#1(8),#1(7),#1(6),#1(5),#1(4),#1(3),#1(2),#1(1),#1(LiftOff!),

当我们将t1.join()注销再看结果:

#0(9),#1(9),#0(8),#1(8),#0(7),#1(7),#0(6),#1(6),#1(5),#0(5),#0(4),
#1(4),#0(3),#1(3),#1(2),#0(2),#0(1),#1(1),#0(LiftOff!),#1(LiftOff!),

  必须注意的是在调用其他线程的join方法时,当前线程是运行中的,也就是join方法在start方法之后才有意义。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值