当前位置: 首页 > 科技观察

说说创建线程的三种基本方法

时间:2023-03-21 23:49:22 科技观察

本文转载自微信公众号“飞天小牛”,作者飞天小牛。转载本文请联系飞天小牛公众号。挺基础的知识,一开始不是很愿意写。毕竟这种简单的知识不一定愿意看,写出来给普罗大众也容易,不过整理一下还是有点收获的。比如我看了Thread类的重写。run方法,理解为什么任务(Runnable)可以脱离线程本身(Thread)。创建线程的三种方式线程的英文翻译是Thread,也是Java中线程对应的类名,在java.lang包下。请注意,它实现了Runnable接口,下面将详细解释。结合Threads和Tasks——直接继承Thread类Thread的创建自然需要执行一些特定的任务。一个线程需要执行的任务,或者说需要完成的事情,都是在Thread类的run方法中定义的。这个run方法从何而来?其实并不是Thread类本身。Thread实现了Runnable接口。run方法在这个接口中被定义为一个抽象方法,Thread实现了这个方法。因此,我们把这个Runnable接口称为任务类可能更好理解。下面是集成Thread类创建自定义线程Thread1的例子://自定义线程对象classThread1extendsThread{@Overridepublicvoidrun(){//线程需要执行任务...}}//创建线程对象Thread1t1=newThread1();看这里,Thread类提供了一个可以为线程指定名称的构造函数:那么,我们可以这样做://创建一个线程对象Thread1t1=newThread1("t1");这样控制台打印到时间就比较清楚了,一看就知道是哪个线程在输出。当然,一般来说,我们写的代码是下面匿名内部类的简化版://创建一个线程对象Threadt1=newThread("t1"){@Override//要执行的任务在run中实现methodpublicvoidrun(){//线程需要执行的任务...}};线程与任务分离——线程+实现Runnable接口如果有多个线程,这些线程执行的任务都是一样的,那么就按照上面的方法处理如果不是的话,我们不是要写很多重复的代码吗?因此,我们考虑将线程执行的任务与线程本身分开。classMyRunnableimplementsRunnable{@Overridepublicvoidrun(){//线程需要执行的任务...}}//创建任务类对象MyRunnablerunnable=newMyRunnable();//创建线程对象Threadt2=newThread(runnable);除了避免重复代码,使用实现Runnable接口的方式也比方法一的单继承Thread类更加灵活。毕竟一个类只能继承一个父类。如果类本身继承了其他类,则不能使用第一种方法。.另外,通过这种方式,更容易与线程池等高级API结合。因此,一般来说,推荐使用这种方式来创建线程。也就是说,不建议直接操作线程对象,建议操作任务对象。上述代码使用匿名内部类的简化版本如下://创建一个任务类对象Runnablerunnable=newRunnable(){publicvoidrun(){//要执行的任务...}};//创建一个线程对象Threadt2=newThread(runnable);同样,我们也可以为它指定一个线程名:Threadt2=newThread(runnable,"t2");上面两个Thread的构造函数如图所示:可以发现,Thread类的构造函数无一例外都调用了init方法。这个方法具体是做什么的?我们点进去看看:它将构造函数传入的Runnable对象传递给了一个成员变量target。target是Thread类中定义的Runnable对象,代表要执行的任务(Whatwillberun)。这个变量的存在就是为什么我们可以把任务(Runnable)和线程本身(Thread)分开。看下面这段代码:没错,这是Thread类默认实现的run方法。在使用第一种方法创建线程时,我们定义了一个Thread子类,并重写了其父类的run方法,这样父类实现的run方法就不会被执行,而执行我们自定义的子类。类中的运行方法。在使用第二种方法创建线程时,我们并没有重写Thread子类中的run方法,所以会执行父类默认实现的run方法。这段run方法代码的意思是如果taget!=null,也就是说如果在Thread的构造函数中传入了一个Runnable对象,那么就会执行这个Runnable对象的run方法。线程和任务分离——Thread+实现了Callable接口。Runnable虽然很不错,但是它还是有一个缺点,就是无法获取到任务的执行结果,因为它的run方法的返回值是void。这样,对于需要获取任务执行结果的线程来说,Callable就成为了一个完美的选择。Callable和Runnable基本相同:与Runnbale相比,Callable只是将run改为call。当然,最重要的是!与voidrun不同,此调用方法具有返回值并且可以抛出异常。这样,一个很自然的想法就是将Callable作为任务对象传递给Thread,然后Thread重写call方法完成。但是,不幸的是,Thread类的构造函数不接收Callable类型的参数。因此,我们需要将Callable包装成Runnable类型,以便传递给Thread构造函数。为此,FutureTask成为了最佳选择。可以看出,FutureTask间接继承了Runnable接口,所以也可以看做是一个Runnable对象,可以作为参数传递给Thread类的构造函数。另外,FutureTask还间接继承了Future接口,而这个Future接口定义了get方法,可以获取call()的返回值:看下面的代码,使用Callable定义一个任务对象,然后将Callable包装到FutureTask中,然后把FutureTask传递给Thread的构造函数来创建一个线程对象。另外,Callable和FutureTask的泛型填充的是Callable任务返回的结果类型(即调用方法的返回类型)。classMyCallableimplementsCallable{@OverridepublicIntegercall()throwsException{//要执行的任务......return100;}}//将Callable打包成FutureTask,FutureTask也是一个RunnableMyCallablecallable=newMyCallable();FutureTasktask=newFutureTask<>(callable);//创建线程对象Threadt3=newThread(task);线程运行时,可以通过FutureTask的get方法获取任务运行结果:Integerresult=task.get();但是,需要注意的是,get方法会阻塞当前调用该方法的线程。比如我们在主线程中调用get方法获取t3线程的任务执行结果,那么只有call方法返回成功,主线程才能继续执行。换句话说,如果调用方法永远得不到结果,主线程将永远无法运行下去。启动线程确定。综上所述,我们已经成功创建了线程,那么如何启动它呢?以第一种创建线程的方法为例://创建线程Threadt1=newThread("t1"){@Override//要执行的任务在run方法中实现publicvoidrun(){//任务该线程需要执行……}};//启动线程t1.start();这就涉及到一道经典的面试题,即Whyusestart来启动线程而不是run方法?使用run方法启动线程好像没有问题吧,run方法定义了要执行的任务,调用run方法不执行任务?这个确实是,任务确实可以正确执行,但不是以多线程方式执行。当我们使用t1.run()时,程序还在创建t1线程的主线程下运行,并没有创建新的t1线程。例如://创建线程Threadt1=newThread("t1"){@Overridepublicvoidrun(){//线程需要执行的任务System.out.println("开始执行");FileReader.read(file地址);//读取文件}};t1.run();System.out.println("执行完成");如果使用run方法启动线程,需要在读取文件后输出一句“执行完成”。也就是说读取文件的操作还是同步的。假设读操作耗时5秒,如果没有线程调度机制,CPU在这5秒内什么也做不了,其他代码就得挂起。而如果使用start方法启动线程,在读取文件前会很快输出一句“executed”,因为多线程使得方法执行异步,读取文件的操作由t1线程执行这样做,并且主线程没有被阻塞。