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

【Java多线程】简单理解Java“多线程和线程池”的实现机制

程序员文章站 2022-05-05 18:02:34
...

从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。–Wikipedia
(注意:本文默认你有操作系统的“进程、线程”等原理性知识,否则这篇文章可能不适合你。)

进程&线程的定义

进程诞生背景:
1,在多道程序环境下,允许多个程序并发执行,此时它们将失去封闭性,并具有间断性及不可再现性的特征。
2,为此引入了进程(Process)的概念,以便更好地描述和控制程序的并发执行,实现操作系统的并发性和共享性。

进程的定义为: 进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。

线程诞生背景:
1,引入进程的目的,是为了使多道程序并发执行,以提高资源利用率和系统吞吐量;
2,而引入线程,则是为了减小程序在并发执行时所付出的时空开销,提高操作系统的并发性能。

线程的定义为: 是一个进程中的执行实体,一个线程中可以有多个(至少一个)执行实体

理解:
1,你的公司这个整体是个操作系统;
2,公司有多个部门,一个部门下有多个小组,每个小组是一个进程;
3,每个小组下有多个成员,每个成员就是一个线程;
4,当一个项目A下来的时候,公司把A项目拿个你所在技术部的第一小组做;
5,小组就是一个进程,小组中的每个成员就是一个线程。

Java的多线程机制

  1. 每个线程,都有自己的名字。main方法作为主线程,线程名就是“main”,其他新建线程也有名字,默认是“Thread-0”,“Thread-1”类此递增。

  2. 多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。

  3. 每当开启新线程后,立即开辟新的栈区:
    【Java多线程】简单理解Java“多线程和线程池”的实现机制

线程状态的转换:
【Java多线程】简单理解Java“多线程和线程池”的实现机制

Thread类实现多线程

操作步骤:

  1. 自定义一个类继承Thread类。
  2. 重写Thread类的run方法,把自定义线程的任务代码写在run方法上。
  3. 创建Thread的子类对象,并且调用start方法启动一个线程。
  4. 注意:千万不要直接调用run方法,调用start方法的时候线程就会开启,线程一旦开启就会执行run方法中代码,如果直接调用run方法,那么就 相当于调用了一个普通的方法而已。

类方法:

  1. String getName(); 获取线程名字
  2. static Thread currentThread(); 在main线程中执行,返回CPU当前正在执行的线程名
  3. static void sleep(毫秒数); 线程运行到此代码时,睡眠了一定的毫秒数后再执行

第一个Thread类继承实现的多线程:

public class Main {
	public static void main(String[] args) {
		//创建第一个线程实例
		MyThread mt = new MyThread();
		// 修改线程名字
		mt.setName("第一个线程运行");
		// 启动线程
		mt.start(); //告诉系统创建一个独立的线程来运行这个实例中的run方法

		// 创建第二个线程实例
		MyThread mt2 = new MyThread();
		mt2.setName("第二个线程运行");
		// 启动线程
		mt2.start(); //告诉系统又创建一个独立的线程来运行这个实例中的run方法
	}
}
class MyThread extends Thread {
	@Override
	public void run() {//
		for (int i = 0; i < 5; i++) {
			System.out.println(getName() + ":" + i);
		}
	}
}
//注意:每次运行的结果都是不一样的

Runnable接口实现

操作步骤:

  1. 自定义一个类实现Runnable接口。
  2. 实现Runnable接口 的run方法,把自定义线程的任务定义在run方法上。
  3. 创建Runnable实现类对象。
  4. 创建Thread类的对象,并且把Runnable实现类的对象作为实参传递。
  5. 调用Thread对象 的start方法开启一个线程。

操作步骤:

  1. 自定义一个类实现Runnable接口。
  2. 实现Runnable接口 的run方法,把自定义线程的任务定义在run方法上。
  3. 创建Runnable实现类对象。
  4. 创建Thread类的对象,并且把Runnable实现类的对象作为实参传递。
  5. 调用Thread对象 的start方法开启一个线程。

Runnable只有一个方法:

【Java多线程】简单理解Java“多线程和线程池”的实现机制

第一个Runnable接口实现的多线程:

public class Main {
	public static void main(String[] args) {
		//创建线程实例
		MyThread mt = new MyThread();
		Thread t = new Thread(mt);
		// 修改线程名字
		t.setName("张三");
		// 启动线程
		t.start();

		// 创建线程实例
		MyThread mt2 = new MyThread();
		Thread t2 = new Thread(mt2);
		t2.setName("老王");
		// 启动线程
		t2.start();
	}
}
class MyThread implements Runnable {
	@Override
	public void run() {
		for (int i = 0; i < 5; i++) {
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
	}
}

类实现和接口实现的区别

问题:为什么需要定一个类去实现Runnable接口呢?继承Thread类和实现Runnable接口有啥区别呢?
解答:

