Java多线程

Java多线程——<一>概述、定义任务

一、概述

  为什么使用线程?从c开始,任何一门高级语言的默认执行顺序是“按照编写的代码的顺序执行”,日常开发过程中写的业务逻辑,但凡不涉及并发的,都是让一个任务顺序执行以确保得到想要的结果。但是,当你的任务需要处理的业务比较多时,且这些业务前后之间没有依赖(比如, a执行的过程中b也可以执行,b没有必要必须等待a执行完毕再去执行),那么此时,我们可以将一个任务拆分成多个小任务。

  例如,任务a负责接收键盘的输入,b负责将一些参数及计算提前做好(假设计算量比较大),c负责将a的输入和b的结果做和。此时,abc顺序执行的话,如果a的输入被阻塞了即正在等待用户输入,b就无法执行,而此时cpu处于空置状态(假设是单cpu且单核),明显效率不高。

  换一个思路,假如:abc分开成为三个任务,a的输入被阻塞了,那么此时就把b交给cpu去执行,待用户输入结果之后,b已经将计算结果输出给了c,此时,用户提交后,c便立即计算出了结果。

  综上:多线程解决的是并发的问题,目的是使任务执行效率更高,实现前提是“阻塞”。它们看上去时同时在执行的,但实际上只是分时间片试用cpu而已。

二、java中的多线程

  1.定义任务

  任务:简单来说,就是一序列工作的集合,这些工作之间有前后顺序,这一系列过程执行过后将实现一个结果或达到一个目的。

  首先,思考一个问题,为什么要定义任务?作为java程序员,我们不关心底层的多线程机制是如何执行的,只关心我写个怎样的任务,java的底层的多线程机制才能认识,才能调用你的任务去执行。java是定义了Runnable接口让你去实现,意思就是:你实现Runnable接口类定义一个类,该类的对象就是我能识别的任务,其他方式定义的程序,我都不会将它认为是任务。

  好,到这里要明确一点,我们此时只谈论任务,不说多线程。任务和你平时在一个类中编写的代码并无区别,只是按照java的要求实现了一个接口,并在该接口的run方法中编写了你的代码。也就是说,你平时想编写一个类,该类能够完成一些功能,这个类里的任何方法、变量由你自己来定义,而编写任务时,你需要实现Runnable接口,把你想让该任务实现的代码写到run方法中,当然,你可以在你定义的任务类中再定义其他变量、方法以在run中调用。

  2.代码实现

复制代码
public class Task implements Runnable {
    protected int countDown = 10;
    private static int taskCount = 0 ;
    private final int id = taskCount++;
    public Task(){}
    public Task(int countDown){
        this.countDown = countDown;
    }
    public String status(){
        return "#"+id+"("+(countDown>0?countDown:"Task!")+").    ";
    }
    @Override
    public void run() {
        while(countDown-->0){
            System.out.print(status());
            Thread.yield();
        }
    }
}
复制代码

注:此处代码源于《thinking in java》

  定义了任务,此时并不涉及多线程,所以,任务本身就是一个类,它的对象我们可以在任意试用到的地方调用,例如:

复制代码
public class TaskMain {
    public static void main(String[] args){
        Task task = new Task();
        task.run();
    }
}
复制代码

  就是在main中声明了该实例的对象,并调用了它的run方法,同我们平时创建类一样来调用对象的方法即可。

  至此,一个任务定义完了。也就是说按照java的要求,我们实现了一个简单的任务。然而,实现任务的目的不只是为了实现任务,而是为了让多线程机制能够调用该任务去执行。请看:Java多线程——<二>将任务交给线程,线程声明


Java多线程——<二>将任务交给线程,线程声明及启动

