Java中的线程

目录

创建线程的4种方式

线程的生命周期

线程状态之间的转换

线程方法

run()、start()

[sleep()、 yield()](#sleep()、 yield())

join

interrupt

daemon

线程原理

运行机制

线程调度



线程是CPU调度的最小单位,它可以和属于同一个进程的其他线程共享这个进程的全部资源

创建线程的4种方式

1.创建继承于Thread类的子类,并重写Thread类的run()方法。

java 复制代码
    @Test
    public void test4(){
        System.out.println("主线程"+Thread.currentThread().getName()+"输出");
        MyThread thread = new MyThread();
        thread.start();

    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("子线程"+Thread.currentThread().getName()+"输出");
        }
    }
  • 优点:编码简单

  • 缺点:线程类已经继承了 Thread 类无法继承其他类了,功能不能通过继承拓展(单继承的局限性)

2.创建一个实现了Runnable接口的类,并实现run()方法

java 复制代码
    @Test
    public void test4(){
        System.out.println("主线程"+Thread.currentThread().getName()+"输出");
        MyRunnable thread = new MyRunnable();
        new Thread(thread,"1号线程").start();

    }

    static class MyRunnable implements Runnable{
        @Override
        public void run() {
            System.out.println("子线程"+Thread.currentThread().getName()+"输出");
        }
    }
  • 缺点:代码复杂一点。

  • 优点:

    1. 线程任务类只是实现了 Runnable 接口,可以继续继承其他类,避免了单继承的局限性

    2. 同一个线程任务对象可以被包装成多个线程对象

    3. 适合多个多个线程去共享同一个资源

    4. 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立

    5. 线程池可以放入实现 Runnable 或 Callable 线程任务对象

3.通过Callable和FutureTask创建线程

java 复制代码
    @Test
    public void test4(){
        System.out.println("主线程"+Thread.currentThread().getName()+"输出");
        MyCallable thread = new MyCallable();
        FutureTask<String> task = new FutureTask<>(thread);
        Thread t = new Thread(task);
        t.start();
        try {
            String s = task.get(); // 获取call方法返回的结果(正常/异常结果)
            System.out.println(s);
        }  catch (Exception e) {
            e.printStackTrace();
        }

    }

    static class MyCallable implements Callable<String> {
        @Override
        public String call() throws Exception {
            return Thread.currentThread().getName() + "->" + "Hello World";
        }
    }

实现 Callable 接口:

  1. 定义一个线程任务类实现 Callable 接口,申明线程执行的结果类型

  2. 重写线程任务类的 call 方法,这个方法可以直接返回执行的结果

  3. 创建一个 Callable 的线程任务对象

  4. 把 Callable 的线程任务对象包装成一个未来任务对象

  5. 把未来任务对象包装成线程对象

  6. 调用线程的 start() 方法启动线程

public FutureTask(Callable<V> callable):未来任务对象,在线程执行完后得到线程的执行结果

  • FutureTask 就是 Runnable 对象,因为 Thread 类只能执行 Runnable 实例的任务对象,所以把 Callable 包装成未来任务对象

public V get():同步等待 task 执行完毕的结果,如果在线程中获取另一个线程执行结果,会阻塞等待,用于线程同步

  • get() 线程会阻塞等待任务执行完成

  • run() 执行完后会把结果设置到 FutureTask 的一个成员变量,get() 线程可以获取到该变量的值

优缺点:

  • 优点:同 Runnable,并且能得到线程执行的结果

  • 缺点:编码复杂

线程的生命周期

