Bootstrap

深入理解线程与进程:从基础概念到实际应用

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 方法签名

  • Runnablerun()方法没有返回值。
  • Callablecall()方法返回一个泛型类型的结果。

4.3 使用场景

  • Runnable适用于不需要返回结果或不抛出检查型异常的任务。
  • Callable适用于需要返回结果或可能抛出检查型异常的任务。
;