一、任务和线程

  《thinking in java》中专门有一小节中对线程和任务两个概念进行了具体的区分,这也恰好说明任务和线程是有区别的。

  正如前文所提到的,任务只是一段代码,一段要达成你目的的代码,这段代码写在哪,怎么写其实无所谓,只是因为你希望java的多线程机制能够识别并调用你编写的任务,所以规定了Runnable接口,让你的任务来实现该接口,把你想做的工作在实现该接口的run方法中实现。

  那么,已经定义了任务类,那任务和线程有什么关系呢?

  java的线程是用来驱动任务执行的,也就是说你得把任务挂载到一个线程上,这样该线程才能驱动你定义的任务来执行。

二、定义线程

  1.显示的定义线程的过程就是将任务附着到线程的过程。线程Thread自身并不执行任何操作,它只是用来被多线程机制调用,并驱动赋予它的任务。

  如前次文章提到的任务类定义如下:

复制代码
public class Task implements Runnable {
    protected int countDown = 10;
    private static int taskCount = 0 ;
    private final int id = taskCount++;
    public Task(){}
    public Task(int countDown){
        this.countDown = countDown;
    }
    public String status(){
        return "#"+id+"("+(countDown>0?countDown:"Task!")+").    ";
    }
    @Override
    public void run() {
        while(countDown-->0){
            System.out.print(status());
            Thread.yield();
        }
    }
}
复制代码

  声明线程并将任务附着到该线程上:

Thread t = new Thread(new Task());

  这样,任务就附着给了线程,下面就是让线程启动,只需要如下的调用:

t.start();

  至此,线程声明ok。

  有时,我会想,是不是像任务和线程的概念分离一样,此时只是声明了线程,而java的线程机制并不会调用该线程运行,还需要特殊的调用才能实现多线程执行。但是下面的一段代码告诉我,Thread类的start方法就是触发了java的多线程机制,使得java的多线程能够调用该线程

public static void main(String[] args){
        Thread t = new Thread(new Task());
        t.start();
        System.out.println("Waiting for Task");
}

输出结果如下:

Waiting for Task
#0(9).    #0(8).    #0(7).    #0(6).    #0(5).    #0(4).    #0(3).    #0(2).    #0(1).    #0(Task!).    

  先输出“Waiting for Task”证明调用完start()方法后,立即返回了主程序,并开始执行下面的语句。而你声明的t线程已经去被java的多线程机制调用,并驱动着它的任务运行了。

  2.补充

  想看到更多的线程任务运行,可以用下面的这段代码

复制代码
public static void main(String[] args){
        for(int i = 0 ; i < 5 ; i++){
            new Thread(new Task()).start();
        }
        System.out.println("Waiting for Task");
}
复制代码

  输出如下:

复制代码
Waiting for Task
#0(9).    #2(9).    #4(9).    #0(8).    #2(8).    #4(8).    #0(7).    #2(7).    #4(7).    #0(6).    #2(6).    #4(6).    #0(5).    #2(5).    #4(5).    #0(4).    #2(4).    #4(4).    #3(9).    #2(3).    #4(3).    #2(2).    #4(2).    #2(1).    #4(1).    #2(Task!).    #4(Task!).    #1(9).    #0(3).    #0(2).    #0(1).    #0(Task!).    #3(8).    #1(8).    #3(7).    #1(7).    #3(6).    #1(6).    #3(5).    #3(4).    #3(3).    #3(2).    #3(1).    #3(Task!).    #1(5).    #1(4).    #1(3).    #1(2).    #1(1).    #1(Task!).    
复制代码

  上面的输出说明不同任务的执行在线程被换进换出时混在了一起——由线程调度器自动控制。不同版本的jdk线程调度方式不同,所以产生的结果也不相同。

  这里涉及了垃圾回收器的一个问题,每个Thread都注册了它自己,因此确实有一个对它的引用,而且在它的任务退出其run并死亡之前,垃圾回收器无法清除它。

注:以上代码均来自《thinking in java》,内容大部分是个人理解和总结,如有错误请各位指正


