1.什么是进程
进程指的是计算机中运行的程序的实例。每个进程都是一个程序在执行过程中的一个独立单位,它拥有独立的内存空间和系统资源,可以被操作系统调度和管理。
简单来说:正在运行的程序称之为进程。进程它是系统分配资源的基本单位。
2.什么是线程
线程(Thread)是进程中的一个执行单元,是操作系统能够进行运算调度的最小单位。与进程不同的是,线程是在同一个进程中并发执行的多个执行流,它们共享同一进程的资源,包括内存空间、文件描述符和其他系统资源。
3. 进程和线程的关系
- 进程是系统资源(如内存、文件资源)的分配单位。每个进程拥有独立的内存空间和资源。
- 线程是 CPU 调度的基本单位,操作系统将 CPU 时间片分配给线程来执行指令。多个线程共享进程的地址空间和其他资源。
- 一个进程可以包含一个或多个线程,至少包含一个主线程(也称为主程序控制线程),这个线程负责执行程序的主要逻辑。
下面我们来举一个例子理解:
进程:
流水线工厂:整个工厂就是一个进程。它是一个独立的执行环境,有自己的内存空间、资源、和执行状态。
工作人员:在工厂里,每个工作人员都代表一个线程。工作人员可以同时处理不同的任务(例如装配、检验、包装),这些任务可以并行进行。
线程:
任务:每一个具体的任务,比如装配部分、检验部分、包装部分等,可以看作是一个线程。多个任务(线程)可以同时在流水线上执行。
资源共享:在流水线中,工作人员可能需要共享某些资源(比如零件、设备),这就像线程可以共享进程的内存空间和资源一样。
进一步理解:
并行执行:在流水线上,不同的任务可以同时进行,就像多个线程可以同时在同一个进程中执行不同的操作。
资源隔离:虽然在流水线上,每个任务可能需要共享某些资源,但是每个任务有自己的工作区域(工作台),这就像线程可以共享进程的资源,但同时也有自己的栈空间和执行上下文。
整体效率:通过优化流水线上每个任务的工作效率(比如调整任务顺序、减少等待时间),可以提高整个工厂的生产效率;同样,通过合理使用多线程,可以提高进程的整体效率和响应速度。
4. 为什么使用多线程
充分利用了cpu,提高了程序的效率。
让cpu在等待时间执行其它的进程,就像流水线的工人在这条流水线的货物到来的间隔时间,完成其它流水线的任务一样,提高了cpu的利用率,增加了线程序的效率。
5. 创建线程的方式
在java中提供了三种创建线程的方式:
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
5.1 继承Thread类
可以创建一个类,继承自
Thread
类,并重写run()
方法来定义线程的行为。
线程类:
// 继承Thread类
public class ThreadOne extends Thread{
// 重写run方法,表示线程启动后执行的业务代码
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(this.getName()+"~~~"+i+"~~~");
}
}
}
测试类:
@Test
public void threadTest01(){
// 创建线程对象
ThreadOne threadOne = new ThreadOne();
// 开启线程,硬件底层调用线程的run方法
threadOne.start();
for (int i = 0; i < 20; i++) {
System.out.println("main~~"+i+"~~~");
}
}
运行结果:
可以看出,在运行期间会交叉打印,这就是cup在主线程执行的间隙的等待时间去执行了另一个线程
案例:四个窗口各卖一百张票
线程类:
public class ThreadFirst extends Thread {
//四个窗口各卖100张门票
private static int ticket = 100;
@Override
public void run() {
while (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "正在出售第" + ticket + "张票");
ticket--;
}
System.out.println(Thread.currentThread().getName()+"票卖完了");
}
}
测试类:
@Test
public void threadFirst() {
ThreadFirst st1 = new ThreadFirst();
ThreadFirst st2 = new ThreadFirst();
ThreadFirst st3 = new ThreadFirst();
ThreadFirst st4 = new ThreadFirst();
st1.setName("窗口A");
st2.setName("窗口B");
st3.setName("窗口C");
st4.setName("窗口D");
st1.start();
st2.start();
st3.start();
st4.start();
}
运行结果:
5.2 实现Runnable接口
这种方法可以避免单继承的限制,因为Java中一个类只能继承一个父类,但可以实现多个接口。
线程类:
// 实现Runnable接口
public class ThreadTwo implements Runnable{
// 实现run()方法
@Override
public void run() {
//线程执行时的任务代码
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"~~~"+i+"~~~");
}
}
}
测试类:
@Test
public void threadTest02(){
//创建线程任务对象
ThreadTwo threadTwo = new ThreadTwo();
//创建线程对象
Thread thread = new Thread(threadTwo);
// 开启线程
thread.start();
for (int i = 0; i < 20; i++) {
System.out.println("main~~"+i+"~~~");
}
}
运行结果:
案列:四个窗口共卖一百张票
线程类:
public class ThreadSecond implements Runnable{
@Override
public void run() {
//四个窗口共卖100张票
int ticket = 100;
while (true) {
if (ticket <= 0) {
break;
}
//模拟延时
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() + "卖票,剩余" + ticket);
}
}
}
测试类:
@Test
public void threadSecond(){
ThreadSecond threadSecond = new ThreadSecond();
Thread thread1 = new Thread(threadSecond);
Thread thread2 = new Thread(threadSecond);
Thread thread3 = new Thread(threadSecond);
Thread thread4 = new Thread(threadSecond);
thread1.setName("窗口A");
thread1.start();
thread2.setName("窗口B");
thread2.start();
thread3.setName("窗口C");
thread3.start();
thread4.setName("窗口D");
thread4.start();
}
运行结果:
根据运行结果可以分析出会出现重卖的现象,还会出现超卖的现象,即剩余为负,我的电脑运行多次没出现超卖的结果,所以没有截图。
这是因为多个线程共享一个资源,导致了线程安全隐患问题。后期我们可以使用锁解决线程安全问题。本次先不涉及到此部分内容
5.3 实现Callable接口
Callable
接口类似于Runnable
接口,但它允许线程执行有返回值的操作,并且可以在执行完毕后获取结果。
线程类:
// 实现Callable接口
public class ThreadThree implements Callable<Double> {
// 实现call()方法
@Override
public Double call() throws Exception {
// 求一千以内的3的倍数的和
double sum = 0;
for (int i = 0; i < 1000; i++) {
if (i % 3 == 0){
sum += i;
}
}
return sum;
}
}
测试类:
@Test
public void threadTest03() throws Exception{
// 创建线程任务对象
ThreadThree threadThree = new ThreadThree();
// 封装到FutureTask
FutureTask futureTask = new FutureTask<>(threadThree);
// 传递futureTask,创建线程对象
Thread thread = new Thread(futureTask);
// 开启线程
thread.start();
// 获取执行结果
Object o = futureTask.get();
System.out.println(o);
}
运行结果:
5.4 Thread类中常用的方法
start(): 启动线程并使其进入就绪状态。
run(): 线程运行时要执行的代码,通常不直接调用,而是通过start()间接调用。
sleep(long millis): 使线程休眠指定的毫秒数。
join(): 等待该线程终止。
interrupt(): 中断线程。
isAlive(): 判断线程是否处于活动状态。
setPriority(int newPriority): 设置线程的优先级。
getName()和setName(String name): 获取和设置线程的名称。
5.5 三种创建方法的区别
继承Thread类与实现Runnable接口比较:
继承 Thread 类的优缺点:
优点: 实现简单,可以直接调用
start()
方法启动线程。缺点: 由于Java只支持单继承,如果已经继承了其他类,则无法再使用这种方式来实现线程。
实现 Runnable 接口的优缺点:
优点: 灵活性更高,可以避免单继承的限制,一个类可以同时实现多个接口。
缺点: 使用稍微复杂一些,需要创建一个
Thread
对象,并将Runnable
对象传递给它。
实现Runnable接口与实现Callable接口比较:
Runnable
接口的run()
方法没有返回值,不能抛出异常,而Callable
接口的call()
方法可以返回一个结果,并且可以抛出受检异常。Runnable
接口可以直接作为Thread
对象的构造参数,而Callable
接口不能。Callable
接口是对Runnable
接口的补充。