java创建多线程的四种方式及其区别

本文详细介绍Java中创建多线程的四种主要方法:继承Thread类、实现Runnable接口、使用Callable和Future、以及使用线程池。每种方法都有详细的步骤说明和示例代码,帮助读者理解不同场景下多线程的创建和管理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 java创建多线程有如下四种方式:

  1. 继承Thread类创建线程
  2. 实现Runnable接口创建线程
  3. 使用Callable和Future创建线程
  4. 使用线程池创建(使用java.util.concurrent.Executor接口)

下面分别介绍这四种创建多线程的方式

1. ----------------------继承Thread类创建线程--------------------------------

通过继承Thread类来创建并启动多线程的一般步骤如下:

1】定义Tread类的子类MyThread,并重写run()方法.run()方法的方法体(线程执行体)就是线程要执行的任务。

2】创建MyThread类的实例

3】调用子类实例的start()方法来启动线程

示例代码:

public class Mythread extends Thread {
	
	private int i;
	public void run(){//run()是线程类的核心方法
		for(int i=0;i<10;i++){
			System.out.println(this.getName()+":"+i);
		}
	}
	public static void main(String[] args) {
		Mythread t1=new Mythread();
		Mythread t2=new Mythread();
		Mythread t3=new Mythread();
		t1.start();
		t2.start();
		t3.start();
	}
}

2.--------------------------------实现 Runnable接口创建线程---------------------------------

通过实现Runnable接口创建并启动线程的一般步骤如下:

1】定义Runnable接口的实现类,必须重写run()方法,这个run()方法和Thread中的run()方法一样,是线程的执行体

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

3】调用start()方法

实例代码:

public class MyThread implements Runnable{

	@Override
	public void run() {
		for(int i=0;i<10;i++){
			System.out.println(Thread.currentThread().getName()+" : "+i);
		}
		
	}
	public static void main(String[] args) {
		MyThread myThread=new MyThread();
		Thread thread1=new Thread(myThread,"线程1");
		Thread thread2=new Thread(myThread,"线程2");
		Thread thread3=new Thread(myThread,"线程3");
		thread1.start();
		thread2.start();
		thread3.start();
	}
}

继承Tread和实现Runnable接口的区别:

a.实现Runnable接口避免单继承局限

b.当子类实现Runnable接口,此时子类和Thread的代理模式(子类负责真实业务的操作(调用start()方法),thread负责资源调度与线程创建辅助真实业务。

3--------------------------覆写Callable接口实现多线程------------------------------------

和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大:

1>call方法可以有返回值

2>call()方法可以声明抛出异常

Java5提供了Future接口来代表Callable接口里call()方法的返回值,并且为Future接口提供了一个实现类FutureTask,这个实现类既实现了Future接口,还实现了Runnable接口,因此可以作为Thread类的target。在Future接口里定义了几个公共方法来控制它关联的Callable任务。

>boolean cancel(boolean mayInterruptIfRunning):视图取消该Future里面关联的Callable任务

>V get():返回Callable里call()方法的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值

>V get(long timeout,TimeUnit unit):返回Callable里call()方法的返回值,最多阻塞timeout时间,经过指定时间没有返回抛出TimeoutException

>boolean isDone():若Callable任务完成,返回True

>boolean isCancelled():如果在Callable任务正常完成前被取消,返回True

介绍了相关的概念之后,创建并启动有返回值的线程的步骤如下:

1】创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。

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

3】使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)

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

实例代码:

public class MyThread implements Callable<String>{//Callable是一个泛型接口

	@Override
	public String call() throws Exception {//返回的类型就是传递过来的V类型
		for(int i=0;i<10;i++){
			System.out.println(Thread.currentThread().getName()+" : "+i);
		}
		
		return "Hello Tom";
	}
	public static void main(String[] args) throws Exception {
		MyThread myThread=new MyThread();
		FutureTask<String> futureTask=new FutureTask<>(myThread);
		Thread t1=new Thread(futureTask,"线程1");
		Thread t2=new Thread(futureTask,"线程2");
		Thread t3=new Thread(futureTask,"线程3");
		t1.start();
		t2.start();
		t3.start();
		System.out.println(futureTask.get());
		
	}
}