Java多线程——<三>简单的线程执行:Executor

一、概述

  按照《Java多线程——<一><二>》中所讲,我们要使用线程,目前都是显示的声明Thread,并调用其start()方法。多线程并行,明显我们需要声明多个线程然后都调用他的start方法,这么一看,似乎有些问题:第一、线程一旦多了,声明势必是个问题;第二、多线程启动如果通过手动执行的话,那可能一个线程已经跑完了,另外一个还没起来(我推测可能会出现这个问题)。所以,我们在想,如果有个管家,能够帮我们管理这么多线程,只需要把我们定义的任务交给管家,管家就能够帮我们把任务附着到线程上,并且当我们给管家发送指令让所有的线程开始并发执行时,他也能够帮助我们开启所有线程执行。

二、java多线程管家——Executor

  Executor允许你管理异步任务的执行,而无须显示地管理线程的生命周期。ExecutorService知道如何构建恰当的上下文来执行Runnable对象。
  1.创建ExecutorService 

  通过Executors能够创建两种方式的ExectorService。第一种、CachedThreadPool会为每个传入的任务新创建一个线程

  ExecutorService exec = Executors.newCachedThreadPool();

  第二种、FixedThreadPool可以一次性预先执行代价高昂的线程分配,所以可以用来限制线程的数量。这可以节省时间,因为你不必为每个任务都固定的付出创建线程的开销。

  ExecutorService exeService = Executors.newFixedThreadPool(5);

  2.把任务附着给ExecutorService

  有了executor,你只需要定义任务并将任务对象传递给executor即可。

  exeService.execute(new Task());

  3.让所有任务开始执行

  这两个方法会让之前提交到该exectuor的所有任务开始执行。为了避免启动后,会被注入新的任务,必须在你将所有线程注入后,执行关闭操作以保证这一点。

  exeService.shutdown();

总结:

复制代码
    |——原来:想执行任务
    |            |            |——1.定义任务
    |            |            |——2.创建任务对象交由Thread对象操纵
    |            |            |——3.显示的调用Thread对象的start()方法
    |            |——遇到问题:比较繁琐,总得自己启动线程调用;本质上是由main函数调用的
    |——现在:使用java.util.concurrent.Executor(执行器)来管理Thread对象
    |            |            |——1.ExecutorService exec = Executors.newAcahedThreadPool();
    |            |            |——2.exec.execute(new Task());
    |            |            |——3.exec.shutdown();
    |            |——在客户端(显示调用线程执行)和执行任务之间提供了一个间接层,用以执行任务;撇开了main函数,由executor直接进行了调用
    |            |——允许你管理异步任务的执行,而无须显示地管理线程的生命周期
复制代码

三、其他

1.何时使用哪种线程池呢?
  CachedThreadPool在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程,因此它是合理的Executor的首选
  只有当这种方式会引起问题时,才需要切换到FixedThreadPool。

2.SingleThreadExecutor

SingleThreadExecutor就像是线程数量为1的FixedThreadPool
       |——在另一个线程中连续运行的任何事务来说都很有用(重点是连续运行,因为这样可以顺序接受处理),故SingleThreadPool会序列化所有提交给它的任务,       并会维护它自己隐藏的悬挂任务队列
       |——例如:向SingleThreadExecutor提交多个任务,那么这些任务将排队,每个任务都会在下一个任务开始之前运行结束,所有任务使用相同线程。    

3.自定义线程工厂

  每个静态的ExecutorService创建方法都被重载为接受一个ThreadFactory对象,该对象将被用来创建新的线程。例如:

public class TaskDaemonFactory implements ThreadFactory{
    public Thread newThread(Runnable r){
        Thread t = new Thread(r);return t;
    }
}

  想使用自己定义的线程工厂

  ExecutorService exec = Executors.newCachedThreadPool(new TaskDaemonFactory());

  这样可以通过具体的要求来改造线程。


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值