• 线程的四种创建方式


      在JAVA中,用Thread类代表线程,所有线程对象,都必须是Thread类或者Thread类子类的实例。每个线程的任务就是执行一段顺序执行的代码,JAVA使用线程执行体来容纳这段代码。所以,我们创建线程时,主要是根据实际需求,编写放入线程执行体的代码。

    一、继承Thread类创建线程

    1、定义一个类继承Thread类,并重写Thread类的run()方法,run()方法的方法体就是线程要完成的任务,因此把run()称为线程的执行体

    2、创建该类的实例对象,即创建了线程对象

    3、调用线程方法的start方法来启动线程

    代码实例:

    public class Test extends Thread{
        private int i;
        public static void main(String[] args) {
            for(int j=0;j<50;j++){
                //获取当前线程
                System.out.println(Thread.currentThread().getName()+" "+j);
                if(j==10){
                    //启动第一个线程
                    new Test().start();
                    //启动第二个线程
                    new Test().start();
                }
            }
        }
    
        public void run(){
            for(;i<100;i++){
                System.out.println(this.getName()+" "+i);
            }
        }
    }
    View Code

     如上图所示,有三个线程分别为main、Thread-0、Thread-1,线程的执行是抢占式的在没有设置优先级等的情况下。Thread-0 、Thread-1两个线程输出的成员变量 i 的值不连续(这里的 i 是实例变量而不是局部变量)。因为:通过继承Thread类实现多线程时,每个线程的创建都要创建不同的子类对象,导致Thread-0 、Thread-1两个线程不能共享成员变量 i 。

    二、实现Runnable接口创建线程

    1、定义Runnable接口的实现类,重写run()方法,run()方法的方法体就是线程要完成的任务

    2、创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象

    3、调用线程的run()方法来启动线程

    public class Test implements Runnable {
        private int i;
        public static void main(String[] args) {
            for(int j=0;j<50;j++){
                //获取当前线程
                System.out.println(Thread.currentThread().getName()+" "+j);
                if(j==10){
                    Test test = new Test();
                    //启动第一个线程
                    new Thread(test).start();
                    //启动第二个线程
                    new Thread(test).start();
                }
            }
        }
    
        public void run(){
            for(;i<100;i++){
                System.out.println(Thread.currentThread().getName()+" "+i);
            }
        }
    }
    View Code

    线程1和线程2输出的成员变量i是连续的,也就是说通过这种方式创建线程,可以使多线程共享线程类的实例变量,因为这里的多个线程都使用了同一个target实例变量。但是,当你使用我上述的代码运行的时候,你会发现,其实结果有些并不连续,这是因为多个线程访问同一资源时,如果资源没有加锁,那么会出现线程安全问题

    三、实现Callable接口和Future创建线程

      JAVA5提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该类实现了Future接口,并实现了Runnable接口,所以FutureTask可以作为Thread类的target,同时也解决了Callable对象不能作为Thread类的target这一问题。
    在Future接口里定义了如下几个公共方法来控制与它关联的Callable任务:

    1、boolean cancel(boolean mayInterruptIfRunning):试图取消Future里关联的Callable任务;

    2、V get():返回Callable任务里call()方法的返回值,调用该方法将导致程序阻塞,必须等到子线程结束以后才会得到返回值;

    3、V get(long timeout, TimeUnit unit):返回Callable任务里call()方法的返回值。该方法让程序最多阻塞timeout和unit指定的时间,如果经过指定时间后,Callable任务依然没有返回值,将会抛出TimeoutException异常;

    4、boolean isCancelled():如果Callable任务正常完成前被取消,则返回true;

    5、boolean isDone():如果Callable任务已经完成, 则返回true;


    创建启动线程步骤如下:

    1、创建Callable接口实现类,并实现call()方法,该方法将作为线程执行体,且该方法有返回值,再创建Callable实现类的实例

    2、使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值

    3、使用FutureTask对象作为Thread对象的target创建并启动新线程

    4、调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

    public class Test {
        public static void main(String[] args) {
            //这里call()方法的重写是采用lambda表达式,没有新建一个Callable接口的实现类
            FutureTask<Integer> task =  new FutureTask<Integer>((Callable<Integer>)()->{
                int i = 0;
                for(;i < 50;i++) {
                    System.out.println(Thread.currentThread().getName() +
                            "  的线程执行体内的循环变量i的值为:" + i);
                }
                //call()方法的返回值
                return i;
            });
    
            for(int j = 0;j < 50;j++) {
                System.out.println(Thread.currentThread().getName() +
                        " 大循环的循环变量j的值为:" + j);
                if(j == 20) {
                    new Thread(task,"有返回值的线程").start();
                }
            }
    
            try {
                System.out.println("子线程的返回值:" + task.get());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    View Code

    上述代码没有使用创建一个实现Callable接口的类,然后创建一个实现类实例的做法。因为,从JAVA8开始可以直接使用Lambda表达式创建Callable对象,所以上面的代码使用了Lambda表达式;call()方法的返回值类型与创建FutureTask对象时<>里的类型一致。

    四、通过线程器ExecutorServices类

      Executor框架的最大优点是把任务的提交和执行解耦。要执行任务的人只需把Task描述清楚,然后提交即可。这个Task是怎么被执行的,被谁执行的,什么时候执行的,提交的人就不用关心了。具体点讲,提交一个Callable对象给ExecutorService(如最常用的线程池ThreadPoolExecutor),将得到一个Future对象,调用Future对象的get方法等待执行结果就好了。Executor框架的内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。因此,在Java 5之后,通过Executor来启动线程比使用Thread的start方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免this逃逸问题——如果我们在构造器中启动一个线程,因为另一个任务可能会在构造器结束之前开始执行,此时可能会访问到初始化了一半的对象用Executor在构造器中。

      Executor接口中之定义了一个方法execute(Runnable command),该方法接收一个Runable实例,它用来执行一个任务,任务即一个实现了Runnable接口的类。ExecutorService接口继承自Executor接口,它提供了更丰富的实现多线程的方法,比如,ExecutorService提供了关闭自己的方法,以及可为跟踪一个或多个异步任务执行状况而生成 Future 的方法。 可以调用ExecutorService的shutdown()方法来平滑地关闭 ExecutorService,调用该方法后,将导致ExecutorService停止接受任何新的任务且等待已经提交的任务执行完成(已经提交的任务会分两类:一类是已经在执行的,另一类是还没有开始执行的),当所有已经提交的任务执行完毕后将会关闭ExecutorService。因此我们一般用该接口来实现和管理多线程。

       ExecutorService的生命周期包括三种状态:运行、关闭、终止。创建后便进入运行状态,当调用了shutdown()方法时,便进入关闭状态,此时意味着ExecutorService不再接受新的任务,但它还在执行已经提交了的任务,当素有已经提交了的任务执行完后,便到达终止状态。如果不调用shutdown()方法,ExecutorService会一直处在运行状态,不断接收新的任务,执行新的任务,服务器端一般不需要关闭它,保持一直运行即可。

     Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。

    newCachedThreadPool()  

    -缓存型池子,先查看池中有没有以前建立的线程,如果有,就 reuse.如果没有,就建一个新的线程加入池中
    -缓存型池子通常用于执行一些生存期很短的异步型任务
     因此在一些面向连接的daemon型SERVER中用得不多。但对于生存期短的异步任务,它是Executor的首选。
    -能reuse的线程,必须是timeout IDLE内的池中线程,缺省timeout是60s,超过这个IDLE时长,线程实例将被终止及移出池。
      注意,放入CachedThreadPool的线程不必担心其结束,超过TIMEOUT不活动,其会自动被终止。

    newFixedThreadPool(int)  

    -newFixedThreadPool与cacheThreadPool差不多,也是能reuse就用,但不能随时建新的线程
    -其独特之处:任意时间点,最多只能有固定数目的活动线程存在,此时如果有新的线程要建立,只能放在另外的队列中等待,直到当前的线程中某个线程终止直接被移出池子
    -和cacheThreadPool不同,FixedThreadPool没有IDLE机制(可能也有,但既然文档没提,肯定非常长,类似依赖上层的TCP或UDP IDLE机制之类的),所以FixedThreadPool多数针对一些很稳定很固定的正规并发线程,多用于服务器
    -从方法的源代码看,cache池和fixed 池调用的是同一个底层 池,只不过参数不同:
    fixed池线程数固定,并且是0秒IDLE(无IDLE)    
    cache池线程数支持0-Integer.MAX_VALUE(显然完全没考虑主机的资源承受能力),60秒IDLE  

    newScheduledThreadPool(int) -调度型线程池
    -这个池子里的线程可以按schedule依次delay执行,或周期执行
    SingleThreadExecutor() -单例线程,任意时间池中只能有一个线程
    -用的是和cache池和fixed池相同的底层池,但线程数目是1-1,0秒IDLE(无IDLE)

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

      一般来说,CachedTheadPool在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程,因此它是合理的Executor的首选,只有当这种方式会引发问题时(比如需要大量长时间面向连接的线程时),才需要考虑用FixedThreadPool。

    Executor执行Runnable任务

    通过Executors的以上四个静态工厂方法获得 ExecutorService实例,而后调用该实例的execute(Runnable command)方法即可。一旦Runnable任务传递到execute()方法,该方法便会自动在一个线程上

    public class Test {
        public static void main(String[] args) {
    //        ExecutorService executorService = Executors.newCachedThreadPool();
            ExecutorService executorService = Executors.newFixedThreadPool(5);
    //        ExecutorService executorService = Executors.newSingleThreadExecutor();
            for (int i = 0; i < 5; i++){
                executorService.execute(new Runnable(){
                    public void run(){
                        System.out.println(Thread.currentThread().getName() + "线程被调用了。");
                    }
                });
                System.out.println("************* a" + i + " *************");
            }
            executorService.shutdown();
        }
    }
    View Code

    execute会首先在线程池中选择一个已有空闲线程来执行任务,如果线程池中没有空闲线程,它便会创建一个新的线程来执行任务。

    Executor执行Callable任务

       一类是实现了Runnable接口的类,一类是实现了Callable接口的类。两者都可以被ExecutorService执行,但是Runnable任务没有返回值,而Callable任务有返回值。并且Callable的call()方法只能通过ExecutorService的submit(Callable<T> task) 方法来执行,并且返回一个 <T>Future<T>,是表示任务等待完成的 Future。
      

      Callable接口类似于Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常而Callable又返回结果,而且当获取返回结果时可能会抛出异常。Callable中的call()方法类似Runnable的run()方法,区别同样是有返回值,后者没有。

      当将一个Callable的对象传递给ExecutorService的submit方法,则该call方法自动在一个线程上执行,并且会返回执行结果Future对象。同样,将Runnable的对象传递给ExecutorService的submit方法,则该run方法自动在一个线程上执行,并且会返回执行结果Future对象,但是在该Future对象上调用get方法,将返回null。

    public class Test {
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newCachedThreadPool();
            List<Future<String>> resultList = new ArrayList<Future<String>>();
            //创建10个任务并执行
            for (int i = 0; i < 10; i++){
                //使用ExecutorService执行Callable类型的任务,并将结果保存在future变量中
                Future<String> future = executorService.submit(new TaskWithResult(i));
                //将任务执行结果存储到List中
                resultList.add(future);
            }
            //遍历任务的结果
            for (Future<String> fs : resultList){
                try{
                    while(!fs.isDone());//Future返回如果没有完成,则一直循环等待,直到Future返回完成
                    System.out.println(fs.get());     //打印各个线程(任务)执行的结果
                }catch(InterruptedException e){
                    e.printStackTrace();
                }catch(ExecutionException e){
                    e.printStackTrace();
                }finally{
                    //启动一次顺序关闭,执行以前提交的任务,但不接受新任务
                    executorService.shutdown();
                }
            }
        }
    }
    
    class TaskWithResult implements Callable<String> {
        private int id;
    
        public TaskWithResult(int id) {
            this.id = id;
        }
        public String call() throws Exception {
            System.out.println("call()方法被自动调用!!!    " + Thread.currentThread().getName());
            //该返回结果将被Future的get方法得到
            return "call()方法被自动调用,任务返回的结果是:" + id + "    " + Thread.currentThread().getName();
        }
    }
    View Code

    submit也是首先选择空闲线程来执行任务,如果没有,才会创建新的线程来执行任务。另外,需要注意:如果Future的返回尚未完成,则get()方法会阻塞等待,直到Future完成返回,可以通过调用isDone()方法判断Future是否完成了返回。

    public ThreadPoolExecutor (int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue)

    corePoolSize:线程池中所保存的线程数,包括空闲线程。

    maximumPoolSize:池中允许的最大线程数。

    keepAliveTime:当线程数大于核心数时,该参数为所有的任务终止前,多余的空闲线程等待新任务的最长时间。

    unit:等待时间的单位。

    workQueue:任务执行前保存任务的队列,仅保存由execute方法提交的Runnable任务。

    五、四种创建方式对比

    1、采用继承Thread类方式:

    (1)优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程。
    (2)缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类。

    2、采用实现Runnable接口方式:

    (1)优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分               开,形成清晰的模型,较好地体现了面向对象的思想。
    (2)缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。

    3、Runnable和Callable的区别: 

      (1)Callable规定的方法是call(),Runnable规定的方法是run().
      (2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
      (3)call方法可以抛出异常,run方法不可以,因为run方法本身没有抛出异常,所以自定义的线程类在重写run的时候也无法抛出异常
      (4)运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务                    的执行,还可获取执行结果。

    4、线程池

      前三种的线程如果创建关闭频繁会消耗系统资源影响性能,而使用线程池可以不用线程的时候放回线程池,用的时候再从线程池取,项目开发中主要使用线程池

    • start()方法用来,开启线程,但是线程开启后并没有立即执行,他需要获取cpu的执行权才可以执行
    • run()方法是由jvm创建完本地操作系统级线程后回调的方法,不可以手动调用(否则就是普通方法)

    面试题:https://www.jianshu.com/p/3e88a5fe75f0

      

  • 相关阅读:
    cocos2d学习笔录1
    js 动态切换视频
    Android之一个简单计算器源代码
    git-daemon的快捷搭建
    题目1384:二维数组中的查找
    [cocos2d-x]HelloWorldDemo
    ios开发者创建app应用开发授权文件 实战方法:
    一些小工具方法,能将容器转换成指定的数组类型有使用泛型
    Mockito文档-单元测试技术
    【UNIX网络编程】FIFO
  • 原文地址:https://www.cnblogs.com/yfstudy/p/13329055.html
Copyright © 2020-2023  润新知