从操作系统层面:

  1. 新建(new):新创建了一个线程对象。
  2. 就绪(runnable):线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。
  3. 运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
  4. 阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被 CPU 调用以进入到运行状态。阻塞的情况分三种:
    1. 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态;
    2. 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),,则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
    3. 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入可运行状态。
  5. 死亡(dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

从JavaAPI层面,Java中的线程状态可以参考Thread类中的一个枚举类state。

线程状态 导致状态发生条件
NEW(新建) 线程刚被创建,但是并未启动,还没调用 start 方法,只有线程对象,没有线程特征
Runnable(可运行) 线程可以在 Java 虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器,调用了 t.start() 方法:就绪(经典叫法)
Blocked(阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入 Blocked 状态;当该线程持有锁时,该线程将变成 Runnable 状态
Waiting(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入 Waiting 状态,进入这个状态后不能自动唤醒,必须等待另一个线程调用 notify 或者 notifyAll 方法才能唤醒
Timed Waiting (限期等待) 有几个方法有超时参数,调用将进入 Timed Waiting 状态,这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.wait
Teminated(结束) run 方法正常退出而死亡,或者因为没有捕获的异常终止了 run 方法而死亡

线程状态之间的转换

现在有线程t

1、NEW --> RUNNABLE

当调用t.start()方法时, NEW --> RUNNABLE

2、RUNNABLE <--> WAITING:

(1)t线程用synchronized(obj)获取了对象锁后,调用 obj.wait()方法时,t 线程进入waitSet中, 从RUNNABLE --> WAITING。

(2)调用obj.notify(),obj.notifyAll(),t.interrupt()时, 唤醒的线程都到entrySet阻塞队列成为BLOCKED状态, 在阻塞队列,和其他线程再进行竞争锁,竞争锁成功,t 线程从 WAITING --> RUNNABLE,竞争锁失败,t 线程从 WAITING --> BLOCKED。

3、RUNNABLE <--> WAITING:

(1)当前线程(不是t线程)调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING。

(2)t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE。

4、RUNNABLE <--> WAITING

(1)当前线程调用 LockSupport.park() 方法会让当前线程从RUNNABLE --> WAITING

(2)调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从WAITING --> RUNNABLE

5、RUNNABLE <--> TIMED_WAITING

(1)t 线程用synchronized(obj)获取了对象锁后调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING

(2)t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时; 唤醒的线程都到entrySet阻塞队列成为BLOCKED状态, 在阻塞队列,和其他线程再进行竞争锁,竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE,竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED

6、RUNNABLE <--> TIMED_WAITING

(1)当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING (注意是当前线程在t 线程对象的waitSet等待)

(2)当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING --> RUNNABLE

7、RUNNABLE <--> TIMED_WAITING

(1)当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING

(2)当前线程等待时间超过了 n 毫秒或调用了线程的 interrupt() ,当前线程从 TIMED_WAITING --> RUNNABLE

8、RUNNABLE <--> TIMED_WAITING

(1)当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE --> TIMED_WAITING

(2)调用LockSupport.unpark(目标线程) 或调用了线程的interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING--> RUNNABLE

9、RUNNABLE <--> BLOCKED

(1)t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED

(2)持有 obj 锁的线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED

10、RUNNABLE --> TERMINATED

当前线程所有代码运行完毕,进入 TERMINATED

线程方法

Thread 类 API:

方法 说明
public void start() 启动一个新线程,Java虚拟机调用此线程的 run 方法
public void run() 线程启动后调用该方法
public void setName(String name) 给当前线程取名字
public void getName() 获取当前线程的名字 线程存在默认名称:子线程是 Thread-索引,主线程是 main
public static Thread currentThread() 获取当前线程对象,代码在哪个线程中执行
public static void sleep(long time) 让当前线程休眠多少毫秒再继续执行 Thread.sleep(0) : 让操作系统立刻重新进行一次 CPU 竞争
public static native void yield() 提示线程调度器让出当前线程对 CPU 的使用
public final int getPriority() 返回此线程的优先级
public final void setPriority(int priority) 更改此线程的优先级,常用 1 5 10
public void interrupt() 中断这个线程,异常处理机制
public static boolean interrupted() 判断当前线程是否被打断,清除打断标记
public boolean isInterrupted() 判断当前线程是否被打断,不清除打断标记
public final void join() 等待这个线程结束
public final void join(long millis) 等待这个线程死亡 millis 毫秒,0 意味着永远等待
public final native boolean isAlive() 线程是否存活(还没有运行完毕)
public final void setDaemon(boolean on) 将此线程标记为守护线程或用户线程

run()、start()

run:称为线程体,包含了要执行的这个线程的内容,方法运行结束,此线程随即终止。直接调用 run 是在主线程中执行了 run,没有启动新的线程,需要顺序执行

start:使用 start 是启动新的线程,此线程处于就绪(可运行)状态,通过新的线程间接执行 run 中的代码

run() 方法中的异常不能抛出,只能 try/catch

  • 因为父类中没有抛出任何异常,子类不能比父类抛出更多的异常

  • 异常不能跨线程传播回 main() 中,因此必须在本地进行处理

sleep()、 yield()

sleep:

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

  • sleep() 方法的过程中,线程不会释放对象锁

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

  • 睡眠结束后的线程未必会立刻得到执行,需要抢占 CPU

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

yield:

  • 调用 yield 会让提示线程调度器让出当前线程对 CPU 的使用

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

  • 会放弃 CPU 资源,不会释放锁资源

join

public final void join():等待这个线程结束

原理:调用者轮询检查线程 alive 状态,thread.join() 等价于:

java 复制代码
public final synchronized void join(long millis) throws InterruptedException {
    // 调用者线程进入 thread 的 waitSet 等待, 直到当前线程运行结束
    while (isAlive()) {
        wait(0);
    }
}
  • join 方法是被 synchronized 修饰的,本质上是一个对象锁,其内部的 wait 方法调用也是释放锁的,但是释放的是当前的线程对象锁,而不是外面的锁

  • 当调用某个线程(thread)的 join 方法后,该线程(thread)抢占到 CPU 资源,就不再释放,直到线程执行完毕

线程同步:

  • join 实现线程同步,因为会阻塞等待另一个线程的结束,才能继续向下运行

    • 需要外部共享变量,不符合面向对象封装的思想

    • 必须等待线程结束,不能配合线程池使用

  • Future 实现(同步):get() 方法阻塞等待执行结果

    • main 线程接收结果

    • get 方法是让调用线程同步等待

java 复制代码
public static int r = 0;
    @Test
    public void test5() throws InterruptedException {

        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r = 10;
        });
        t1.start();
        t1.join();//不等待线程执行结束,输出的10
        System.out.println(r);

    }

interrupt

打断线程

public void interrupt():打断这个线程,异常处理机制

public static boolean interrupted():判断当前线程是否被打断,打断返回 true,清除打断标记,连续调用两次一定返回 false

public boolean isInterrupted():判断当前线程是否被打断,不清除打断标记

打断的线程会发生上下文切换,操作系统会保存线程信息,抢占到 CPU 后会从中断的地方接着运行(打断不是停止)

  • sleep、wait、join 方法都会让线程进入等待状态,打断线程会清空打断状态(false)

    java 复制代码
        @Test
        public void test5() throws InterruptedException {
            Thread t1 = new Thread(()->{
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "t1");
            t1.start();
            Thread.sleep(500);
            t1.interrupt();
            System.out.println(" 打断状态: " + t1.isInterrupted());// 打断状态: false
    
        }
  • 打断正常运行的线程:不会清空打断状态(true)

java 复制代码
    @Test
    public void test5() throws InterruptedException {
        Thread t2 = new Thread(()->{
            while(true) {
                Thread current = Thread.currentThread();
                boolean interrupted = current.isInterrupted();
                if(interrupted) {
                    break;
                }
            }
        }, "t2");
        t2.start();
        Thread.sleep(500);
        t2.interrupt();
        System.out.println("打断状态: "+t2.isInterrupted());
    }

打断 park

park 作用类似 sleep,打断 park 线程,不会清空打断状态(true)

java 复制代码
    @Test
    public void test5() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println("park...");
            LockSupport.park();
            System.out.println("unpark...");
        }, "t1");
        t1.start();
        Thread.sleep(2000);
        t1.interrupt();
        System.out.println("打断状态:" + Thread.currentThread().isInterrupted());//打断状态:true
    }

