欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

线程创建的3种方式

程序员文章站 2022-04-05 12:38:30
...

线程和进程介绍

几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序,每个运行中的的程序就是一个进程。当一个程序运行时,内部可能包含多个顺序执行流

所有运行中的任务通常对应一个进程(Process) 。当一个程序进入内存运行中,即变成一个进程。进程是处于运行过程中的程序,并具有一定的独立功能,进程是系统进行资源分配调度的一个独立单位。

  • 独立性: 进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过程序本身运行的情况下,一个用户进程不可以直接访问其他进程的地址空间

  • 动态性: 进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的

  • 并发性: 多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。

注意并发性并行性是两个概念,并行指在同一时刻,有多条指令在多个处理器上同时执行;并发指在同一时刻只有一条指令执行,但多个进程指令被快速轮换执行,使得宏观上具有多个进程同时执行的效果


多线程的优势

  • 线程在程序中是独立的、并发的执行流,与分隔的进程相比,进程中程序之间的隔离程序要小。它们共享内存、文件句柄和其他每个进程应有的状态。

  • 因为线程的划分尺度小于进程,使得多线程程序的并发性高。进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

  • 线程比进程具有更高的性能,这是由于同一个进程中的线程都有共性——多个线程共享同一个进程虚拟空间。线程共享的环境包括: 进程代码段、进程的公有数据等。利用这些共享数据,线程很容易实现互相之间的通信。

总结起来、多线程编程具有如下优点 :

  1. 进程之间不能共享内存,但线程之间共享内存非常容易。
  2. 系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小的多,因此使用多线程来实现多任务并发比多进程的效率高。
  3. Java 语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了 Java 的多线程编程

线程的创建和启动

  • Java 使用 Thread 类代表线程,所有的线程对象都必须是 Thread 类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流(一段顺序的代码)。Java 使用线程执行体来代表这段程序流。

1、继承 Thread 类 创建线程类

  • 通过继承 Thread 类来创建并启动多线程的步骤如下.
  1. 定义 Thread 类的子类,并重写该类的 run() 方法,该run() 方法的方法体就代表了线程需要完成的任务。因此把run() 方法称为线程执行体。
  2. 创建 Thread 子类的实例,即创建了线程对象
  3. 调用线程对象的 start() 方法来启动该线程

示例如下:

public class ThreadDemo extends Thread{
	private int i ;
	
	//重写 run() 方法,run() 方法的方法体就是线程的执行体
	
	public void run(){
		
		for(; i < 100; i++){	
			//当线程继承 Thread 类时,直接使用 this 即可获取当前线程
			//Thread 对象的 getName() 返回当前线程的名字
			//因此可以直接调用getName() 方法返回当前线程的名字
			System.out.println(getName() + " " + i);
		}
		
		
	}
	
	public static void main(String[] args) {
		for(int i = 0; i < 100; i++){
			//调用 Thread 的 currentThread()方法获取当前线程
			System.out.println(Thread.currentThread()+" "+i);
			
			if(i == 20){
				//创建并启动第一个线程
				new ThreadDemo().start();
				//创建并启动第二个线程
				new ThreadDemo().start();
			}
		}
	}
	
}
  • 当 Java 程序开始运行后,程序至少会创建一个主线程,主线程的线程执行体不是由 run() 方法确定的,而是由 main() 方法确定的 —— main() 方法的方法体代表主线程的线程执行体。
    注意:进行多线程编程时不要忘记了 Java 程序运行时默认的主线程, main() 方法的方法体就是主线程的执行体。
  • 上列中用到的2个方法
  1. Thread.currentThread() : currentThread() 是 Thread 类的静态方法,该方法总是返回当前正在执行的线程对象。
  2. getName(): 该方法是 Thread 类的实例方法,该方法返回调用该方法的线程名字。
  3. 补充: setName() : 程序可以通过此方法设置线程的名字

注意使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量。


2、实现 Runnable 接口 创建线程类

  • 实现 Runnable接口创建并启动多线程的步骤如下。
  1. 定义Runnable 接口的实现类,并重写该接口中的 run() 方法, 该run() 方法的方法体同样是该线程执行体。

  2. 创建 Runnable 实现类的实例,并为此实例作为 Thread 的 target(目标) 来创建Thread 对象,该 Thread 对象才是真正的线程对象。代码示例

//以 Runnable 实现类的对象作为 Thread 的 target 来创建 Thread 对象,即线程对象。
//Runnable 是函数式接口
//此构造 Thread(Runnable target) 分配一个新的 Thread对象。 
new Thread ( () -> { });
  1. 调用线程对象的 start() 来启动线程.

关于步骤 2 我们来说明下

  • 创建Thread类的对象,只有创建Thread类的对象才可以创建线程。

  • 线程任务已被封装到Runnable接口的run方法中,而这个run方法所属于Runnable接口的子类对象,所以将这个子类对象作为参数传递给Thread的构造函数,这样,线程对象创建时就可以明确要运行的线程的任务。

我们知道线程执行体是由run()方法确定的,我们来观察下 Thread 类中的 run() 方法

 @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

由上源代码可以明确的看出 只要 target 不为空就执行 target.run() 方法,target 是 Runnable 实现类对象。

public class RunnableDemo implements Runnable{
	private int i ;
	
	//重写 run() 方法,run() 方法的方法体就是线程的执行体
	
	public void run(){
		
		for(; i < 100; i++){	
			//当 线程实现Runnable 接口时
			//如果想获取当前线程,只能用 Thread.currentThread() 方法
			System.out.println(Thread.currentThread().getName() + " " + i);
		}	
	}
	
