(二)Java 线程

一、创建和运行线程

程序在启动时,默认就已经存在了一个主线程,如果想要在主线程之外创建线程,有以下几种方式:

1.1 直接使用 Thread

// 创建线程对象
Thread t = new Thread(){
    @Override
    public void run() {
        // 要执行的任务
    }
};
// 启动线程
t.start();

例如:

@Slf4j(topic = "c.test")
class test {
    public static void main(String[] args) {
        // 创建线程对象
        Thread t = new Thread(){
            @Override
            public void run() {
                // 要执行的任务
                log.debug("running");
            }
        };
        // 启动线程
        t.setName("t");
        t.start();
        log.debug("running");
    }
}

输出:
image.png

1.2 使用 Runnable 配合 Thread

把【线程】和【任务】(要执行的代码)分开

  • Thread 代表线程
  • Runnable 可运行的任务(线程要执行的代码)
Runnable r = new Runnable() {
    @Override
    public void run() {
        // 要执行的任务
    }
};
// 创建线程对象
Thread t = new Thread(r);
// 启动线程
t.start();

例如:

@Slf4j(topic = "c.CreateThreadMethod")
public class CreateThreadMethod {
    public static void main(String[] args) {
        Runnable r = new Runnable() {
            @Override
            public void run() {
                // 要执行的任务
                log.debug("hello");
            }
        };
        // 创建线程对象
        Thread t = new Thread(r);
        t.setName("t");
        // 启动线程
        t.start();
        log.debug("hello");
    }
}

输出:
image.png

分析 Thread 源码,理清 Thread 与 Runnable 之间的关系

public class Thread implements Runnable {
	private Runnable target; // 成员变量
    
    // 构造函数
	public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */

            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }

            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();

        /*
         * Do we have the required permissions?
         */
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        
        this.target = target; // 此处将传递过来的 Runnable 对象赋值给成员变量 target
        
        setPriority(priority);
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

    /**
     * If this thread was constructed using a separate
     * <code>Runnable</code> run object, then that
     * <code>Runnable</code> object's <code>run</code> method is called;
     * otherwise, this method does nothing and returns.
     * <p>
     * Subclasses of <code>Thread</code> should override this method.
     *
     * @see     #start()
     * @see     #stop()
     * @see     #Thread(ThreadGroup, Runnable, String)
     */
    @Override
    public void run() {
        if (target != null) {
            target.run(); // 当 target 不为 null 时,调用 target 中的 run 方法。
        }
    }
}

继承 Thread 类的方式重写了 run 方法,那么 start 方法最终调用的就是子类中的 run 方法。
实现 Runnable 接口中的 run 方法的方式,通过源码分析发现,最终在调用 run 方法时,调用的也是 Runnable 接口的 run 方法。
小结:

  • 方法 1 是把线程和任务合并在了一起,方法 2 是把线程和任务分开了
  • 用 Runnable 更容易与线程池等高级 API 配合
  • 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活

1.3 FutureTask 配合 Thread

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

@Slf4j(topic = "c.FutureTaskTest")
class FutureTaskTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        /*FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
				log.debug("hello");
                return 100;
            }
        });*/
        // 上述代码可使用 lambda 表达式进行简化,如下
        FutureTask<Integer> task = new FutureTask<>(() -> {
            log.debug("hello");
            return 100;
        });
        Thread t = new Thread(task);
        t.start(); // 启动线程,执行线程方法。这里最终调用的是 FutureTask 中的 run 方法
        Integer integer = task.get(); // 通过 Future 类中的 get 方法等待获取 task 的执行结果
        // 这里的等待意思是指只有 task 返回了结果,等待才会结束。即 task 会一直阻塞在这,直到拿到返回结果。
        log.debug("结果是:{}", integer);
    }
}

输出:
image.png
Runnable 接口本身是无法获取线程的处理结果的,FutureTask 类实现了 RunnableFuture
� 接口,而 RunnableFuture 接口继承了 Runnable 和 Future 这两个类,而 get 方法就来自于 Future 类。
所以 new Thread(task); 这段代码,执行时依然调用的是 Thread 类中的下面的构造函数:

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

那么 start 方法最终调用的 run 方法也就是 FutureTask 中的 run 方法。

public class FutureTask<V> implements RunnableFuture<V> {
	private Callable<V> callable;
    /** The result to return or exception to throw from get() */
    private Object outcome; 