如果打断标记已经是 true, 则 park 会失效

java 复制代码
LockSupport.park();
System.out.println("unpark...");
LockSupport.park();//失效,不会阻塞
System.out.println("unpark...");//和上一个unpark同时执行

可以修改获取打断状态方法,使用 Thread.interrupted(),清除打断标记

终止模式

终止模式之两阶段终止模式:Two Phase Termination

目标:在一个线程 T1 中如何优雅终止线程 T2?优雅指的是给 T2 一个后置处理器

错误思想:

  • 使用线程对象的 stop() 方法停止线程:stop 方法会真正杀死线程,如果这时线程锁住了共享资源,当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁

  • 使用 System.exit(int) 方法停止线程:目的仅是停止一个线程,但这种做法会让整个程序都停止

两阶段终止模式图示:

打断线程可能在任何时间,所以需要考虑在任何时刻被打断的处理方法:

java 复制代码
public class Test {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start();
        Thread.sleep(3500);
        tpt.stop();
    }
}
class TwoPhaseTermination {
    private Thread monitor;
    // 启动监控线程
    public void start() {
        monitor = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    Thread thread = Thread.currentThread();
                    if (thread.isInterrupted()) {
                        System.out.println("后置处理");
                        break;
                    }
                    try {
                        Thread.sleep(1000);                 // 睡眠
                        System.out.println("执行监控记录");   // 在此被打断不会异常
                    } catch (InterruptedException e) {      // 在睡眠期间被打断,进入异常处理的逻辑
                        e.printStackTrace();
                        // 重新设置打断标记,打断 sleep 会清除打断状态
                        thread.interrupt();
                    }
                }
            }
        });
        monitor.start();
    }
    // 停止监控线程
    public void stop() {
        monitor.interrupt();
    }
}

