飞道的博客

@程序员,如何花式构建线程?

167人阅读  评论(0)

作者 | 曾建

责编 | 郭芮

出品 | CSDN(ID:CSDNnews)

在项目和业务的开发中,我们难免要经常使用线程来进行业务处理,使用线程可以保证我们的业务在相互处理之间可以保证原子性,减少相互之间的耦合和影响。通常情况下,我们会使用创建一个继承Thread的对象或实现Runnable接口的类来创建线程,我们很少会注意如何创建线程更简洁,更方便,更能提高开发效率,其实创建线程的方式有很多种,下面就来感受一下创建线程这个操作所拥有的魅力。

Java中创建线程主要有三种方式,我们先来看下第一种方式,通过继承Thread类创建线程类。该方式创建线程有3个步骤:

  • 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。

  • 创建Thread子类的实例,即创建线程对象。

  • 调用线程对象的start()方法来启动该线程。


   
  1. package com.test.thread;
  2. public  class ThreadTest extends Thread{
  3.      /**
  4.      * 需要手动重写run方法,run方法内为线程执行体
  5.      */
  6.      public void run() {
  7.          for (int i =  0; i <  100; i++) {
  8.             System.out.println(getName()+ " "+i);
  9.         }
  10.     }
  11.      public  static void main(String[] args) {
  12.         System.out.println(Thread.currentThread().getName());
  13.          for (int i =  0; i <  100; i++) {
  14.              if(i== 20){
  15.                  new ThreadTest().start();
  16.                  new ThreadTest().start();
  17.             }
  18.         }
  19.     }
  20. }

这种方式创建线程也是最基本和最常用的方式,上述代码中Thread.currentThread()方法返回当前正在执行的线程对象,getName()方法返回调用该方法的线程的名字。

在实际应用过程中,我们经常会使用匿名内部类的方式在方法或代码块中创建线程,其示例如下:


   
  1. public  class ThreadTest2 {  
  2.    public  static void main(String[] args) {  
  3.              new Thread() {  
  4.                  public void run() {  
  5.                      while ( true) {  
  6.                          try {  
  7.                             System.out.println( "线程输出");  
  8.                              //休眠两秒  
  9.                             Thread.sleep( 2 *  1000);  
  10.                         }  catch (InterruptedException e) {  
  11.                             e.printStackTrace();  
  12.                         }  
  13.                     }  
  14.                 };  
  15.             }.start();  
  16.         }  
  17.     } 

这种方式使用了匿名内部类的方式创建线程,在代码块中就可以直接创建线程,从而进行业务处理。这种方式也减少了创建线程类的步骤,直接就可以使用,提高了创建线程的灵活性。

第二种创建线程的主要方式为通过实现Runnable接口创建线程类,该方式创建线程主要有以下3个步骤:

  • 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。

  • 创建Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。

  • 调用线程对象的start()方法来启动该线程。


   
  1. package com.test.thread;
  2. public  class RunnableThreadTest implements Runnable{
  3.     @Override
  4.      public void run() {
  5.          for (int i =  0; i <  100; i++) {
  6.             System.out.println(Thread.currentThread().getName());
  7.         }
  8.     }
  9.      public  static void main(String[] args) {
  10.         System.out.println(Thread.currentThread().getName());
  11.          for (int i =  0; i <  100; i++) {
  12.              if(i== 20){
  13.                 RunnableThreadTest test =  new RunnableThreadTest();
  14.                 Thread thread1 =  new Thread(test);
  15.                 Thread thread2 =  new Thread(test);
  16.                 thread1.start();
  17.                 thread2.start();
  18.             }
  19.         }
  20.     }
  21. }

通过实现Runnable接口创建线程类时,我们需要将该线程类的对象作为Thread类的传参,然后才可以创建对象,因为Thread类的底层封装了一个Runnable接口为参数的构造函数,然后调用Thread类的start()方法开始执行线程内的方法体,Runnable接口为参数的构造函数其代码如下,可以感受一番:


   
  1.    public Thread(Runnable target) {
  2.            // 该方法为Thread类中初始化加载线程的方法
  3.         init( null, target,  "Thread-" + nextThreadNum(),  0);
  4.     }

第三种创建线程的方式主要是通过Callable和Future创建线程。