    // 构造函数
	public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }
    
    public void run() {
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call(); // 调用 call 方法
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

    protected void set(V v) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = v;
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            finishCompletion();
        }
    }

     public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        return report(s);
    }

    private V report(int s) throws ExecutionException {
        Object x = outcome;
        if (s == NORMAL)
            return (V)x;
        if (s >= CANCELLED)
            throw new CancellationException();
        throw new ExecutionException((Throwable)x);
    }
}

FutureTask 类在 run 方法中调用了 Callable 接口的 call 方法,并将结果赋值给成员变量 outcome。
当我们调用 get() 方法时,最终获取到 outcome 的结果。

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

Callable 接口中只有一个 call() 方法,同时该接口还标注有 @FunctionalInterface 接口,表明该接口可以通过 lambda 表达式进行简化书写 。

1.4 查看进程和线程

Windows

  • 任务管理器可以查看进程和线程数,也可以用来杀死进程。
  • tasklist 查看进程
  • taskkill 杀死进程

image.png
Linux

  • ps -fe 查看所有进程
  • ps -fT -p 查看某个进程(PID)的所有线程
  • kill 杀死进程
  • top 按大写 H 切换是否显示线程
  • top -H -p 查看某个进程(PID)的所有线程

Java

  • jps 命令查看所有 Java 进程
  • jstack 查看某个 Java 进程(PID)的所有线程状态
  • jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

二、线程运行原理

2.1 栈与栈帧

Java Virtual Machine Stacks (Java虚拟机栈)
JVM由堆、栈、方法区所组成,其中栈内存就是给线程用的,每个线程启动后,虚拟机就会为其分配一块栈内存。

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

线程运行原理-栈帧图解

public class TestFrames {
    public static void main(String[] args) {
        method1(10);
    }

    private static void method1(int x) {
        int y = x + 1;
        Object m = method2();
        System.out.println(m);
    }

    private static Object method2() {
        Object n = new Object();
        return n;
    }
}

分析:
main 方法进栈,虚拟机会产生一个 String 的数组对象,然后将其引用地址赋值给 args。main 方法的返回地址即为退出地址。
image.png
程序计数器变更下调指令地址,即 method1 方法的指令地址。
image.png
method1 方法进栈,在创建栈帧时,将会进行内存分配,局部变量表的创建、返回地址的记录。
image.png
开始执行 method1 方法,包括 int y = x + 1;
image.png
程序计数器变更下一条指令地址,即 Object m = method2();
image.png method2() 方法进栈,在创建栈帧时,将会进行内存分配,局部变量表的创建、返回地址的记录。
image.png
变更程序计数器中的下一条指令的地址。
image.png
开始执行代码 Obejct n = new Object();,将堆中的 Object 对象地址赋值给变量 n
image.png
变更程序计数器中的下一条指令的地址,return n。
image.png
执行 return n;,将 n 的值赋值给 m,释放掉 method2 栈帧
image.png
变更程序计数器中的下一条指令的地址,System.out.println(m);。 image.png
打印语句执行结束,method1 栈帧释放掉,根据返回地址找到主方法中需要继续执行的位置。
image.png
由于主方法中没有代码可继续执行,main 栈帧也将会出栈,程序执行结束。

线程运行原理-多线程

public class TestFrames {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
           method1(20);
        });
        t1.setName("t1");
        t1.start();
        method1(10);
    }

    private static void method1(int x) {
        int y = x + 1;
        Object m = method2();
        System.out.println(m);
    }

    private static Object method2() {
        Object n = new Object();
        return n;
    }
}

设置程序断点如下:
image.png
断点模式选择 Thread(右击断点选择)
image.png
以 debug 模式运行程序
image.png
两个线程相互独立,各自拥有自己的栈内存。

2.2 线程上下文切换(Thread Context Switch)

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

  • 线程的 CPU 时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 JVM 指令的执行地址,是线程私有的。

  • 状态包括程序计数器、虚拟机栈中的每个栈帧的信息,如局部变量、操作数栈、返回地址等
  • Context Switch 频繁发生回影响性能

三、常见方法

