【多线程】(一)多线程概念


一、进程和线程

1.1 进程和线程的概念

进程的概念:

简而言之,进程就是运行起来的程序。它被视为一个独立的执行环境,包括程序代码、数据和上下文等。每个进程都有自己的地址空间,可以独立的执行,并且与其他进程隔离开来。如果要想与其他进程之间传输数据信息,那么就要通过进程间通信(IPC)来进行数据的交换和协作。

线程的概念
线程可以看做是进程中的一个执行单元,也就是说一个线程就是一个执行流。一个进程可以包含多个线程,每个线程都有自己的执行路径和执行状态,并且共享进程的地址空间以及资源。它们可以直接访问进程的数据,包括全局变量和共享内存区域。因此线程之间的通信和数据共享相对于进程来说更加的容易和高效。另外,与创建进程相比,线程的创建和切换开销更小。

1.2 进程和线程的区别

进程和线程之间的主要区别可以简单总结如下:

  1. 资源关系:进程拥有独立的内存空间和系统资源,而线程共享进程的内存和资源。

  2. 执行关系:每个进程都是独立执行的实体,有自己的程序计数器和堆栈空间,而线程在进程内部执行,共享相同的地址空间和上下文。

  3. 调度关系:进程是操作系统进行资源分配基本单位,而线程是调度的基本单位。线程的切换开销较小,因为它们共享进程的上下文。

  4. 通信与同步:进程间通信(IPC)复杂且开销较大,而线程之间通信和同步相对容易,可以直接访问共享数据和使用线程同步机制。

总体而言,进程是独立的执行实体,拥有独立的资源和执行环境,而线程是进程内部的执行单元,共享进程的资源和上下文。进程间通信复杂且开销较大,线程间通信和同步相对容易且开销较小。选择使用进程或线程取决于任务的特点、性能需求和并发需求等因素。

二、多线程

多线程是指在同一个程序中创建和执行多个线程的编程技术。每个线程都是独立的执行路径,可以同时执行不同的任务或函数。

使用多线程可以提高程序的性能和资源利用率,特别是在多核处理器系统中。通过将任务分配给不同的线程,可以并发执行这些任务,从而加速程序的执行速度。多线程编程可以实现任务的并发执行,使得程序能够同时处理多个任务或响应多个事件。

三、Java中的线程

线程是操作系统中的概念,操作系统内核实现了线程机制,并且向用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库)。Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装。

3.1 Thread类

在 Java 中,Thread 类是用于创建和管理线程的核心类。它是 Java 提供的一个抽象类,用于表示线程的概念,并提供了一些列方法来操作线程的行为和状态。

以下是Java中Thread类的常用方法:

方法描述
Thread()创建一个新的线程对象
Thread(Runnable target)使用给定的Runnable对象作为线程的目标
Thread(String name)使用给定的名称创建一个新的线程对象
start()启动线程并执行run()方法中的任务逻辑
run()定义线程要执行的任务逻辑,需要在子类中重写该方法
sleep(long millis)使线程休眠指定的毫秒数
join()等待线程执行完成
getState()获取线程的状态
isAlive()判断线程是否还活着(正在执行或准备执行)
setPriority(int priority)设置线程的优先级
getPriority()获取线程的优先级
setName(String name)设置线程的名称
getName()获取线程的名称
currentThread()获取当前正在执行的线程对象
yield()建议线程放弃当前的CPU资源,使得其他线程有机会执行
isDaemon()判断线程是否为守护线程
setDaemon(boolean on)设置线程是否为守护线程
interrupt()中断线程的执行
isInterrupted()判断线程是否被中断
interrupted()静态方法,判断当前线程是否被中断,并清除中断状态

3.2 创建线程的方式

1. 继承Thread抽象类,重写 run 方法

class MyThread extends Thread{
    @Override
    public void run() {
    	System.out.println("hello, thread!");
    }
}

public class ThreadDemo1 {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start(); // 主线程创建一个子线程,有子线程执行run方法
        // t.run(); // 主线程执行run方法
        
        System.out.println("hello, main!"); 
    }
}

2. 实现 Runnable 接口,重写 run 方法

class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("hello runnable");
    }
}

public class ThreadDemo2 {
    public static void main(String[] args) {
        // Runnable 只是描述了这个任务
        Runnable r = new MyRunnable();
        // 把这个任务交给线程
        Thread t = new Thread(r);
        
        t.start();
    }
}

3. 使用匿名内部类创建线程

public class ThreadDemo3 {
    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                System.out.println("hello, thread!");
            }
        };
        t.start();
    }
}

4. 匿名内部类实现 Runnable 接口,实现 run 方法

public class ThreadDemo4 {
    public static void main(String[] args) {
      
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello, thread1!");
            }
        });
        t.start();
    }
}

5. 使用 Lambda 表达式实现 run 方法

public class ThreadDemo5 {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            System.out.println("hello, thread2!");
        });
        t.start();
    }
}

四、线程的基本操作

4.1 启动线程