	public static void main(String[] args) {
		for(int i = 0; i < 100; i++){
			//调用 Thread 的 currentThread()方法获取当前线程
			System.out.println(Thread.currentThread()+" "+i);
			
			if(i == 20){				
				RunnableDemo st = new RunnableDemo();
				//通过 new Thread(target, name)方法创建新线程
				new Thread(st, "新线程1");
				new Thread(st, "新线程2");
			}
		}
	}
	
}

结论: 发现采用 Runnable 接口的方式创建的多个线程可以共享线程类的实例变量。这是因为在这种方式下,程序所创建的 Runnable 对象只是线程的 target,而多个线程可以共享同一个 target,所以多个线程可以共享一个线程类(实际上应该是线程的 target 类) 的实例变量。


3、使用 Callable 和 Future 创建线程
从 Java 5开始,Java提供了 Callable 接口,该接口怎么看都是 Runnable 接口的增强版,Callable 接口提供了一个 Call() 方法可以作为线程执行体,但 Call() 方法比 run() 方法功能更强大。

  • call() 方法可以有返回值。
  • call() 方法可以声明抛出异常。
  1. 因此完全可以提供一个 Callable 对象作为 Thread 的 target,而该线程的执行体就是该 Callable 对象的 Call() 方法。

  2. Callable 接口是 Java 5 新增的接口,而且它不是 Runnable 接口的子接口,所以 Callable 对象不能直接作为
    Thread 的 target。

  3. Call() 方法还有一个返回值——call() 方法并不是直接调用,它是作为程序执行体被调用的。

  4. Java 5 提供了 Future 接口来代表 Callable 接口里 call() 方法的返回值,并为 Future 接口提供了一个 FutureTask 实现类,该实现类实现了 Future 接口,并实现了 Runnable 接口——可以作为 Thread 类的 target。

Future 接口里定义了如下几个公共方法来控制它关联的 Callable 任务

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

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

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

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

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

注意:Callable 接口有泛型限制,Callable 接口里的泛型形参类型与call() 方法返回值类型相同。而且Callable 接口是 函数式接口,因此可使用 Lambda 表达式 创建Callable 对象。

  • 创建并启动有返回值的线程的步骤如下。
    1. 创建 Callable 接口的实现类,并实现 call() 方法,该call() 方法将作为线程执行体,且该call() 方法有返回值,再创建 Callable 实现类的实例。

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

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

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

下面代码实际:

public class CallableDemo {
	
	public static void main(String[] args) {
			
		//创建 Callable 对象
		//先使用 Lambda 表达式 创建 Callable<Integer>对象
		//使用 FutureTask 来包装 Callable 对象
		
		FutureTask<Integer> task = new FutureTask<>( ()->{
			int i = 0;
			for( ; i < 100; i++){
				System.out.println(Thread.currentThread().getName()+"的循环变量值: " + i);
			}
			//call()方法可以有返回值
			return i;
		});
		
		
		for(int i = 0; i < 100; i++){
			System.out.println(Thread.currentThread().getName() +"的循环变量值: " + i);
			
			if(i == 20){
				//实质还是以 Callable 对象来创建并启动 线程的
				new Thread(task, "有返回值的线程").start();
			}
		}
		
		try {
			System.out.println("子线程的返回值 : " + task.get());
			
		} catch (InterruptedException | ExecutionException e) {
			
			e.printStackTrace();
		}
		
	}

}
  • 实现 Callable 接口与实现 Runnable 接口并没有太大差别,只是 Callable 的 call() 方法允许声明抛出异常,而且允许带返回值。
  • 程序最后调用 Future 对象的 get() 方法来返回 call() 方法的返回值——该方法将导致主线程被阻塞,直到 call() 方法结束并返回为止。

关于实现 Callable 接口方式创建线程,作为 Thread 构造 参数 target 传递的 只是 Futrue 接口实现类,
Future有一个 FutureTask 实现类,此类重写了 run() 方法,在此run() 方法中只是调用了 Callable接口实例的 call() 方法,实则和实现Runnable 一样,只是多包装了一成,而 Future 这一层就类似 实现 Runnable 一样。


创建线程的三种方式对比

通过继承 Thread 类或 实现 RunnableCallable 接口都可以实现多线程,不过实现 Runnable 接口与实现 Callable 接口的方式基本相同, 只是 Callable 接口里定义的 方法有返回值,可以声明抛出异常而已。因此可以将实现 Runnable 接口和实现 Callable 接口归为一种方式。这种方式与继承 Thread 方式之间的主要差别如下:

  • 采用实现 Runnable、Callable接口的方式创建多线程的优缺点:
  1. 线程类只是实现了 Runnable 接口 或 Callable 接口,还可以继承其他类。
  2. 在这种方式下,多个线程可以共享同一个 target 对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将 CPU、代码和数据分开,形成清晰的模型,较好的提醒了面向对象思想。
  3. 劣势是,编程稍稍复杂,如果需要范围当前线程,则必须使用 Thread.currentThread() 方法。
  • 采用继承 Thread 类的方式创建多线程的优缺点:
  1. 劣势是,因为线程类是已继承了 Thread 类,所以不能再继承其他父类。
  2. 优惠是,编写简单,如果需要访问当前线程,则无需 Thread.currentThread()方法,直接使用 this 即可获得当前线程

一般推荐采用实现 Runnable 接口、Callable 接口的方式来创建多线程。