方法名static功能说明注意
start()启动一个新线程,在新的线程运行run 方法中的代码start方法只是让线程进入就绪,里面的代码不一定立刻运行(CPU的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现 java.lang.IllegalThreadStateException
run()新线程启动后会调用的方法如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为
join()等待线程运行结束
join(long n)等待线程运行结束,最多等待 n 毫秒
getId()获取线程长整型的 idID 唯一
getName()获取线程名
setName(String)修改线程名
getPriority()获取线程优先级
setPriority(int)修改线程优先级Java中规定线程优先级是 1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率
getState()获取线程状态Java 中线程状态是用 6 个 enum
表示,分别为:NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED
isInterrupted()判断是否被打断不会清楚打断标记
isAlive()线程是否存活(还没有运行完毕)
interrupt()打断线程如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记;如果打断的正在运行的线程,则会设置打断标记;park 的线程被打断,也会设置打断标记
interrupted()static判断当前线程是否被打断会清除打断标记
currentThread()static获取当前正在执行的线程
sleep(long n)static让当前执行的线程休眠 n 毫秒,休眠时让出 CPU 的时间片给其他线程
yield()static提示线程调度器让出当前线程对 CPU 的使用权主要是为了测试和调试

3.1 start与run

@Slf4j(topic = "c.ThreadMethodTest")
public class ThreadMethodTest {
    public static void main(String[] args) {
         Thread t1 = new Thread(() -> {
             log.debug("running...");
         });
         t1.run();
         log.debug("do other thing...");
    }
}

输出:
image.png

  • 直接调用 run 是在主线程中执行了 run,没有启动新的线程
  • 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码

start() 方法调用前后线程的状态:

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test5")
public class Test5 {
    public static void main(String[] args) {
        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                log.debug("running...");
            }
        };
        System.out.println(t1.getState());
        t1.start();
        System.out.println(t1.getState());
    }
}

运行结果:
image.png
start() 方法无法被多次调用,否则会报 IllegalThreadStateException 异常。线程只要变成了 RUNNABLE 状态就不能再 start 了。

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test5")
public class Test5 {
    public static void main(String[] args) {
        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                log.debug("running...");
            }
        };
        System.out.println(t1.getState());
        t1.start();
        t1.start();
        System.out.println(t1.getState());
    }
}

运行结果:
image.png

3.2 sleep 与 yield

sleep

1.调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)

@Slf4j(topic = "c.ThreadMethodTest")
public class ThreadMethodTest {
    public static void main(String[] args) {
         Thread t1 = new Thread(() -> {
             try {
                 Thread.sleep(2000);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }, "t1");
         t1.start(); // 启动 t1 线程
         log.debug("t1 state {}", t1.getState()); // 此时 t1 线程尚未休眠
        try {
            Thread.sleep(500); // 主线程休眠 500ms
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 主线程休眠结束后,此时 t1 线程也进入休眠
        log.debug("t1 state {}", t1.getState());
    }
}

image.png

2.其他线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test7")
public class Test7 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                log.debug("enter sleep...");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) { // 说明线程 sleep 的过程中可能会被打断
                    log.debug("wake up...");
                    e.printStackTrace();
                }
            }
        };
        t1.start();
        Thread.sleep(1000); // 主线程睡眠 1 秒,等待 t1 线程睡眠
        log.debug("interrupt...");
        t1.interrupt();
    }
}

运行结果:
image.png
3.睡眠结束后的线程未必会立刻得到执行

  • 睡眠结束后的线程未必会立刻得到执行,这是因为此时 CPU 可能正在执行其他线程的代码,所以线程必须要等待任务调度器将时间片分配给线程后,才能继续运行。也就是说线程睡眠结束后,未必能够立刻获得 CPU 的使用权。

4.建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

  • TimeUnit 带有时间单位,具有更好的可读性。其内部依然是调用的 Thread.sleep() 方法,只不过进行了一次时间单位的换算。
  • Thread.sleep() 在哪个线程中被调用就是哪个线程睡眠
import java.util.concurrent.TimeUnit;

@Slf4j(topic = "c.Test")
public class Test8 {
    public static void main(String[] args) throws InterruptedException {
        log.debug("starting test");
        TimeUnit.SECONDS.sleep(1);
        log.debug("finished test");
//        Thread.sleep(1000);
    }
}

运行结果:
image.png

yield

1.调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其他线程

  • 调用 yield 之后,当前线程会让出 CPU 的使用权,让其他线程有机会去使用 CPU。

2.具体的实现依赖于操作系统的任务调度器

  • 虽然当前线程调用了 yield 方法,打算让出 CPU 的使用权,但是当前并 没有其他线程在运行,此时的任务调度器依然会把时间片分给当前线程。即想让没有让出去,这种情况非常常见。

Runnable(就绪)状态和 Timed Waiting(阻塞)状态的区别:
任务调度器只会将时间片分配给就绪状态的线程,是不会分配各阻塞状态的线程的,阻塞状态的线程只有在阻塞时间结束,线程醒过来以后才会分配时间片。即 sleep 中线程在结束 sleep 后,线程的状态会从阻塞变为就绪。