在前面创建线程的例子中,已经看到了创建一个线程对象都要重写 run 方法,但是线程对象被创建出来并不意味着线程就开始运行了,而是还需要调用 start 方法。因此可以理解为:

  • 重写 run 方法就是描述该线程要做的事情;
  • 创建线程对象表示线程已经准备就绪了;
  • 调用 start 方法就是告诉准备好的线程可以开始执行任务了。

所有,调用 start 方法,才是真正的在操作系统底层创建了一个线程。

4.2 中断线程

中断现场就是要当前正在运行的线程停止工作,而通知运行中的线程停止工作主要有下面两种方法:

  1. 自定义一个共享变量来通知线程停止工作;
  2. 另一个线程调用 interrupt()方法来通知线程停止工作。

示例一:自定义一个共享变量来通知线程停止工作。

public class ThreadDemo {
    // 中断一个线程,方式一:自定义一个标志位
    private static boolean flag = true;

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (flag) {
                System.out.println("hello, world!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t.start();

        // 在主线程中通过改变flag的值来结束子线程
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = false;
    }
}

首先设置 flag 的初始值为 true,然后创建并执行 t 线程。运行 3 秒后,主线程将 flag 的值修改为 false,此时 t 线程结束执行 run方法,线程也就终止了。

示例二: 使用 t.interrupt()Thread.currentThread().isInterrupted() 代替自定义标志位。

public class ThreadDemo {
    // 使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位.
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (!Thread.currentThread().isInterrupted()){ // Thread.currentThread()相当于this
                    System.out.println("hello, world!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        break;
                    }
                }
            }
        });

        t.start();
        Thread.sleep(3000);

        // sleep 的时候,唤醒线程,但是设置的标志位还原
        t.interrupt();// 只是通知终止t线程
    }
}

上述代码的含义是在子线程中调用 Thread.currentThread().isInterrupted()方法来判断当前线程有没有收到终止信号。在主线程中,执行 3 秒后调用t.interrupt()方法来通知 t 线程,但是其运行的结果如下:

可以发现,当子线程收到中断信号之后抛出了 InterruptedException异常后仍然在继续执行,这是因为 interrupt() 只是向线程 t 发出中断信号,并不直接终止线程。在线程 t 的 run 方法中,需要根据实际需求来检查中断状态并处理中断操作。例如,在异常的处理中添加一句 break:

try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    e.printStackTrace();
    break;
}


此时,子线程 t 就能在收到中断异常的时候退出执行了。

关于 Thead 内部的中断标记

Thread 类的内部包含一个 boolean 类型的变量作为线程是否被中断的标记,关于操作这个标记的方法有:

方法说明
public void interrupt()中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位
public static boolean interrupted()判断当前线程的中断标志位是否设置,调用后清除标志位
public boolean isInterrupted()判断对象关联的线程的标志位是否设置,调用后不清除标志位

示例三:使用静态方法 interrupted 方式通知线程中止

public class ThreadDemo {

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (!Thread.interrupted()){
                    System.out.println("hello, world!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        t.start();

        Thread.sleep(3000);

        t.interrupt();
    }
}

运行程序:发现此时的效果和我们自定义的 flag 标志时的效果一样。

线程 t 收到通知的方式有两种:

