进程与线程
线程的发展历程:
-
用户输入一个指令,计算机做一个操作。当用户在思考或输入数据时,计算机就在等待。
-
批处理操作系统出现. 问题在于如果有两个任务A和B,任务A在执行到一半的过程中,需要读取大量的数据输入,而此时CPU只能静静地等待任务A读取完数据才能继续执行,这就白白浪费了CPU资源.
-
解决上面问题的方案: 在内存中装多个程序,当A任务执行耗时的I/O操作时(DMA(Direct Memory Access)芯片接管A任务),让CPU执行B任务. 那么新的问题来了,内存中多个程序使用的数据如何分辩?一个程序运行暂停后,后面再次启动时如何恢复到原来的状态?
-
进程出现了。进程对应一个应用程序,每个进程对应一定的内存地址空间,并且约定每个进程只能使用自己的内存空间,各进程互不干扰。进程空间中可以保存程序运行的状态,CPU轮询每个进程进行执行,当下一次进程重新切换回来时,便可以从进程空间中恢复原来的状态了.
-
进程的缺点是一个进程在一个时间段内只能处理一个任务。所以引入了线程。一个进程中包含多个线程,让每个线程执行一个子任务,如此线程实现了进程内部的并发执行.
-
区别与联系:
一个进程可以包含多个线程,这些线程共享此进程的资源和内存空间。
进程是操作系统资源分配的基本单位,线程是操作系统调度的基本单位.。(请记住这句话)
两者的区别:
- 一个应用程序生成一个进程, 但一个进程可以产生多个线程
- 进程间通信非常麻烦, 但线程非常方便。进程间通信: 数据库, http, rest, … 微服务 , web service
- 进程独享资源, 线程共享它们所属的进程的资源
- 进程结束,则这个进程所产生的线程也会销毁
线程创建篇
1.继承Thread类
/*内部类创建线程*/
public class Test2_Thread extends Thread {
public static void main(String[] args) {
System.out.println("内部类开始运行");
new InnerThread().start();
/*
这两段代码是一样的意思
Thread thread = new InnerThread();
thread.start();*/
}
// 内部类
static class InnerThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
/*外部类创建线程*/
public class Test2_Thread extends Thread {
public static void main(String[] args) {
System.out.println("这是外部类开始运行");
new MyThread().start();
}
}
//外部类
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
简单说,就是新建一个类继承Thread类并重写run()方法,在run()方法中写我们的业务,然后创建类的对象并调用start()方法,就会使run()方法生效。
但是,真的就这么简单吗?我们为什么不直接调用run方法?
在Java中,通过调用线程对象的 start()
方法来启动一个新线程,start()
方法并不直接调用 run()
方法,而是告诉线程调度器(Java虚拟机的一部分),它可以开始调度这个线程。调度器会在适当的时候执行这个线程的 run()
方法。也就是说,进程的调度决定权不在于我们程序员,我们只是告诉调度器,你可以执行这个线程了。而什么时候调度,怎么执行,由调度器的调度策略和优先级决定。
来看看Thread类start()方法的源码:
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
我们注意到:start()方法并没有调用run方法,而是调用了start0()方法:
private native void start0();
start0()
方法声明为private native void start0();
。这意味着它的实现在Java之外的地方提供,通常是在C或C++中。它执行实际的系统级操作,用于管理线程的创建和生命周期。start0()
方法负责执行启动线程所需的底层操作,如资源分配和调用线程的run()
方法。这就再次验证了我们上面的说法。线程的调度确实不是由程序员直接决定的,而是由Java虚拟机(JVM)的调度器来决定。
对于这一段的解释详情请见start()底层分析
2.Runnable接口
Runnable接口是一个功能单一的接口,我们打开Runnable接口的源码就会发现,它内部其实就一 个 void run()方法。通过实现 Runnable 接口,可以将线程的任务与线程的执行分离开来,使得任务可以被多个线程共享执行。这种方式实现了代码的解耦,提高了代码的灵活性和复用性。
比如说我们有N个线程,需要同时启用他们输出当前时间,难道我们真的要写N个类继承Thread然后重写run()方法吗?
不难发现,N个线程所做的事情是一样的,他们有着相同的任务。而我们便可以把这个任务单独拆分出来,实现Runnable接口,在Runnable接口的run方法中单独实现我们的功能,而这样的类我们也叫做任务类。这样,我们就极大的解决了代码冗余的问题了。
以下是Runnable接口实现的几种形式,直接见代码:
public class Test3_Runnable {
public static void main(String[] args) {
// 1.外部类
Runnable run1 = new ShowTimeThread();
Thread t1 = new Thread(run1);// 创建一个任务,绑定任务
t1.start();
// 2.内部类
Runnable run2 = new ShowTimeThreadInner();
Thread t2 = new Thread(run2);
t2.start();
// 3.匿名内部类
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 hh:mm:ss");
Date d = null;
while (true){
d = new Date();
System.out.println(Thread.currentThread().getName()+"输出当前时间:"+sdf.format(d));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
t3.start();
// 4. Lambda表达式 ->Runnable接口支持Lambda表达式
Thread t4 = new Thread(() -> {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 hh:mm:ss");
Date d = null;
while (true){
d = new Date();
System.out.println(Thread.currentThread().getName()+"输出当前时间:"+sdf.format(d));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t4.start();
}
/**
* 内部类
*/
static class ShowTimeThreadInner implements Runnable{
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 hh:mm:ss");
Date d = null;
while (true){
d = new Date();
System.out.println(Thread.currentThread().getName()+"输出当前时间:"+sdf.format(d));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
/**
* 从任务角度看 这是一个任务类,要绑定到线程,由线程启用
* 外部类
* 打印当前时间
*/
class ShowTimeThread implements Runnable{
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 hh:mm:ss");
Date d = null;
while (true){
d = new Date();
System.out.println(Thread.currentThread().getName()+"输出当前时间:"+sdf.format(d));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
继续看源码:
下面三段代码是我从Thread类中截取的源码片段
/* What will be run. */
private Runnable target;
public Thread(Runnable target) {
this(null, target, "Thread-" + nextThreadNum(), 0);
}
@Override
public void run() {
if (target != null) {
target.run();
}
}
/*当我们调用Thread类的有参构造方法将任务类的对象以参数传递给Thread类后,会将任务类的对象作为全局变量保存,最后执行该线程调用线程的run()方法后,实际上调用的就是绑定的任务类对象的run()方法*/
3.实现Caleable接口
使用 Callable
接口的主要目的是为了支持在多线程环境中执行具有返回值的任务,并且能够在任务执行过程中捕获可能抛出的异常。Callable
接口与 Runnable
接口类似,但是具有以下主要区别:
- 返回值:
Callable
接口的call()
方法可以返回一个结果,而Runnable
接口的run()
方法是void
类型的,没有返回值。 - 异常处理:
call()
方法可以声明抛出异常,允许任务抛出检查异常,而run()
方法只能在方法体内处理异常。
public class Test4_callable_FutureTask {
public static void main(String[] args) {
// 方式一:匿名内部类
FutureTask<Integer> task1 = new FutureTask<>(new Callable<Integer>() {
// 功能 实现累加返回结果
@Override
public Integer call() throws Exception {
int result = 0;
for (int i = 0;i<=5;i++){
Thread.sleep(1000);
result++;
// if (i==5){
// throw new RuntimeException("error");
// }
}
return result;
}
});
// 将任务与线程绑定
Thread t1 = new Thread(task1);
t1.start();
// 方式二:lameda写法
FutureTask<Integer> task2 = new FutureTask<>(()->{
int result = 0;
for (int i = 0;i<=5;i++){
Thread.sleep(1000);
result++;
// if (i==5){
// throw new RuntimeException("error");
// }
}
return result;
});
// 将任务与线程绑定
Thread t2 = new Thread(task2);
t2.start();
//有返回值
try {
System.out.println("累加和为"+task1.get());//调用任务的get()方法,获取任务的返回值
System.out.println("累加和为"+task2.get());
System.out.println("请理解说明是阻塞。。。");//只有拿到返回结果,才会继续执行线程
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
4.线程组
Java线程组(ThreadGroup)是一种用于组织和管理线程的机制。它允许将多个线程组织成一个单元,从而更容易进行管理和控制。线程组的主要作用包括组织、控制、监视和安全性。通过线程组,可以将相似或相关的线程放在同一个组内,便于管理;可以对整个线程组执行操作,如挂起、恢复、中断等;可以获取线程组的状态信息,如活动线程数、线程组名称等;还可以用于设置安全性策略,限制组内线程的权限。
线程组可以形成层级结构,即父线程组下可以有子线程组。
简单点说,就是把多个线程放在一起进行统一管理。
public class Test14_threadgroup {
public static void main(String[] args) {
TestTask task1 =new TestTask();
TestTask task2 =new TestTask();
ThreadGroup threadGroup = new ThreadGroup("新建线程组1");
Thread t0 = new Thread(threadGroup,task1);
Thread t1 = new Thread(threadGroup,task2);
t0.start();
t1.start();
// 通过线程组来管理线程
System.out.println("活动的线程数为"+threadGroup.activeCount());
System.out.println("线程组的名字为"+threadGroup.getName());
//线程组中断,则这个组的所有线程都会被中断
threadGroup.interrupt();//发出中断信号
}
}
class TestTask implements Runnable{
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()){
System.out.println("线程名:"+Thread.currentThread().getName());
Thread.sleep(3000);//因为sleep也是线程的一个生命周期,所以只要线程被中断,sleep函数就会抛出异常
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
5.线程池
使用线程池是一种有效管理和利用线程资源的方法,特别是在多线程环境下,它能够提供以下几个重要的优势和功能:
为什么要使用线程池?
- 资源管理:
- 线程池能够有效管理系统中的线程数量。它可以限制并发的线程总数,防止因大量线程导致系统资源耗尽或性能下降的问题。
- 提高响应速度:
- 使用线程池可以减少线程创建和销毁的开销,因为线程的重复利用避免了频繁地创建和销毁线程对象。
- 提高系统稳定性:
- 控制并发线程数量可以避免系统因线程过多而导致的资源竞争和阻塞,从而提高系统的稳定性和可靠性。
- 任务队列管理:
- 线程池通常会使用队列来存储等待执行的任务,这样可以平滑处理突发的大量任务请求,而不至于立即耗尽系统资源。
- 统一管理和调优:
- 可以通过线程池的配置参数(如核心线程数、最大线程数、线程存活时间等)来灵活调整线程池的行为,以适应不同的应用场景和负载情况。
线程池的详细解读
一个典型的线程池通常由以下几个关键组件组成:
- 工作队列(Work Queue):
- 用于存放提交的任务,待线程池中的线程去执行。通常是一个阻塞队列,支持任务的存放和获取操作。
- 线程池管理器(ThreadPool Manager):
- 负责创建、管理和销毁线程池中的线程,同时管理任务队列中的任务。
- 线程池执行器(ThreadPool Executor):
- 负责执行提交到线程池的任务。它会从任务队列中取出任务,并分配给线程池中的线程执行。
- 线程工厂(Thread Factory):
- 用于创建新线程的工厂类。可以定制线程的创建方式,例如命名、优先级等。
- 拒绝策略(Rejected Execution Handler):
- 当任务无法被提交到线程池执行时,用于决定如何处理这些无法处理的任务。例如可以抛弃任务、抛出异常、执行任务的调用线程等。
线程池的配置参数
在使用线程池时,通常需要根据实际需求进行适当的配置,主要的配置参数包括:
- 核心线程数(Core Pool Size):线程池中始终保持的线程数量,即使它们是空闲的。
- 最大线程数(Maximum Pool Size):线程池中允许的最大线程数量。当工作队列满了,并且当前线程数小于最大线程数时,新任务将创建新线程执行。
- 线程存活时间(Keep Alive Time):非核心线程的空闲时间超过这个时间将被终止并从线程池中移除。
- 工作队列(Work Queue):存放待执行任务的队列,可以是有界队列或无界队列。
- 拒绝策略(Rejected Execution Handler):定义了当任务无法被提交执行时的处理方式。
话不多说,直接见代码:
public class Test5_pool {
public static void main(String[] args) {
/*核心线程池带的大小,决定了线程池创建的最小线程数量*/
int corePoolSize = 3;
/*线程池的最大线程数量,这个参数决定了线程池创建的最大线程个数*/
int maxPoolSize =5;
/*线程最大空闲时间*/
long keepAliveTime = 10;
/*时间单位*/
TimeUnit unit = TimeUnit.SECONDS; //enum枚举 常量
/*有界,阻塞队列 容量为2 最多允许放入两个空闲线程 */
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2); //五个正在执行的任务 ,两个等待执行的任务
/*创建线程工厂*/
ThreadFactory threadFactory = new NameThreadFactory();
/*线程拒绝策略*/
RejectedExecutionHandler handler = new MyIgnorePolicy();
ThreadPoolExecutor executor = null;
try{
/*推荐创建线程池的方法*/
/*不推荐使用现成APi生成*/
executor = new ThreadPoolExecutor(corePoolSize,maxPoolSize,keepAliveTime,unit,workQueue,threadFactory,handler);
/*预期动所有线程 提升效率*/
executor.prestartAllCoreThreads();
/*任务数*/
int count = 10;
for (int i = 1;i<=count;i++){
Task task = new Task(String.valueOf(i));
executor.submit(task);//向线程池中提交10个任务
}
}finally {
assert executor!=null;//断言 可开 -ea -da
executor.shutdown();//关闭线程池
}
System.out.println("因为线程池最大线程数量为5,所以只会创建5个线程对象");
}
/**
* 线程工厂,用来创建线程
*/
static class NameThreadFactory implements ThreadFactory{
//AtomicInteger 是一个原子整数类,保证了多线程环境下对 threadId 的安全访问和更新。
//初始值设为 1,表示线程的初始 ID 从 1 开始。
private final AtomicInteger threadId = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r,"线程-"+threadId.getAndIncrement());//相当于:i++ => 1+1 赋值
System.out.println(t.getName()+"已被创建");
return t;
}
}
/**
* 线程拒绝策略,用来对被拒绝任务的处理
*/
public static class MyIgnorePolicy implements RejectedExecutionHandler{
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
doLog(r,executor);
}
private void doLog(Runnable runnable,ThreadPoolExecutor e){
System.err.println("线程池:"+e.toString()+runnable.toString()+"被拒绝");
}
}
/**
* 任务类
*/
static class Task implements Runnable {
private String name;
public Task(String name) {
this.name = name;
}
@Override
public void run() {
try {
System.out.println( this.name + "-isRunning!");
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
最终会有五个线程被创建,七个任务被执行,三个任务被拒绝。