FutureTask非常适合用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。 FutureTask可以确保即使调用了多次run方法,它都只会执行一次Runnable或者Callable任务,或者通过cancel取消FutureTask的执行等。

 

思考?

同时实现Runnable和Callable接口会怎么样?

public class MyThread implements Callable<String>,Runnable{//Callable是一个泛型接口

	@Override
	public String call() throws Exception {//返回的类型就是传递过来的V类型
		for(int i=0;i<10;i++){
			System.out.println(Thread.currentThread().getName()+" : "+i);
		}
		
		return "Hello Tom";
	}
	public static void main(String[] args) throws Exception {
		MyThread myThread=new MyThread();
		FutureTask<String> futureTask=new FutureTask<>(myThread);
		Thread t0=new Thread(myThread,"线程0");
		Thread t1=new Thread(futureTask,"线程1");
		Thread t2=new Thread(futureTask,"线程2");
		Thread t3=new Thread(futureTask,"线程3");
		t0.start();
		t1.start();
		t2.start();
		t3.start();
		System.out.println(futureTask.get());
		
	}
	@Override
	public void run() {
		for(int i=0;i<10;i++){
			System.out.println(Thread.currentThread().getName()+" : "+i);
		}
		
	}
}

使用Runnable接口实现类做target创建的的线程和FutureTask的线程交叉着运行,但是由于FutureTask只执行一次Callable任务,在一次运行结束后就终止了程序。

 

4.-------------------使用线程池例如用Executor框架----------------------------------------------

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

 

    Executor框架包括:线程池,Executor,Executors,ExecutorService,CompletionService,Future,Callable等。

 

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

 

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

 

 

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

    public static ExecutorService newFixedThreadPool(int nThreads)

    创建固定数目线程的线程池。

    public static ExecutorService newCachedThreadPool()

    创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线   程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。

    public static ExecutorService newSingleThreadExecutor()

    创建一个单线程化的Executor。

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

    创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。

 

    这四种方法都是用的Executors中的ThreadFactory建立的线程,下面就以上四个方法做个比较:   
 

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()
-单例线程,任意时间池中只能有一个线程https://blog.youkuaiyun.com/baihualindesu/article/details/89555758
-用的是和cache池和fixed池相同的底层池,但线程数目是1-1,0秒IDLE(无IDLE)

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

常用的七种线程池:https://blog.youkuaiyun.com/baihualindesu/article/details/89555758

 

下面是java中作为线程池Executor底层实现类的ThredPoolExecutor的构造函数:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
       ...
    }

其中各个参数含义如下:
corePoolSize- 池中所保存的线程数,包括空闲线程。需要注意的是在初创建线程池时线程不会立即启动,直到有任务提交才开始启动线程并逐渐时线程数目达到corePoolSize。若想一开始就创建所有核心线程需调用prestartAllCoreThreads方法。
maximumPoolSize-池中允许的最大线程数。需要注意的是当核心线程满且阻塞队列也满时才会判断当前线程数是否小于最大线程数,并决定是否创建新线程。
keepAliveTime - 当线程数大于核心时,多于的空闲线程最多存活时间
unit - keepAliveTime 参数的时间单位。
workQueue - 当线程数目超过核心线程数时用于保存任务的队列。主要有3种类型的BlockingQueue可供选择:无界队列,有界队列和同步移交。将在下文中详细阐述。从参数中可以看到,此队列仅保存实现Runnable接口的任务。
threadFactory - 执行程序创建新线程时使用的工厂。
handler - 阻塞队列已满且线程数达到最大值时所采取的饱和策略。java默认提供了4种饱和策略的实现方式:中止、抛弃、抛弃最旧的、调用者运行。

Executor执行Runnable任务

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

import java.util.concurrent.ExecutorService;   
import java.util.concurrent.Executors;   
  
public class TestCachedThreadPool{   
    public static void main(String[] args){   
        ExecutorService executorService = Executors.newCachedThreadPool();   
        for (int i = 0; i < 3; i++){  //执行三个任务,那么线程池中最多创建三个线程
            executorService.execute(new TestRunnable());   
            System.out.println("************* a" + i + " *************");   
        }   
        executorService.shutdown();   
    }   
}   
  
