文章目录
前言
为什么要使用线程?
在程序中完成某⼀个功能的时候,我们会将他描述成任务,这个任务需要在线程中完成.
一、串⾏与并发
如果在程序中,有多个任务需要被处理,此时的处理方式可以有串行和并发:
-
串行
(同步):所有的任务,按照⼀定的顺序,依次执行。如果前⾯的任务没有执⾏结束,后⾯的任务等待。 -
并发
(异步):将多个任务同时执行,在⼀个时间段内,同时处理多个任务。
二、进程和线程
-
进程
:是对⼀个程序在运行过程中,占用的各种资源的描述。 -
线程
:是进程中的⼀个最小的执行单元。其中操作系统中,最小的任务执⾏单元并不是线程,而是句柄。只不过句柄过小,操作起来⾮常的麻烦,因此线程就是我们可控的最小的任务执⾏单元。
1.进程和线程的异同
- 相同点: 进程和线程都是为了处理多个任务并发⽽存在的。
- 不同点: 进程之间是资源不共享的,⼀个进程中不能访问另外⼀个进程中的数据。⽽线程之间是资源共享的,多个线程可以共享同⼀个数据。也正因为线程之间是资源共享的,所以会出现临界资源的问题。
2.进程和线程的关系
⼀个进程,在开辟的时候,会⾃动的创建⼀个线程,来处理这个进程中的任务。 这个线程被称为是主线程。在程序运⾏的过程中, 还可以开辟其他线程, 这些被开辟出来的其他线程, 都是⼦线程。
也就是说,⼀个进程中,是可以包含多个线程。⼀个进程中的某⼀个线程崩溃了,只要还有其他线程存在, 就不会影响整个进程的执行。 但是如果⼀个进程中,所有的线程都执行结束了,那么这个进程也就终止了。
3.进程与线程总结
- 程序:⼀个可执行的⽂件
- 进程:⼀个正在运行的程序.也可以理解成在内存中开辟了⼀块儿空间
- 线程:负责程序的运⾏,可以看做⼀条执⾏的通道或执⾏单元,所以我们通 常将进程的⼯作理解成线程的⼯作
- 进程中可不可以没有线程? 必须有线程,⾄少有⼀个.
- 当有⼀个线程的时候我们称为单线程(唯⼀的线程就是主线程). 当有⼀个以上的线程同时存在的时候我们称为多线程.
- 多线程的作⽤:为了实现同⼀时间⼲多件事情(并发执⾏).
三、线程的创建
1.线程创建的四种方法
创建线程主要有四种方法:
- 继承Thread类创建线程对象
- 使用Runnable接口
- 通过 Callable 和 Future 创建线程
- 通过线程池创建线程
2.继承Thread类创建线程对象
继承⾃Thread类,做⼀个Thread的⼦类。在⼦类中, 重写⽗类中的run⽅法,在这个重写的⽅法中,指定这个线程需要处理的任务。
代码示例:
/**
* @author: Mercury
* Date: 2022/3/27
* Time: 13:15
* Description:通过继承Thread来创建线程
* Version:1.0
*/
public class Test3 {
public static void main(String[] args) {
Task2 task2 = new Task2("通过继承Thread创建的线程1");
task2.start();
}
}
class Task2 extends Thread{
private String threadName;
public Task2() {
}
public Task2(String threadName) {
this.threadName = threadName;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(threadName+": "+i);
}
}
}
1) Thread类常用方法
方法名 | 描述 |
---|---|
currentThread() | 返回对当前正在执行的线程对象的引用 |
getId() | 返回此线程的标识符 |
getName() | 返回此线程的名称 |
getPriority() | 返回此线程的优先级 |
getState() | 返回此线程的状态 |
setName(String name) | 将此线程的名称更改为参数name |
sleep(long millis) | 使当前的线程以指定的毫秒数暂停 |
start() | 使此线程开始执行 |
toString() | 返回此线程的名称、优先级和线程组 |
yield() | 对调度程序的一个暗示,即当前线程愿意产生当前使用的处理器 |
3.使用Runnable接口
在Thread类的构造⽅法中,有⼀个重载的构造⽅法,参数是Runnable 接⼝。因此,可以通过Runnable接⼝的实现类对象进⾏Thread对象的实例化。
代码示例:
/**
* @author: Mercury
* Date: 2022/3/27
* Time: 13:05
* Description:通过实现Runnable接口来创建线程
* Version:1.0
*/
public class Test2 {
public static void main(String[] args) {
Task task = new Task();
Thread thread = new Thread(task,"线程1");
thread. start();
}
}
class Task implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
5.继承Thread和使用Runnable接口的优缺点对比
- 继承的⽅式:优点在于可读性⽐较强,缺点在于不够灵活。如果要定制⼀个线程,就必须要继承⾃Thread类,可能会影响原有的继承体系。
- 接⼝的⽅式:优点在于灵活,并且不会影响⼀个类的继承体系。缺点在于可读性较差。
6.通过 Callable 和 Future 创建线程
创建Callable接口的实现类,并实现call()方法,该方法可以作为线程执行体,并且有返回值。接下来创建Callable实现类的实例,并使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法返回值。使用FutureTask对象作为Thread的target创建并启动新线程。然后调用FutureTask对象的get()来获得子线程执行结束后的返回值。如果不需要接受返回值,可以不用这部分操作。
代码示例:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @author: Mercury
* Date: 2022/3/27
* Time: 13:29
* Description:通过 Callable 和 Future 创建线程
* Version:1.0
*/
public class Test4 {
public static void main(String[] args) {
CallableTest callableTest = new CallableTest();
//使用FutureTask类包装Callable对象
FutureTask<Integer> futureTask = new FutureTask<Integer>(callableTest);
//将FutureTask的对象作为参数传递到Thread类中,创建Thread对象,并调用start()
new Thread(futureTask).start();
//如果需要获取返回值的话
try {
Integer i = futureTask.get();
System.out.println("i="+i);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
//创建Callable接口的实现类
class CallableTest implements Callable<Integer>{
//实现call()方法
@Override
public Integer call() throws Exception {
int i = 0;
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+": "+i);
}
return i;
}
}
6.通过线程池创建线程
线程池
:其实就是⼀个容器,⾥⾯存储了若⼲个线程。
使⽤线程池,最主要是解决
线程复⽤
的问题。之前使⽤线程的时候,当我 们需要使⽤⼀个线程时,实例化了⼀个新的线程。当这个线程使⽤结束后,对这个线程进⾏销毁。对于需求实现来说是没有问题的,但是如果频繁的进⾏线程的开辟和销毁,其实对于CPU来说,是⼀种负荷,所以要尽量的优化这⼀点。
可以使⽤复⽤机制解决这个问题。当我们需要使⽤到⼀个线程的时候,不是直接实例化,⽽是先去线程池中查找是否有闲置的线程可以使⽤。如果有,直接拿来使⽤;如果没有,再实例化⼀个新的线程。并且当这个线程使⽤结束后,并不是⻢上销毁,⽽是将其放⼊到线程池中,以便下次继续使⽤。
线程池中的所有线程可以分成俩个部分:
核心线程和临时线程
核心线程
:核⼼线程常驻于线程池中,这些线程,只要线程池存在,他们不会被销毁。只有当线程池需要被销毁的时候,他们才会被销毁。
临时线程
:就是临时⼯。当遇到了临时的⾼密度的线程需求时, 就会临时开辟⼀些线程,处理⼀些任务。这些临时的线程在处理完⾃⼰需要处理的任务后, 如果没有其他的任务要处理,就会闲置。当闲置的时间到达了指定的时间之后,这个临时线程就会被销毁。
代码示例:
package test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author: Mercury
* Date: 2022/3/27
* Time: 12:48
* Description:线程池
* Version:1.0
*/
public class Test1 {
public static void main(String[] args) {
//创建线程池对象,参数10 表示线程池中有10个线程
ExecutorService service = Executors.newFixedThreadPool(10);
//创建Runnable线程对象
Task3 task3 = new Task3();
//从线程池中获取线程对象
service.submit(task3);
service.submit(task3);
//关闭线程池
service.shutdown();
}
}
class Task3 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+": "+i);
}
}
}
1.线程池常用方法
方法 | 描述 |
---|---|
execute(Runnable runnable) | 将任务提交给线程池,由线程池分配线程来并发处理 |
shutdown() | 向线程池发送一个停止信号,并不会停止线程池中的线程,而是在线程池中所有任务结束执行后,结束线程和线程池 |
shutdownNow() | 立即停止线程池中的所有线程和线程池 |
2.线程池的工具类
线程池的开辟,除了可以使⽤构造⽅法进⾏实例化,还可以通过Executors⼯具类进⾏获取。实际应⽤中,⼤部分的场景下,可以不⽤前⾯的构造⽅法进⾏线程池的实例化,⽽是⽤Executors⼯具类中的⽅法进⾏获取。
方法 | 描述 |
---|---|
Executors.newSingleThreadExecutor() | 核心线程数:1,最大线程数:1 |
Executors.newFixedThreadPool(int size) | 核心线程size,最大线程size |
submit() | 向线程池中添加任务 |
shutdown() | 停⽌线程池 |
总结
以上就是与多线程的相关内容,简单介绍了进程和线程以及多线程的四种创建方式,祝各位小伙伴们学习愉快!