线程优先级

  • 线程优先级会提示调度器有限调度该线程,但它仅仅是一个提示,调度器可以忽略它
  • 如果 CPU 比较忙,那么优先级高的线程会获得更多的时间片,但 CPU 闲时,优先级几乎没作用
    | setPriority(int) | 修改线程优先级 | Java中规定线程优先级是 1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率 |
    | — | — | — |

image.png
最小优先级为 1,最大优先级为 10,默认优先级为 5。 数字越大表示优先级越高。

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test")
public class Test9 {
    public static void main(String[] args) {
        Runnable task1 = () -> {
            int count = 0;
            for (;;) {
                System.out.println("-------->1 " + count++);
            }
        };

        Runnable task2 = () -> {
            int count = 0;
            for (;;) {
//                Thread.yield();
                System.out.println("               -------->2 " + count++);
            }
        };

        Thread t1 = new Thread(task1, "t1");
        Thread t2 = new Thread(task2, "t2");

//        t1.setPriority(Thread.MIN_PRIORITY);
//        t2.setPriority(Thread.MAX_PRIORITY);

        t1.start();
        t2.start();
    }
}

普通情况下的运行结果:
image.png
线程 t1 与 t2 的执行结果相差不多
测试 yield 方法,将 Thread.yield(); 前的注释放开,执行结果如下:
image.png
测试优先级,将 Thread.yield(); 注释,将优先级相关的代码注释放开,测试结果如下:
image.png
优先级在一定场景下可能会起作用,但不可靠不可控,仅仅只是对任务调度器的一个提示而已,调度器可以忽略它。

3.3 join 方法详解

3.3.1 join 方法介绍

为什么需要 join?
下面的代码执行,打印 r 是什么?

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j(topic = "c.Test")
public class Test10 {
    static int r = 0;

    public static void main(String[] args) {
        test1();
    }

    private static void test1() {
        log.debug("开始");
        Thread t1 = new Thread(() -> {
            log.debug("开始");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            log.debug("结束");
            r = 10;
        });
        t1.start();
        log.debug("结果为:{}", r);
        log.debug("结束");
    }
}

运行结果:
image.png
分析:

  • 因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10
  • 而主线程一开始就要打印 r 的结果,所以只能打印出 r=0

解决方法:

  • 用 sleep 行不行?为什么?
    • 可以,但不推荐。因为我们并不知道线程 t1 从开始执行到结束总共花费了多长时间。
  • 用 join,加载 t1.start() 之后即可。
    • join:等待线程运行结束。哪个线程对象调用 join,就是等待该线程结束,即 t1.join(); 就是等待线程 t1 执行结束。
  • 测试 join,修改代码如下:
package com.atheima.test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j(topic = "c.Test")
public class Test10 {
    static int r = 0;

    public static void main(String[] args) {
        test1();
    }

    private static void test1() {
        log.debug("开始");
        Thread t1 = new Thread(() -> {
            log.debug("开始");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            log.debug("结束");
            r = 10;
        });
        t1.start();
        try {
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.debug("结果为:{}", r);
        log.debug("结束");
    }
}

运行结果:
image.png

3.3.2 应用之同步(案例 1)

以调用方角度来讲,如果

  • 需要等待结果返回,才能继续运行就是同步
  • 不需要等待结果范湖,就能继续运行就是异步

image.png
代码案例:

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j(topic = "c.Test")
public class TestJoin {
    static int r1 = 0;
    static int r2 = 0;

    public static void main(String[] args) throws InterruptedException {
        test2();
    }

    private static void test2() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
                r1 = 10;
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        Thread t2 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
                r2 = 20;
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        long start = System.currentTimeMillis();
        t1.start();
        t2.start();
        log.debug("join begin");
        t1.join();
        log.debug("t1 join end");
        t2.join();
        log.debug("t2 join end");
        long end = System.currentTimeMillis();
        log.debug("r1: {} r2: {} cost: {}", r1, r2, (end - start));
    }
}

运行结果:
image.png
分析:首先明确 join() 方法时等待调用该方法的线程执行结束。t1 线程开始执行时,t2 线程也开始了执行,当 t1 线程执行结束时,已经花费了一秒钟,即此时的 t2 线程也已经执行了 1 秒钟。所以从 t1.join 执行结束到 t2.join 执行结束总共只花费了 1 秒钟,而不是 2 秒钟。
那如果颠倒两个 join 呢?
最终输出的结果都是 2 秒。
image.png
可以看到 t1 和 t2 的 join 方法几乎是同时执行结束的。这是因为 t2 线程开始执行时,t1 线程也开始了执行,当 t2 线程执行结束时,已经花费了 2 秒钟,即此时的 t1 线程也已经执行结束,所以 t1.join 就无需再等待 t1 线程的执行结束。
image.png