从继承Thread类和实现Runnable接口可以看出,上述两种方法都不能有返回值,且不能声明抛出异常。而Callable接口则实现了此两点,Callable接口如同Runnable接口的升级版,其提供的call()方法将作为线程的执行体,同时允许有返回值。

但是Callable对象不能直接作为Thread对象的target,不是Runnable接口的子接口。对于这个问题的解决方案,就引入了Future接口,此接口可以接受call()的返回值,RubbaleFuture接口是Future接口和Runnable接口的子接口,可以作为Thread对象的target。并且,Future接口提供了一个实现类:FutureTask。FutureTask实现了RunnableFuture接口,可以作为Thread对象的target。

通过Callable和Future创建线程主要有以下四个步骤:

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

  • 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

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

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


   
  1. package com.test.thread;
  2. import java.util.concurrent.Callable;
  3. import java.util.concurrent.ExecutionException;
  4. import java.util.concurrent.FutureTask;
  5. public  class CallableThreadTest implements Callable<Integer>{
  6.     @Override
  7.      public Integer call() throws  Exception {
  8.         int i= 0;
  9.          for (; i <  10; i++) {
  10.             System.out.println(Thread.currentThread().getName());
  11.         }
  12.          return i;
  13.     }
  14.      public  static void main(String[] args) {
  15.         CallableThreadTest thredClass =  new CallableThreadTest();
  16.         FutureTask<Integer> future =  new FutureTask<>(thredClass);
  17.          for (int i =  0; i <  10; i++) {
  18.             System.out.println(Thread.currentThread().getName());
  19.              if(i== 2){
  20.                  new Thread(future).start();
  21.             }
  22.         }
  23.          try {
  24.             future.get();
  25.         }  catch (InterruptedException e) {
  26.             e.printStackTrace();
  27.         }  catch (ExecutionException e) {
  28.             e.printStackTrace();
  29.         }
  30.     }
  31.     }

上述示例是通过实现Callable接口的普通类来创建线程,通过上述示例我们不仅可以进行异常抛出,还可以手动获取线程的返回值。在实际的应用过程中,我们会经常通过匿名内部类在方法或代码块中创建线程,示例如下:


   
  1. package com.test;
  2. import java.util.concurrent.FutureTask;
  3. import java.util.concurrent.Callable;
  4. public  class FutureThread {
  5.      public  static void main(String[] args) {
  6.         FutureTask<Integer> future =  new FutureTask<Integer>( new Callable<Integer>() {
  7.             @Override
  8.              public Integer call() throws  Exception {
  9.                 System.out.println(Thread.currentThread().getName());
  10.                  return  2+ 3;
  11.             }
  12.         });
  13.         Thread futurerThread =  new Thread(future);
  14.         futurerThread.start();      
  15.     }   
  16. }

当我们通过FutureTask类来创建实例对象时,我们会发现FutureTask的泛型参数是一个必填参数,我们可以打开FutureTask的底层会发现,FutureTask类有两个构造函数,其底层构造代码如下:


   
  1.    /**
  2.      * Creates a {@code FutureTask} that will, upon running, execute the
  3.      * given {@code Callable}.
  4.      *
  5.      * @param  callable the callable task
  6.      * @throws NullPointerException if the callable is null
  7.      */
  8.      public FutureTask(Callable<V> callable) {
  9.          if (callable ==  null)
  10.              throw  new NullPointerException();
  11.         this.callable = callable;
  12.         this.state =  NEW;        // ensure visibility of callable
  13.     }
  14.      /**
  15.      * Creates a {@code FutureTask} that will, upon running, execute the
  16.      * given {@code Runnable}, and arrange that {@code get} will return the
  17.      * given result on successful completion.
  18.      *
  19.      * @param runnable the runnable task
  20.      * @param result the result to return on successful completion. If
  21.      * you don't need a particular result, consider using
  22.      * constructions of the form:
  23.      * {@code Future<?> f = new FutureTask<Void>(runnable, null)}
  24.      * @throws NullPointerException if the runnable is null
  25.      */
  26.      public FutureTask(Runnable runnable, V result) {
  27.         this.callable = Executors.callable(runnable, result);
  28.         this.state =  NEW;        // ensure visibility of callable
  29.     }

