1. 什么是线程和进程?
- 进程:进程是操作系统分配资源和调度的基本单位,它是一个正在运行的程序,每个进程都有独立的内存空间和系统资源。一个程序可以同时启动多个进程。
- 线程:线程是进程中的一个执行单元,负责执行程序的代码。一个进程可以包含多个线程,这些线程共享进程的内存空间和资源,但每个线程有自己的栈和寄存器。
让我们用一个现实生活中的例子来解释进程和线程。
1.1 进程和线程的通俗解释
- 进程:可以把进程想象成一家餐厅。在这家餐厅中,餐厅本身就是一个进程,它有自己的厨房、餐厅、工作人员和顾客。每个餐厅(进程)都是独立的,有自己的空间和资源。
- 线程:在这家餐厅中,每个厨师就是一个线程。多个厨师(线程)在同一个厨房(进程)里工作,共享厨房的资源,比如灶台、锅、食材等。每个厨师有自己的任务和工作区域,但他们都在为餐厅(进程)服务。
1.2 具体例子
-
餐厅(进程):假设有两家餐厅A和B,每家餐厅都有自己的厨房、菜单和顾客。餐厅A的厨师和餐厅B的厨师不会共享资源,他们各自独立运行,就像两个独立的进程。
-
厨师(线程):在餐厅A里,有多个厨师同时在厨房工作。他们可以同时准备不同的菜肴(执行不同的任务),但他们共享厨房的所有资源(内存、灶台等)。即使他们同时工作,他们的工作还是在同一个餐厅(进程)里进行的。
1.3 关键点
- 进程:是独立运行的实体,有自己的内存空间和系统资源。餐厅A和餐厅B彼此独立。
- 线程:是进程中的执行单元,多个线程共享进程的资源。餐厅A里的多个厨师共享同一个厨房。
1.4 进一步理解
- 餐厅扩展:如果餐厅(进程)需要扩展,比如增加新的菜品或提高服务速度,可以增加更多的厨师(线程)来同时准备更多的菜品,提高效率。
- 线程的好处:使用线程可以让餐厅(进程)更高效,因为多个厨师(线程)可以同时工作,提高了资源的利用率和任务的执行速度。
- 线程的问题:多个厨师(线程)在同一个厨房(进程)里工作时,可能会出现争抢资源的情况,比如争用灶台或食材,需要协调和管理,避免冲突。
2. 创建线程的方式
2.1 继承Thread类
通过创建一个继承自
Thread
类的子类,然后重写run
方法。
class MyThread extends Thread {
public void run() {
System.out.println("Thread is running");
}
}
//在main方法中创建线程,调用start方法开启线程
MyThread t = new MyThread();
t.start();
案例
四个窗口各卖100张门票。
① 线程类
public class SellTicketThread extends Thread{
//四个窗口各卖100张门票
private static int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "正在出售第" + ticket + "张票");
ticket--;
} else {
System.out.println(Thread.currentThread().getName()+"票卖完了");
break;
}
}
}
}
② 测试类
public class Test {
public static void main(String[] args) {
SellTicketThread st1 = new SellTicketThread();
SellTicketThread st2 = new SellTicketThread();
SellTicketThread st3 = new SellTicketThread();
SellTicketThread st4 = new SellTicketThread();
st1.setName("窗口A");
st2.setName("窗口B");
st3.setName("窗口C");
st4.setName("窗口D");
st1.start();
st2.start();
st3.start();
st4.start();
}
}
③ 运行结果
2.2 实现Runable接口
通过实现
Runnable
接口的类,并将该类的实例作为参数传递给Thread
对象完成线程的创建。
class MyRunnable implements Runnable {
public void run() {
System.out.println("Thread is running");
}
}
//将该线程类的实例作为参数传递给`Thread`对象完成线程的创建
Thread t = new Thread(new MyRunnable());
t.start();
案例
四个窗口共卖100张票
根据运行结果可以分析出问题:超卖和重卖现象。
这是因为多个线程共享一个资源,导致了线程安全隐患问题。后期解决线程安全问题。 后期我们可以使用锁解决。
① 线程类
public class SellTicketRunnable 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);
}
}
}
② 测试类
public class Test {
public static void main(String[] args) {
SellTicketRunnable sellTicketRunnable = new SellTicketRunnable();
Thread thread1 = new Thread(sellTicketRunnable);
Thread thread2 = new Thread(sellTicketRunnable);
Thread thread3 = new Thread(sellTicketRunnable);
Thread thread4 = new Thread(sellTicketRunnable);
thread1.setName("窗口A");
thread1.start();
thread2.setName("窗口B");
thread2.start();
thread3.setName("窗口C");
thread3.start();
thread4.setName("窗口D");
thread4.start();
}
}
③ 运行结果
2.3 实现Callable接口并使用FutureTask
实现
Callable
接口并通过FutureTask
包装,最后传递给Thread
对象。
class MyCallable implements Callable<String> {
public String call() {
return "Callable result";
}
}
//将该类的线程的实例对象包装到FutureTask类中,然后传递给Thread,完成线程的创建
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
Thread t = new Thread(futureTask);
t.start();
案例
求1-1000内3的倍数的整数和。
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
//求1-1000以内3的倍数的整数和
int sum = 0;
for (int i = 1; i <= 1000; i++) {
if (i % 3 == 0) {
sum += i;
}
}
return sum;
}
}
public class TestCallable {
public static void main(String[] args) throws Exception{
//创建本线程类
MyCallable mc = new MyCallable();
//封装到FutureTask
FutureTask ft = new FutureTask<>(mc);
//传递给thread
Thread thread = new Thread(ft);
//开启线程
thread.start();
//获取执行结果
Object o = ft.get();
System.out.println(o);
}
}
3. Thread类中常用的方法
start()
:启动线程并使其进入就绪状态。run()
:线程运行时要执行的代码,通常不直接调用,而是通过start()
间接调用。sleep(long millis)
:使线程休眠指定的毫秒数。join()
:等待该线程终止。interrupt()
:中断线程。isAlive()
:判断线程是否处于活动状态。setPriority(int newPriority)
:设置线程的优先级。getName()
和setName(String name)
:获取和设置线程的名称。
4. Runnable和Callable的区别
4.1 接口
Runnable
:是一个函数式接口,只包含一个run()
方法,不返回任何结果,也不抛出检查型异常。Callable
:也是一个函数式接口,包含一个call()
方法,可以返回结果并且可以抛出检查型异常。
4.2 方法签名
Runnable
的run()
方法没有返回值。Callable
的call()
方法返回一个泛型类型的结果。
4.3 使用场景
Runnable
适用于不需要返回结果或不抛出检查型异常的任务。Callable
适用于需要返回结果或可能抛出检查型异常的任务。