3.3.3 join 限时同步

join(long n):等待线程运行结束,最多等待 n 毫秒。
当线程的执行时间小于设定的时间 n 时,以线程的执行时间为准。
当线程的执行时间大于设定的时间 n 时,以设定的时间 n 为准 ,即线程执行 n 毫秒后结束线程的运行。

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j(topic = "c.Test")
public class TestJoin {
    static int r1 = 0;

    public static void main(String[] args) throws InterruptedException {
        test3();
    }

    private static void test3() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
                r1 = 10;
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        long start = System.currentTimeMillis();
        t1.start();
        log.debug("join begin");
        t1.join(1500);
        long end = System.currentTimeMillis();
        log.debug("r1: {} cost: {}", r1, (end - start));
    }
}

执行结果:
image.png
1s = 1000ms
17.208 - 15.701 = 16.1208 - 15.701 = 1.507s,大约为 1.5s。
如果将 t1.join(1500) 改为 t1.join(3000),执行结果如下:
image.png
执行时间大约为 27.814 - 25.809 = 2.005s,大约为 2s。即线程执行结束花费的总时间,而不是设定的 3 秒。

3.4 interrupt 方法详解

3.4.1 打断 sleep、wait、join 的线程

这几个方法都会让线程进入阻塞状态,打断 sleep 的线程,会清空打断状态,以 sleep 为例:

@Slf4j(topic = "c.InterruptTest")
public class InterruptTest {
    public static void main(String[] args) throws InterruptedException {
        Thread t1= new Thread(() -> {
            log.debug("sleep...");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"t1");
        t1.start();
        Thread.sleep(3000); // 此处让主线程稍微等一下 t1 线程,否则主线程执行的要快一些,无法验证打断效果
         debug("interrupt");
        t1.interrupt();
        log.debug("打断标记:{}", t1.isInterrupted()); // 获取打断标记
    }
}

image.png
正常来讲, 线程被打断后,其打断状态应当会被标识为真,但是 sleep、join、wait 的线程在被打断后,会以异常的方式来表示被打断了,同时还会清空打断标记,即置为 false。

3.4.2 打断正常运行的线程

打断正常运行的线程,不会清空打断状态

public static void test2() throws InterruptedException {
    Thread thread = new Thread(() -> {
        while (true) {
            boolean interrupted = Thread.currentThread().isInterrupted();
            if(interrupted){ // 通过打断标记来判断是否退出循环
                log.debug("被打断了,退出循环");
                break;
            }
        }
    }, "t1");
    thread.start();

    Thread.sleep(2000);
    log.debug("interrupt");
    thread.interrupt();
}

正常运行的线程,其打断标记会被置为 true,默认为 false。

3.4.3 终止模式之两阶段终止模式

Two Phase Termination
在一个线程 T1 中如何“优雅”终止线程 T2?这里的【优雅】指的是给 T2 一个料理后事的机会。

1.错误思路

  • 使用线程对象的 stop() 方法停止线程
    • stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其他线程将永远无法获取锁。
    • 使用 System.exit(int) 方法停止线程。目的仅是停止一个线程,但这种做法会让整个程序停止。

2.两阶段终止模式

image.png

@Slf4j(topic = "c.Test1")
public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start();
        Thread.sleep(3500);
        tpt.stop();
    }
}

@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination  {
    private Thread monitor;

    // 启动监控线程
    public void start(){
        monitor = new Thread(() -> {
            while(true){
                Thread current = Thread.currentThread();
                if (current.isInterrupted()) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000); // 情况1,在sleep过程中被打断
                    log.debug("执行监控记录"); // 情况2,正常执行时被打断
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    current.interrupt(); // 重新设置打断标记
                }

            }
        });
        monitor.start();
    }

    // 停止监控线程
    public void stop(){
        monitor.interrupt();
    }
}

image.png

3.isInterrupted() 和 interrupted() 的区别

两个方法均可以判断当前线程是否被打断,但是 interrupted() 会清除打断标记 isInterrupted() 则不会清除。

3.3.4 打断 park 线程

LockSupport.park() 可以用来阻塞线程。
打断 park 线程,不会清除打断标记。

@Slf4j(topic = "c.TestInterrupt")
class TestInterrupt {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("park...");
            LockSupport.park(); // 线程 park 住了 
            log.debug("unpark...");
        }, "t1");
        t1.start();
    }
}