daemon

public final void setDaemon(boolean on):如果是 true ,将此线程标记为守护线程

线程启动前调用此方法:

java 复制代码
Thread t = new Thread() {
    @Override
    public void run() {
        System.out.println("running");
    }
};
// 设置该线程为守护线程
t.setDaemon(true);
t.start();

用户线程:平常创建的普通线程

守护线程:服务于用户线程,只要其它非守护线程运行结束了,即使守护线程代码没有执行完,也会强制结束。守护进程是脱离于终端并且在后台运行的进程,脱离终端是为了避免在执行的过程中的信息在终端上显示

说明:当运行的线程都是守护线程,Java 虚拟机将退出,因为普通线程执行完后,JVM 是守护线程,不会继续运行下去

常见的守护线程:

  • 垃圾回收器线程就是一种守护线程

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

线程原理

运行机制

Java Virtual Machine Stacks(Java 虚拟机栈):每个线程启动后,虚拟机就会为其分配一块栈内存

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存

  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

线程上下文切换(Thread Context Switch):一些原因导致 CPU 不再执行当前线程,转而执行另一个线程

  • 线程的 CPU 时间片用完

  • 垃圾回收

  • 有更高优先级的线程需要运行

  • 线程自己调用了 sleep、yield、wait、join、park 等方法

程序计数器(Program Counter Register):记住下一条 JVM 指令的执行地址,是线程私有的

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等

JVM 规范并没有限定线程模型,以 HotSopot 为例:

  • Java 的线程是内核级线程(1:1 线程模型),每个 Java 线程都映射到一个操作系统原生线程,需要消耗一定的内核资源(堆栈)

  • 线程的调度是在内核态运行的,而线程中的代码是在用户态运行,所以线程切换(状态改变)会导致用户与内核态转换进行系统调用,这是非常消耗性能

Java 中 main 方法启动的是一个进程也是一个主线程,main 方法里面的其他线程均为子线程,main 线程是这些线程的父线程

线程调度

线程调度指系统为线程分配处理器使用权的过程,方式有两种:协同式线程调度抢占式线程调度(Java 选择)

协同式线程调度:线程的执行时间由线程本身控制

  • 优点:线程做完任务才通知系统切换到其他线程,相当于所有线程串行执行,不会出现线程同步问题

  • 缺点:线程执行时间不可控,如果代码编写出现问题,可能导致程序一直阻塞,引起系统的奔溃

抢占式线程调度:线程的执行时间由系统分配

  • 优点:线程执行时间可控,不会因为一个线程的问题而导致整体系统不可用

  • 缺点:无法主动为某个线程多分配时间

Java 提供了线程优先级的机制,优先级会提示调度器优先调度该线程,但这仅仅是一个提示,调度器可以忽略它。在线程的就绪状态时,如果 CPU 比较忙,那么优先级高的线程会获得更多的时间片,但 CPU 闲时,优先级几乎没作用。

说明:并不能通过优先级来判断线程执行的先后顺序

相关推荐
Daniel 大东20 分钟前
BugJson因为json格式问题OOM怎么办
java·安全
Ajiang28247353041 小时前
对于C++中stack和queue的认识以及priority_queue的模拟实现
开发语言·c++
幽兰的天空1 小时前
Python 中的模式匹配:深入了解 match 语句
开发语言·python
Theodore_10224 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
冰帝海岸5 小时前
01-spring security认证笔记
java·笔记·spring
世间万物皆对象6 小时前
Spring Boot核心概念:日志管理
java·spring boot·单元测试
没书读了6 小时前
ssm框架-spring-spring声明式事务
java·数据库·spring
----云烟----6 小时前
QT中QString类的各种使用
开发语言·qt
lsx2024066 小时前
SQL SELECT 语句:基础与进阶应用
开发语言
小二·6 小时前
java基础面试题笔记(基础篇)
java·笔记·python