并发编程(一)之创建线程和线程中的常用方法

    技术2024-07-24  22

    章节目录

    1. 进程线程1.1 进程1.2 线程 2. Java中的线程2.1 创建并运行线程2.1.1 Thread2.1.2 Runnable2.1.3 FutureTask2.1.4 Thread与Runnable 2.2 栈与栈帧2.3 上下文切换 3. 常用的方法3.1 start()与run()3.2 sleep()3.3 join()3.4 interrupt()3.4.1 打断阻塞的线程3.4.2 打断运行的线程3.4.3 应用

    1. 进程线程

    1.1 进程

            程序由指令和数据组成。但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备,而进程就是用来加载指令、管理内存、管理IO的

    程序和进程         当一个程序被运行,从磁盘加载这个程序的代码至内存。这时,就开启了一个进程。进程(动态)可以视为程序(静态)的一个实例。大部分程序可以同时运行多个实例进程。如:记事本、浏览器;也有的程序只能启动一个实例进程。如:任务管理器

    1.2 线程

            一个进程可以分为多个线程,而一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行。

            在Java中,线程作为最小调度单位,进程作为资源分配的最小单位。

    2. Java中的线程

    在Java程序启动时,都会创建一个线程----Main线程。

    2.1 创建并运行线程

    如果你想在Main线程之外创建其他线程,可以用如下方法。

    ThreadRunnable接口FutureTask

    2.1.1 Thread

    // 创建线程 (使用了匿名内部类) Thread t1 = new Thread() { public void run() { // 要执行的任务 } } // 启动线程 t1.start()

    2.1.2 Runnable

    Runnable r = new Runnable() { public void run() { // 要执行的任务 } } // 创建线程对象 Thread t1 = new Thread(r, "t1"); // 启动线程 t1.start();

    Java8 以后可以使用 Lambda 表达式来精简代码。如:

    Runnable r = () -> { // 要执行的任务 } // 创建线程对象 Thread t1 = new Thread(r, "t1"); // 启动线程 t1.start();

    如果对 Lambda 表达式不了解的,可以看看这篇博客 手把手地带你走进Lambda表达式之门

    2.1.3 FutureTask

    FutureTask 能够接收 Callable 接口的参数,用来处理有返回结果的情况。

    FutureTask<Integer> task = new FutureTask<>(() -> { // 要执行的任务 return 666; }) new Thread(task, "t1").start(); // 主线程阻塞,同步等待 task 执行完毕的结果 Integer result = task.get();

    2.1.4 Thread与Runnable

    查看 Thread 源码:

    Thread 实现了 Runnable 接口Thread 有一个 Runnable 类型的成员变量 target

    查看 Thread 的构造方法:

    public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); }

    最终会调用:

    private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { ... this.target = target; ... }

    将构造方法中的参数传递给了成员变量 target。

    查看 Thread 中的 run() 方法:

    public void run() { if (target != null) { target.run(); } }

    如果 target != null,则运行 Runnable 接口中的 run() 方法;否则,直接运行Thread 中的 run() 方法。

    方法1 中的原理:         通过 匿名内部类实现的方式可以看做是 Thread 的子类(亦或继承 Thread),子类重写了父类中的 run() 方法。所以,最终运行的是子类中的方法

    方法2 中的原理:         把 Runnable 类型的变量作为 Thread 的构造方法的参数,在此构造方法中,将此参数传递给成员变量 target,所以 target != null,最终会执行 Runnable 接口中的方法,而上述的 Lambda 表达式将 Runnable 接口中的 run() 方法进行重写,所以,最终会执行 Lambda表达式。

    2.2 栈与栈帧

            JVM是由堆、栈、方法区等组成,其中,栈内存是给线程用的,每个线程启动后,JVM会为它分配一个栈内存。而每个栈是由多个栈帧组成,对应着每次方法调用时所占用的内存

    2.3 上下文切换

    因为以下原因导致CPU不再执行当前的线程,转而执行另一个线程的代码:

    线程的CPU时间片用完垃圾回收有更优先级的线程需要执行线程自己调用了sleep()、wait()、lock() 方法等

    3. 常用的方法

    方法名功能描述注意start启动一个新的线程,在新的线程中运行 run() 方法start() 方法只是让线程进入就绪状态,里面的代码不一定就马上执行(CPU时间片还没分给它)。每个线程对象的 start() 方法只能调用一次,如果调用多次,会抛出异常IllegalThreadStateExceptionrun新的线程启动后,会执行的代码join等待线程运行结束isInterrupted判断是否被打断不会清除打断标记interrupt打断线程如果被打断的线程正在sleep、wait、join 会导致被打断的线程抛出异常 InterruptedException,并清除标记;如果打断正在运行的线程,则会设置打断标记;park 线程被打断,也会设置打断标记interrupted判断当前线程是否被打断会清除打断标记sleep(n)让当前执行的线程休眠 n 毫秒,休眠时间让出 CPUyield提示线程调度器让出当前线程对 CPU 的使用

    3.1 start()与run()

    start() 方法表示启动一个新的线程,run() 方法表示线程启动后要执行的代码,那能否能直接调用 run() 方法?

    public static void main(String[] args) { Thread t1 = new Thread() { @Override public void run () { System.out.println("hello world" + "=====" +Thread.currentThread().getName()); } }; t1.run(); }

    run() 方法还是可以执行,但并没有创建新的线程,还是在 main 线程中执行的

    3.2 sleep()

    调用 sleep() 方法会让当前线程从 Running 进入 Timed Waiting 状态其它线程可以使用 interrupt() 方法打断正在睡眠的线程,这时,sleep() 方法会抛出 InterruptedException异常 public static void main(String[] args) throws Exception{ Thread t1 = new Thread() { @Override public void run () { try { Thread.sleep(2000); } catch (InterruptedException e) { System.out.println("wake up"); } } }; t1.start(); Thread.sleep(1000); t1.interrupt(); }

    3.3 join()

    看下这段代码,打印 i 是什么?

    static int i = 1; private static void test() throws Exception{ Thread t1 = new Thread(() -> { try { Thread.sleep(1000); i = 100; } catch (InterruptedException e) { e.printStackTrace(); } }); t1.start(); System.out.println("i = " + i); } public static void main(String[] args) throws Exception{ test(); }

             流程分析:main 线程启动,调用 test() 方法,开启一个新的线程 t1,main 和 t1 线程都是在同时运行的,main 线程会先执行语句 “System.out.println("i = " + i);”,只有当 t1 线程休眠1s后,才会将 i 赋值为 100。

    那么,如何让打印的 i 为100呢? 只需要添加一个 join() 方法

    t1.start(); t1.join(); System.out.println("i = " + i);

    当 main 线程 执行到语句 “t1.join()”时,main 线程会一直等待着 t1 线程(t1 调用了 join()方法),直到 t1 线程运行结束后,main 线程才会继续执行。

    3.4 interrupt()

    3.4.1 打断阻塞的线程

    打断 sleep、wait、join 的线程会清空打断标记。以 sleep 为例

    public static void main(String[] args) throws Exception{ Thread t1 = new Thread(() -> { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } }); t1.start(); Thread.sleep(100); t1.interrupt(); System.out.println("打断标记:" + t1.isInterrupted()); }

    3.4.2 打断运行的线程

    打断运行的线程是不会清除标记的

    3.4.3 应用

    两阶段终止模式 在线程 t1 中如何优雅地终止线程 t2?

            先看看两阶段终止模式的一个应用场景----做一个系统的健康监控,如:定时地去监控CPU 的使用率、内存的使用率等。可以使用一个后台的监控线程每隔2s不断地进行记录即可,当点击停止按钮时便不再监控了。

    代码实现:

    public class TwoPhraseTermination { // 监控线程 private Thread monitor; // 启动监控线程 public void start() { monitor = new Thread(() -> { while (true) { Thread current = Thread.currentThread(); if (current.isInterrupted()) { System.out.println("料理后事"); break; } try { Thread.sleep(2000); System.out.println("执行监控记录"); } catch (InterruptedException e) { // 重新设置打断标记 current.interrupt(); } } }); monitor.start(); } public void stop() { monitor.interrupt(); } public static void main(String[] args) throws Exception{ TwoPhraseTermination termination = new TwoPhraseTermination(); termination.start(); Thread.sleep(5000); termination.stop(); } }

    这就是实现了在一个 main 线程中优雅地终止了另一个线程。

    Processed: 0.021, SQL: 9