输出:
image.png
线程 t1 发生阻塞
使用 interrupt 进行打断:

@Slf4j(topic = "c.TestInterrupt")
class TestInterrupt {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("park...");
            LockSupport.park();
            log.debug("unpark...");
            log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
        }, "t1");
        t1.start();
        Thread.sleep(1);
        t1.interrupt(); // 打断 park 线程,使其继续执行。不会清除打断标记。
    }
}

输出:
image.png

问题一:
进行两次 park,观察输出。

@Slf4j(topic = "c.TestInterrupt")
class TestInterrupt {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("park...");
            LockSupport.park();
            log.debug("unpark...");
            log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
            LockSupport.park(); // 重新进行park
            log.debug("unpark...");
        }, "t1");
        t1.start();
        Thread.sleep(1);
        t1.interrupt();
    }
}

输出:
image.png
第二次 park 后,程序直接结束,并未阻塞住。这是因为 interrup() 方法会重新设置打断标记,将 t1 线程的打断标记置为了 true,在打断标记为 true 的情况下调用 park() 方法,线程是无法阻塞住的。
Thread.interrupted() 方法会返回当前线程的打断状态,并会清除打断标记,重新置为false。
改进方法,修改打断标记的获取方式(Thread.interrupted()),清除打断标记,将其重新置为false。

@Slf4j(topic = "c.TestInterrupt")
class TestInterrupt {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("park...");
            LockSupport.park();
            log.debug("unpark...");
            log.debug("打断状态:{}", Thread.interrupted());
            LockSupport.park();
            log.debug("unpark...");
        }, "t1");
        t1.start();
        Thread.sleep(1);
        t1.interrupt();
    }
}

输出:
image.png

总结
  • LockSupport.park() 可以用来阻塞线程。
  • park 住的线程在被 interrupted() 方法打断后,可以继续执行,但是被打断了 park 线程,其打断标记会被置为 true。
  • 打断标记为真(true)时,执行 park 方法会失效。

3.4 过时方法

还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁。

方法名功能说明
stop()停止线程运行
suspend()挂起(暂停)线程运行
resume()恢复线程运行

3.5 主线程与守护线程

默认情况下,Java 进程需要等待所有线程运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其他非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

@Slf4j(topic = "c.TestDaemon")
class TestDaemon {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while(true){
                if(Thread.currentThread().isInterrupted()){
                    break;
                }
            }
            log.debug("结束");
        }, "t1");
        t1.start();
        Thread.sleep(1000);
        log.debug("结束");
    }
}

输出:
image.png
主线程运行结束,但是,默认情况下,Java 进程需要等待所有线程运行结束,才会结束。而由于 t1 线程是一个死循环,所以整个程序无法结束。

将 t1 线程设置为守护线程:

@Slf4j(topic = "c.TestDaemon")
    class TestDaemon {
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                while(true){
                    if(Thread.currentThread().isInterrupted()){
                        break;
                    }
                }
                log.debug("结束");
            }, "t1");
            t1.setDaemon(true);  // 设置 t1 线程为守护线程
            t1.start();
            Thread.sleep(1000);
            log.debug("结束");
        }
    }

�输出:
image.png
只要其他非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
:::warning
注意:

  • 垃圾回收器线程就是一种守护线程
  • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求。
    :::

四、线程状态

4.1 五种状态

这是从操作系统层面来描述的
image.png

  • 初始状态:仅是在语言层面创建了线程对象,还未与操作系统线程关联
  • 可运行状态(就绪状态):指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
  • 运行状态:指获取了 CPU 时间片运行中的状态
    • 当 CPU 时间片用完,会从“运行状态”转换至“可运行状态”,会导致线程的上下文切换
  • 阻塞状态
    • 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入“阻塞状态”
    • 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至“可运行状态”
    • 与“可运行状态”的区别是,对“阻塞状态”的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
  • 终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转换为其他状态

4.2 六种状态

这是从 Java API 层面来描述的
image.png
根据 Thread.State 枚举,分为六种状态
image.png

  • NEW:线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE:当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的“可运行状态”、“运行状态”和“阻塞状态”(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行的)
  • BLOCKED、WAITING、TIMED_WAITING 都是 Java API 层面对“阻塞状态”的细分
  • TERMINATED:当线程代码运行结束

4.3 线程状态演示