  1. 实现Runnable接口,避免了继承Thread类的单继承局限性。覆盖Runnable接口中的run方法,将线程任务代码定义到run方法中。
  2. 创建Thread类的对象,只有创建Thread类的对象才可以创建线程。
  3. 线程任务已被封装到Runnable接口的run方法中,而这个run方法所属于Runnable接口的子类对象,所以将这个子类对象作为参数传递给Thread的构造函数,这样,线程对象创建时就可以明确要运行的线程的任务。

匿名内部类实现多线程

public class Main {
	public static void main(String[] args) {
		//继承方式  XXX extends Thread{ public void run(){}}
		new Thread() {
			public void run() {
				System.out.println("!!!");
			}
		}.start();

		// 实现接口方式 XXX implements Runnable{ public void run(){}}
		Runnable r = new Runnable() {
			public void run() {
				System.out.println("###");
			}
		};
		new Thread(r).start();

		//简写法
		new Thread(new Runnable() {
			public void run() {
				System.out.println("@@@");
			}
		}).start();
	}
}

线程池

背景:

  1. 如果反复创建线程,也会有一定的系统资源开销;
  2. 线程池,其实就是一个容纳多个线程的容器;Java初始时提供一定数量的、可重复使用的线程;
  3. 省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

为什么要使用线程池?

  1. 在java中,如果每个请求到达就创建一个新线程,开销是相当的。

  2. 在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。

  3. 如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足

  4. 为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。

  5. 线程池主要用来解决线程生命周期开销问题和资源不足问题

  6. 通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。

  7. 这样,就可以立即为请求服务,使用应用程序响应更快。

  8. 另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。

原理:
1, 程序一开始的时候,创建多个线程对象,存储到集合中;当需要线程的时候,从几回合中获取线程出来;
2, 从JDK1.5开始,程序员不再需要自己开发线程池,而是使用内置线程池技术;

Runnable接口实现线程池

通常,线程池都是通过线程池工厂创建,再调用线程池中的方法获取线程,再通过线程去执行任务方法。

核心类:

  • Executors:线程池创建工厂类public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象(本质上是返回ExecutorService接口的实现类对象)
  • ExecutorService:线程池类Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行
     Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用

特性:

  1. 因为实现的Runnable接口中的run方法,所以此种线程不能有返回值,不能向外抛异常。

使用线程池中线程对象的步骤:

  1. 创建线程池对象
  2. 创建Runnable接口子类对象
  3. 提交Runnable接口子类对象
  4. 关闭线程池

试一试:

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

public class Main {
	public static void main(String[] args) {
		//1. 创建线程池对象
		ExecutorService es = Executors.newFixedThreadPool(2); //这个线程池容量为2
		//2. 创建Runnable接口实现类对象,即创建线程
		ThreadPoolRunnable tpr01 = new ThreadPoolRunnable(); 
		ThreadPoolRunnable tpr02 = new ThreadPoolRunnable();
		
		//3. 把线程放入线程池
		es.submit(tpr01); //放入线程池后,线程自动调用run方法运行
		es.submit(tpr02);
		//4. 销毁线程池
		es.shutdown();
	}
}

class ThreadPoolRunnable implements Runnable {
	public void run(){
		System.out.println(Thread.currentThread().getName()+" 线程提交任务");
	}
}

Callable接口实现线程池

诞生背景:

  • Runnable实现线程池的弊端,该接口的run方法返回值为空(不能返回线程执行完毕后的参数),不能抛出异常。
  • 解决方案是:在java.util.concurrent包中的Callable类中“V call() throws Exception”方法完美地解决了上述的两个弊端。

Callable接口的核心:

  1. 与Runnable接口功能相似,用来指定线程的任务。
  2. 其中的call()方法,用来返回线程任务执行完毕后的结果,call方法可抛出异常。类似于run方法。
  3. 注意:call方法不能接收参数。

试一试:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Main {
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		ExecutorService es = Executors.newFixedThreadPool(2);
		//提交线程任务的方法submit方法返回 Future接口的实现类
		Future<String> f = es.submit(new ThreadPoolCallable());
		String s = f.get();
		System.out.println(s);
	}
}

class ThreadPoolCallable implements Callable<String>{
	public String call() {
		return "abc";
	}
}

多线程示例

需求:两个线程,1个线程计算1+100,另一个线程计算1+200的和

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Main {
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		ExecutorService es = Executors.newFixedThreadPool(2);
		Future<Integer> f1 =es.submit(new GetSumCallable(100));
		Future<Integer> f2 =es.submit(new GetSumCallable(200));
		System.out.println(f1.get());
		System.out.println(f2.get());
		es.shutdown();
	}
}

class GetSumCallable implements Callable<Integer>{
	private int a;
	public GetSumCallable(int a){ //利用构造器,规避了call方法不能传参的问题
		this.a=a;
	}
	
	public Integer call(){ //多线程执行的方法
		int sum = 0 ;
		for(int i = 1 ; i <=a ; i++){
			sum = sum + i ;
		}
		return sum;
	}
}

注意:以上的部分内容描述和图片来自“黑马程序员”上课笔记。