class TestRunnable implements Runnable{   
    public void run(){   
    	for(int i=0;i<5;i++){
    		 System.out.println(Thread.currentThread().getName() + "线程被调用了。");   
    	}
    }   
}  

上面是某次执行后的结果,可以看出,当有任务需要执行时,首先选择线程池中一个执行完任务的线程复用执行,如果线程池中没有可利用的线程,就创建一个新的线程。

Executor执行Callable任务

    在Java 5之后,任务分两类:一类是实现了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。

示例代码如下:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;

public class CachedThreadPool{   
    public static void main(String[] args) throws Exception{   
        ExecutorService executorService = Executors.newCachedThreadPool();   
//      ExecutorService executorService = Executors.newFixedThreadPool(5);  
//      ExecutorService executorService = Executors.newSingleThreadExecutor();  
        for (int i = 0; i < 2; i++){  
        	TestCallable callable=new TestCallable();
        	FutureTask<String>futureTask=new FutureTask<>(callable);
            executorService.execute(futureTask);   
            System.out.println(futureTask.get());
            System.out.println("************* a" + i + " *************");   
        }   
        executorService.shutdown();   
    }   
}   
  
class TestCallable implements Callable{

	@Override
	public Object call() throws Exception {
		
		for(int i=0;i<5;i++){
			System.out.println(Thread.currentThread().getName()+" :"+i);
		}
		return "Callable is Used!";
	}   
    
}  

运行结果:

************* a0 *************
pool-1-thread-1 :0
pool-1-thread-1 :1
pool-1-thread-1 :2
pool-1-thread-1 :3
pool-1-thread-1 :4
Callable is Used!
************* a1 *************
pool-1-thread-1 :0
pool-1-thread-1 :1
pool-1-thread-1 :2
pool-1-thread-1 :3
pool-1-thread-1 :4
Callable is Used!

当通过实现Callable接口创建线程时,调用了Future接口的实现类FutureTask,该类还实现了Runnable接口,所以可以让实例化对象作为Thread对象的Target。并且每次只执行一个线程的任务,当执行完线程的主任务后,返回值。

 

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class RunNewFixedThreadPool {
    public static void main(String[] args) {
    	RunNewFixedThreadPool run = new RunNewFixedThreadPool();
        ExecutorService executorService = Executors.newFixedThreadPool(6);
        for (int i = 0; i < 5; i++) {//创建五个线程
            executorService.execute(run.new MyRunnable(" "+(i+1)));
        }
        for (int i = 5; i < 10; i++) {//再创建五个线程,如果当前线程池中有线程可以使用,则复用,否则创建新的线程,但是线程池中最多只能有6个线程
            executorService.execute(run.new MyRunnable(" "+(i+1)));
        }
    }
    
    public class MyRunnable implements Runnable{
        private String username;
        
        public MyRunnable(String username) {
            this.username = username;
        }
 
 
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+" username="+username+" begin "+System.currentTimeMillis());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" username="+username+" end   "+System.currentTimeMillis());
        }
        
    }
}

 

这里面每次创建的线程数可以设定,上面设为了5,所以一次性开启了五个线程。如果设为3,那么一次最多开三个进程,并且只有当一些进程关闭时,才会开启相同数目的进程。

 

--------------------------------------四种创建线程方法对比--------------------------------------

实现Runnable和实现Callable接口的方式基本相同,不过是后者执行call()方法有返回值,后者线程执行体run()方法无返回值,并且如果使用FutureTask类的话,只执行一次Callable任务。因此可以把这两种方式归为一种这种方式与继承Thread类的方法之间的差别如下:

1、线程只是实现Runnable或实现Callable接口,还可以继承其他类。

2、这种方式下,多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。

3.继承Thread类只需要this就能获取当前线程。不需要使用Thread.currentThread()方法

4、继承Thread类的线程类不能再继承其他父类(Java单继承决定)。

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

6.实现接口的创建线程的方式必须实现方法(run()  call())。


 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值