@Slf4j(topic = "c.TestThreadState")
public class TestThreadState {
    public static void main(String[] args) {
        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                log.debug("running...");
            }
        };

        Thread t2 = new Thread("t2"){
            @Override
            public void run() {
                while(true){

                }
            }
        };
        t2.start();

        Thread t3 = new Thread("t3"){
            @Override
            public void run() {
                log.debug("running...");
            }
        };
        t3.start();

        Thread t4 = new Thread("t4"){
            @Override
            public void run() {
                synchronized (TestThreadState.class){
                    try {
                        Thread.sleep(1000000); // Timed Waiting
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t4.start();

        Thread t5 = new Thread("t5"){
            @Override
            public void run() {
                try {
                    t2.join(); // Waiting
                    // t2 无限循环,t5 需要等待 t2 执行结束,由于不知道 t2 什么时候执行结束,所以是 Waiting
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t5.start();

        Thread t6 = new Thread("t6"){
            @Override
            public void run() {
                synchronized (TestThreadState.class){ // blocked,t4 线程先拿到锁,t6 拿不到,阻塞住了
                    try {
                        Thread.sleep(1000000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t6.start();

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

        log.debug("t1 state {}", t1.getState());
        log.debug("t2 state {}", t2.getState());
        log.debug("t3 state {}", t3.getState());
        log.debug("t4 state {}", t4.getState());
        log.debug("t5 state {}", t5.getState());
        log.debug("t6 state {}", t6.getState());
    }
}

输出:
image.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/773329.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

加密(3)非对称加密

一、介绍 1、概念 非对称加密&#xff0c;又称现代加密算法&#xff0c;非对称加密是计算机通信安全的基石&#xff0c;保证了加密数据不会被破解。加密和解密使用的是两个不同的密钥&#xff0c;这种算法叫作非对称加密算法。 2、示例 首先生成密钥对, 公钥为(5,14)&#…

Redis基础教程(七):redis列表(List)

&#x1f49d;&#x1f49d;&#x1f49d;首先&#xff0c;欢迎各位来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里不仅可以有所收获&#xff0c;同时也能感受到一份轻松欢乐的氛围&#xff0c;祝你生活愉快&#xff01; &#x1f49d;&#x1f49…

Redis-Redis可视化工具Redis Insight下载及安装

下载 1、博主已经上传资源&#xff0c;点此下载 2、点此进入官方下载 2.1 点击Installing Redis Insight 2.2 点击Install on desktop 2.3 选择Install on desktop&#xff0c;点击Redis Insight is available for download for free from this web site从网站下载 2.4 下载…

中小企业适用的HTTPS证书

在当今数字化时代&#xff0c;企业的网站安全及其数据传输的安全是至关重要的。对于中小企业而言&#xff0c;选择适合的HTTPS证书&#xff08;即SSL证书&#xff09;是确保网站通信安全、增强用户信任和保护企业数据不可或缺的一步。下面将围绕中小企业适用的HTTPS证书进行深入…

【CT】LeetCode手撕—4. 寻找两个正序数组的中位数

目录 题目1- 思路2- 实现⭐4. 寻找两个正序数组的中位数——题解思路 3- ACM 实现 题目 原题连接&#xff1a;4. 寻找两个正序数组的中位数 1- 思路 思路 将寻找中位数 ——> 寻找两个合并数组的第 K 大 &#xff08;K代表中位数&#xff09; 实现 ① 遍历两个数组 &am…

【LeetCode:3033. 修改矩阵 + 模拟】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

如何在Qt使用uchardet库

如何在 Qt 中使用 uchardet 库 文章目录 如何在 Qt 中使用 uchardet 库一、简介二、uchardet库的下载三、在Qt中直接调用四、编译成库文件后调用4.1 编译工具下载4.2 uchardet源码编译4.3 测试编译文件4.4 Qt中使用 五、一些小问题5.1 测试文件存在的问题5.2 uchardet库相关 六…

GaussDB关键技术原理:高性能(四)

GaussDB关键技术原理&#xff1a;高性能&#xff08;三&#xff09;从查询重写RBO、物理优化CBO、分布式优化器、布式执行框架、轻量全局事务管理GTM-lite等五方面对高性能关键技术进行了解读&#xff0c;本篇将从USTORE存储引擎、计划缓存计划技术、数据分区与分区剪枝、列式存…

Appium环境搭建,华为nova8鸿蒙系统(包括环境安装,环境配置)(一)

1.安装代码工具包 appium python client pip install appium-python-client 2.安装JDK 参考链接: ant+jmeter+jenkins从0实现持续集成(Windows)-CSDN博客 3.下载并安卓SDK 下载地址:AndroidDevTools - Android开发工具 Android SDK下载 Android Studio下载 Gradle下载…

MySQL 8.0 架构 之 中继日志(Relay log)

文章目录 MySQL 8.0 架构 之 中继日志&#xff08;Relay log&#xff09;中继日志&#xff08;Relay log&#xff09;概述相关参数参考 【声明】文章仅供学习交流&#xff0c;观点代表个人&#xff0c;与任何公司无关。 来源|WaltSQL和数据库技术(ID:SQLplusDB) MySQL 8.0 OCP …

vue+openlayers之几何图形交互绘制基础与实践

文章目录 1.实现效果2.实现步骤3.示例页面代码3.基本几何图形绘制的关键代码 1.实现效果 绘制点、线、多边形、圆、正方形、长方形 2.实现步骤 引用openlayers开发库。加载天地图wmts瓦片地图。在页面上添加几何图形绘制的功能按钮&#xff0c;使用下拉列表&#xff08;sel…

【java高级】【算法】通过子节点 反向获取 树路径父节点 且不获取无关节点

有一个奇葩需求 要求 用户配置在某选择框的选项 例如 然后在选择时显示 用户配置的选项 依旧是返回树,但是只包含 选择的子节点。 以及涉及的父节点,树路径 不返回无关节点 【一般】我们开发中都是直接通过 树节点 返回 其下子节点 这个需求的确很奇葩。 而且还要考…

语音大模型引领自然交互新时代,景联文科技推出高质量语音大模型数据库

近期&#xff0c;OpenAI正式发布语音大模型GPT-4o&#xff0c;可以综合利用语音、文本和视觉信息进行推理&#xff0c;扮演一个个人语音交互助手。 在音频处理方面&#xff0c;它不仅能识别和转录多种口音和方言&#xff0c;改变语音的速度音调和振动&#xff0c;还能进行声音模…

CAS(compare and swap)

文章目录 CAS 的应用标准库的原子类自旋锁 CAS的ABA问题什么是 ABA 问题ABA 问题引来的 BUG相关面试题 CAS是一条CPU指令,就可以完成比较和交换这样的操作 我们假设内存中的原数据V&#xff0c;旧的预期值A&#xff0c;需要修改的新值B。 1.比较 A 与 V 是否相等。&#xff08;…

2024年7月4日 (周四) 叶子游戏新闻

老板键工具来唤去: 它可以为常用程序自定义快捷键&#xff0c;实现一键唤起、一键隐藏的 Windows 工具&#xff0c;并且支持窗口动态绑定快捷键&#xff08;无需设置自动实现&#xff09;。 卸载工具 HiBitUninstaller: Windows上的软件卸载工具 《最终幻想14》画面升级后 著名…

【高级篇】第10章 Elasticsearch 集群管理与扩展

在本章中,我们将深入探讨Elasticsearch集群的管理与扩展策略,旨在帮助读者构建一个既能应对大规模数据处理需求,又能保持高可用性和弹性的系统架构。我们将从集群架构设计入手,解析不同节点的角色与配置,然后转向节点发现与配置同步机制,最后讨论水平扩展与容错策略,确保…

【Python实战因果推断】20_线性回归的不合理效果10

目录 Neutral Controls Noise Inducing Control Feature Selection: A Bias-Variance Trade-Off Neutral Controls 现在&#xff0c;您可能已经对回归如何调整混杂变量有了一定的了解。如果您想知道干预 T 对 Y 的影响&#xff0c;同时调整混杂变量 X&#xff0c;您所要做的…

系统提示我未定义与 ‘double‘ 类型的输入参数相对应的函数 ‘finverse‘,如何解决?

&#x1f3c6;本文收录于「Bug调优」专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收藏&&…

新火种AI|AI搜索挑战百度谷歌,重塑信息检索的市场?

作者&#xff1a;一号 编辑&#xff1a;美美 AI正在颠覆传统的搜索引擎市场。 随着ChatGPT等大型语言模型的火爆&#xff0c;AI搜索技术成为了公众和业界关注的焦点。这些技术不仅能够提供快速、准确的信息检索&#xff0c;还能够通过自然语言处理技术理解用户的复杂查询&am…

步进电机(STM32+28BYJ-48)

一、简介 步进电动机&#xff08;stepping motor&#xff09;把电脉冲信号变换成角位移以控制转子转动的执行机构。在自动控制装置中作为执行器。每输入一个脉冲信号&#xff0c;步进电动机前进一步&#xff0c;故又称脉冲电动机。步进电动机多用于数字式计算机的外部设备&…