第一个构造是通过传入一个Callable的对象创建线程,Callable对象会自动执行call()方法,第二个构造是通过传入一个实现Runnable的对象创建线程,后面有一个result参数,其用来返回线程执行的成功失败的状态。所以我们可以通过以上两种构造方式来创建FutureTask对象,然后将其作为Thread对象的target创建并启动新线程。

当我们了解Java8的时候,你会发现上面创建线程的方式其实是很复杂的。Java8提供了函数式接口编程,函数式接口编程极简化了线程的创建方式,增强了代码的可读性。什么是函数式接口编程呢?jdk8引入的lambda表达式和Stream为Java平台提供了函数式编程的支持,极大的提高了开发效率。

函数式编程针对为函数式接口而言,函数式接口是有且只有一个抽象方法的接口。Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。当我们把一个Lambda表达式赋给一个函数式接口时,这个表达式对应的必定是接口中唯一的抽象方法。因此就不需要以匿名类那么繁琐的形式去实现这个接口。可以说在语法简化上,Lambda表达式完成了方法层面的简化,函数式接口完成了类层面的简化。

函数式编程接口都只有一个抽象方法,编译器会将函数编译后当做该抽象方法的实现。如果接口有多个抽象方法,编译器就不知道这段函数应该是实现哪个方法了。例如:

以Callable接口作为示例,它是一个函数式接口,包含一个抽象方法call()。现在要定义一个Callable对象,传统方式是这样的:


   
  1.     Callable c =  new Callable() {
  2.         @Override
  3.          public Object call() throws  Exception {
  4.             int resultCode =  200;
  5.              return resultCode;
  6.         }
  7.     };

而使用函数式编程,可以这样定义:


   
  1.     Consumer c = (o) -> {
  2.         System.out.println(o);
  3.     }; 

通过了解函数式编程接口之后我们发现通过函数式接口可以极大简化代码和开发效率。当我们在创建线程的时候也可以使用函数式编程接口的方法来创建线程。

示例如下:


   
  1. package com.test.thread;
  2. import java.util.concurrent.Callable;
  3. import java.util.concurrent.ExecutionException;
  4. import java.util.concurrent.FutureTask;
  5. /**
  6.  * 
  7.  * 〈java8 流式创建线程〉<br> 
  8.  * 〈功能详细描述〉
  9.  *
  10.  * @author zengjian
  11.  */
  12. public  class ThreadStreamTest {
  13.      public  static void main(String[] args) {
  14.           try {
  15.               FutureTask<Integer> task =  new FutureTask<Integer>(()->threadMethod());
  16.               Thread taskThread =  new Thread(task);
  17.               taskThread.start();
  18.               Integer result = task.get();
  19.               System.out.println(result);
  20.         }  catch (InterruptedException e) {
  21.             e.printStackTrace();
  22.         }  catch (ExecutionException e) {
  23.             e.printStackTrace();
  24.         }
  25.     }
  26.      public  static int threadMethod(){
  27.          return  2+ 4;
  28.     }
  29. }

通过示例我们会发现,通过函数式编程的方式,极大的简化了创建线程的代码,可以有效的提高代码的可读性和维护性,所以在开发的过程中可以经常使用这种方式进行创建线程,提高我们的开发效率。

FutureTask是为了弥补Thread的不足而设计的,他可以让程序员准确地知道线程什么时候执行完成并获得到线程执行完成后返回的结果。FutureTask是一种可以取消的异步的计算任务。它的计算是通过Callable实现的,它等价于可以携带结果的Runnable,并且有三个状态:等待、运行和完成。完成包括所有计算以任意的方式结束,包括正常结束、取消和异常。Executor框架利用FutureTask来完成异步任务,并可以用来进行任何潜在的耗时的计算。一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。在多线程高并发的场景下用途比较广泛,可以处理多任务异步处理等。

线程知识丰富且复杂,通了解创建线程的这一操作,可以帮助我们理解线程相关的知识和细节,也可以帮忙我们理解在应用过程中多线程和高并发相关的知识。通过这篇文章,希望对你有所帮助哦。

作者:曾建,目前就职于苏宁易购,专注于CDN相关系统开发。

你点的每个“在看”,我都认真当成了喜欢


转载:https://blog.csdn.net/csdnnews/article/details/104075273
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场