  1. 如果线程因为调用 ``wait/join/sleep等方法而阻塞挂起,则以InterruptedException` 异常的形式通知,并清除中断标志

    • 当出现 InterruptedException 的时候,要不要结束线程取决于catch 中代码的写法。可以选择忽略这个异常,也可以break跳出循环结束线程。
  2. 否则,只是内部的一个中断标志被设置,线程 t 可以通过

  • Thread.interrupted() 判断当前线程的中断标志是否被设置,然后清除中断标志。
  • Thread.currentThread().isInterrupted() 判断指定线程的中断标志是否被设置,不清除中断标志。因此这种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到

示例四:观察标志位是否被清除

标志位是否清除,就类似于一个开关:
* Thread.interrupted() 相当于按下开关,开关自动弹起来了,这个称为 “清除标志位”;
* 而Thread.currentThread().isInterrupted() 相当于按下开关之后,开关弹不起来,这个称为 “不清除标志位”。

  1. 使用 Thread.interrupted(),线程中断会清除标志位
public class ThreadDemo {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; ++i) {
                    System.out.println(Thread.interrupted());
                }
            }
        });

        t.start();
        t.interrupt();
    }
}

运行结果:

通过观察,看到只有一开始是打印的是 true,后边都是 false,这因为标志位被清除。

  1. 使用 Thread.currentThread().isInterrupted(),线程中断标记位不会清除
public class ThreadDemo {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; ++i) {
                    System.out.println(Thread.currentThread().isInterrupted());
                }
            }
        });

        t.start();
        t.interrupt();
    }
}    

运行结果:

全是打印的 true,说明标志没有被清除。

4.3 等待线程

有时,需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。

public class ThreadDemo {

    public static void main(String[] args) throws InterruptedException {
        Runnable target = () -> {
            for (int i = 0; i < 3; i++) {
                try {
                    System.out.println(Thread.currentThread().getName()
                            + ": 我还在工作!");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ": 我结束了!");
        };
        Thread thread1 = new Thread(target, "李四");
        Thread thread2 = new Thread(target, "王五");
        System.out.println("先让李四开始工作");
        thread1.start();
        thread1.join();
        System.out.println("李四工作结束了,让王五开始工作");
        thread2.start();
        thread2.join();
        System.out.println("王五工作结束了");
    }
}

运行这段代码的结果为:

即主线程先等待 “李四” 线程结束之后才启动的 “王五” 线程,最后当 “王五” 线程结束之后,主线程也才退出。此时如果将 join()方法注释掉,其运行的结果如下:

此时两个线程就在同时进行了,并且它们的执行顺序也变得杂乱无章了。因此join()方法的功能就是等待线程,即调用这个方法的线程(主线程)会一直阻塞,直到等待的这个子线程退出了才继续执行。

关于 Join 方法:

方法说明
public void join()等待线程结束
public void join(long millis)等待线程结束,最多等 millis 毫秒
public void join(long millis, int nanos)同理,但可以等待更高精度

4.4 休眠线程

休眠当前线程使用的方法是sleep静态方法:

方法说明
public static void sleep(long millis) throws InterruptedException休眠当前线程 millis 毫秒
public static void sleep(long millis, int nanos) throws InterruptedException可以更高精度的休眠

五、线程状态

5.1 线程的所有状态

线程的状态是一个枚举类型 Thread.State,可以通过下面的代码查看所有的状态:

public class ThreadDemo {
    public static void main(String[] args) {
        for (Thread.State state : Thread.State.values()) {
            System.out.println(state);
        }
    }
}

运行结果:

说明:

  1. 新建(New):线程被创建但尚未启动。

  2. 可运行(Runnable):线程可以在JVM中执行,但可能正在等待系统资源(如处理器时间片)或其他条件(如同步锁)。

  3. 运行(Running):线程正在执行其任务逻辑。

  4. 阻塞(Blocked):线程被阻塞,暂时停止执行。可能由于等待某个条件的发生(如等待锁),或者被其他线程中断。

  5. 等待(Waiting):线程等待某个特定条件的发生,一旦条件满足,可以被其他线程唤醒。

  6. 超时等待(Timed Waiting):线程等待一段指定的时间,一旦时间到达或特定条件满足,可以被唤醒。

  7. 终止(Terminated):线程完成了其任务或异常终止,不再运行。

5.2 线程状态转移及其意义

线程在执行过程中可以经历不同的状态,线程状态的转移反映了线程在不同阶段的行为和状态变化。理解线程状态转移及其意义可以帮助我们更好地管理和调试多线程程序。

线程状态转移示意图:

线程状态之间的转移如下:

  • 新建(New)-> 可运行(Runnable):调用线程的 start() 方法启动线程。

  • 可运行(Runnable)-> 运行(Running):线程调度器选择该线程,并执行其 run() 方法。

  • 运行(Running)-> 阻塞(Blocked):线程可能会因为等待获取某个锁或其他资源而被阻塞。

  • 阻塞(Blocked)-> 可运行(Runnable):线程获取到所需的资源,可以被调度器重新选择并继续执行。

  • 运行(Running)-> 等待(Waiting):线程调用了 wait() 方法等待某个条件满足。

  • 等待(Waiting)-> 可运行(Runnable):其他线程调用了相应对象的 notify()notifyAll() 方法,唤醒等待的线程。

  • 运行(Running)-> 超时等待(Timed Waiting):线程调用了 sleep()join() 方法,或者等待某个条件满足一段指定的时间。

  • 超时等待(Timed Waiting)-> 可运行(Runnable):等待时间到达或某个特定条件满足。

  • 可运行(Runnable)-> 终止(Terminated):线程完成了其任务,或者因为异常而终止。

5.3 观察线程状态转移

下面是一个简单的示例代码,展示了如何观察线程的状态转移:

public class ThreadStateExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            try {
                // 线程开始执行
                System.out.println("Thread is running");
                Thread.sleep(2000);
                // 线程等待一段时间
                System.out.println("Thread is waiting");
                synchronized (ThreadStateExample.class) {
                    // 获取同步锁,进入阻塞状态
                    ThreadStateExample.class.wait();
                }
                // 被唤醒后继续执行
                System.out.println("Thread is running again");
                Thread.sleep(2000);
                // 线程执行完成
                System.out.println("Thread is terminated");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        System.out.println("Thread state: " + thread.getState()); // 输出线程状态:New

        thread.start(); // 启动线程

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Thread state: " + thread.getState()); // 输出线程状态:Runnable

        try {
            Thread.sleep(2500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Thread state: " + thread.getState()); // 输出线程状态:Timed Waiting

        synchronized (ThreadStateExample.class) {
            ThreadStateExample.class.notify(); // 唤醒线程
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Thread state: " + thread.getState()); // 输出线程状态:Runnable

        try {
            thread.join(); // 等待线程执行完成
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Thread state: " + thread.getState()); // 输出线程状态:Terminated
    }
}

在这个示例中,创建了一个新的线程,并在其 run() 方法中模拟了不同的状态转移。通过输出线程状态,我们可以观察线程在不同阶段的状态变化。

运行结果:

  • 6
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

求知.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值