JUC-Java线程

53 阅读4分钟

创建线程

方式一:使用Thread,重写run方法

//method one
new Thread("t1"){
    @Override
    public void run() {
        System.out.println("t1");
    }
}.start();

方式二:使用Runable接口,并重写run方法

//method two
new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("t2");
            }
        }, "t2").start();

//method three
new Thread(() -> System.out.println("t3")).start();

//method four
Runnable r = () -> System.out.println("t4");
new Thread(r,"t4").start();

方式三:使用FutureTask+callable

//method five
FutureTask<String> ft = new FutureTask<>(new Callable<String>() {
    @Override
    public String call() throws Exception {
        Thread.sleep(3000);
        return "t5";
    }
});
new Thread(ft,"t5").start();
System.out.println(ft.get());//等待线程执行完毕后获取结果

Start 和 Run的区别

  • 直接调用 run 是在主线程中执行了 run,没有启动新的线程

  • 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码

sleep 与 yield

sleep

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

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

  • 睡眠结束后的线程未必会立刻得到执行

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

yield

  • 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  • 具体的实现依赖于操作系统的任务调度器

线程优先级

  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
  • 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用

join 方法详解

static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}

private static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
r = 10;
});

t1.start();
log.debug("结果为:{}", r);
log.debug("结束");
}

分析

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

解决方法

  • 用 sleep 理论上可以,但是不知道线程运行多久,无法明确Sleep时间。
  • 用 join,加在 t1.start() 之后即可
  • join会等待线程结束后再开始下一步工作。

interrupt 方法详解

  • 打断 sleep,wait,join 的线程
  • 这几个方法都会让线程进入阻塞状态,且线程的isInterrupt为false
  • 打断 sleep 的线程, 会清空打断状态,以 sleep 为例
  • 打断正常运行的线程, 不会清空打断状态

两阶段模型

public static void main(String[] args) throws InterruptedException {
    Thread t1 =new Thread(() ->{
        while (true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                e.printStackTrace();
            }
            System.out.println("t1");
            if(Thread.currentThread().isInterrupted()){
                System.out.println("t1 is interrupted");
                break;
            }
        }
    },"t1");
    t1.start();
    Thread.sleep(5000);
    t1.interrupt();
}

主线程与守护线程

  • 默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守

  • 护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

注意

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

线程的状态

五种状态

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

  • 【初始状态】 仅是在语言层面创建了线程对象,还未与操作系统线程关联

  • 【可运行状态】(就绪状态) 指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行

  • 【运行状态】 指获取了 CPU 时间片运行中的状态,当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换

  • 【阻塞状态】 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们

  • 【终止状态】 表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

  • 截屏2022-08-01 15.44.03.png

六种状态

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

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