Java并发编程 JUC——Thread.class

88 阅读9分钟

创建和运行线程

方法一 直接创建Thread

public static void main(String[] args) {
    Thread thread1 = new Thread(new Runnable() {
        @Override
        public void run() {
        //线程要执行的任务
            log.info("你好,世界!");
        }
    },"线程1");
    //启动线程就绪状态,等待Cpu分配时间片执行线程
    thread1.start();
}

Java8以后可以用lambda简写,如下

public static void main(String[] args) {
    Thread thread1 = new Thread(() -> log.info("你好,世界!"),"线程1");
    //线程就绪,等待Cpu分配时间片执行线程
    thread1.start();
}

方法二 使用Runnable配合Thread

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

  • Thread代表线程
  • Runnable可运行的任务(线程要执行的代码)
public static void main(String[] args) {
    //创建任务对象
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            log.info("你好,世界!");
        }
    };
    //参数一是任务对象,参数二是线程名
    Thread thread1 = new Thread(runnable,"线程1");
    //线程就绪,等待Cpu分配时间片执行线程
    thread1.start();
}

总结:Thread与Runnable的关系

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

方法 FutureTask配合Thread

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

public static void main(String[] args) throws ExecutionException, InterruptedException {
    // 创建任务对象
    FutureTask<Integer> task3 = new FutureTask<>(() -> {
        log.info("hello");
        return 100;
    });
    // 参数1 是任务对象; 参数2 是线程名字,推荐
    new Thread(task3, "t3").start();
    // 主线程阻塞,同步等待 task3 执行完毕的结果
    Integer result = task3.get();
    log.info("结果是:{}", result);
}

输出

19:22:27 [t3] c.ThreadStarter - hello
19:22:27 [main] c.ThreadStarter - 结果是:100

主线程与守护线程

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

例如:

log.info("开始运行...");
Thread t1 = new Thread(() -> {
    log.info("开始运行...");
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    log.info("运行结束...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
Thread.sleep(1000);
log.info("运行结束...");

输出:

20:37:49.694 [main] org.juc.yren.Main - 开始运行...

20:37:49.781 [daemon] org.juc.yren.Main - 开始运行...

20:37:50.783 [main] org.juc.yren.Main - 运行结束...

可以看出daemon守护线程没有执行运行结束....线程就结束了

注意

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

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

线程的状态

五种状态(从操作系统层面来描述)

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

image.png

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

六种状态(从JAVA API层面来描述)

这是从 JAVA API 层面来描述的

image.png

根据Thread.State枚举,分为六种状态

public enum State {
    /**
     * 尚未启动的线程的线程状态。
     */
    NEW,

    /**
     * 可运行线程的线程状态。处于可运行状态的线程正在Java虚拟机中执行,
     * 但它可能正在等待来自操作系统的其他资源,例如处理器
     */
    RUNNABLE,

    /**
     * 该线程被阻止等待监视器锁定。处于阻塞状态的线程
     * 正在等待监视器锁进入同步块/方法,或在调用Object.wait后重新进入同步块或方法。
     */
    BLOCKED,

    /**
     * 等待线程的线程状态。
     * 处于等待状态的线程正在等待另一个线程执行特定操作。
     * 例如,对对象调用了Object.wait()的线程正在等待另一个线程
     * 对该对象调用Object.notify()或Object.notifyAll()。
     * 调用了thread.join()的线程正在等待指定的线程终止。
     */
    WAITING,

    /**
     * 等待线程的线程状态。
     * 具有指定等待时间的等待线程的线程状态
     */
    TIMED_WAITING,

    /**
     * 终止线程的线程状态。线程已完成执行。
     */
    TERMINATED;
}
  • NEW 线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
  • BLOCKEDWAITINGTIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分
  • TERMINATED 当线程代码运行结束

线程(Thread)常用方法

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

sleep()

  1. 调用sleep会让当前线程从Running进入Timed Waiting状态(阻塞)
  2. 其他线程可以使用interrupt方法打断该正在sleep的睡眠线程,这时sleep方法会抛出InterruptedException
  3. 睡眠结束后的线程也未必会立刻得到执行(虽然一直持有锁,但是需要获得CPU是时间片才可以继续执行)
  4. 建议使用TimeUnit的sleep代替Thread的sleep来获得更好的可读性

打断正在sleep的线程

Thread thread1 = new Thread() {
    @Override
    public void run() {
        while (true) {
            //休眠100秒
            try {
                TimeUnit.SECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("我要退出了!");
                break;
            }
        }
    }
};

运行上面的代码,发现程序无法终止。为什么? 需要修改为

Thread thread1 = new Thread() {
    @Override
    public void run() {
        while (true) {
            //休眠100秒
            try {
                TimeUnit.SECONDS.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); //新增
                e.printStackTrace();
            }
            if (this.isInterrupted()) {
                System.out.println("我要退出了!");
                break;
            }
        }
    }
};

注意:sleep方法由于中断而抛出异常之后,线程的中断标志会被清除(置为false),所以在异常中需要执行this.interrupt()方法,将中断标志位置为true

sleep/wait区别

功能都相当于暂停当前线程,有什么区别?

  • sleep是Thread的方法,阻塞当前线程不会释放锁
  • wait是Object的方法,必须在synchronized同步代码块中执行,会使当前对象释放锁,进入Monitor ->waitSet等待唤醒(notify)

线程中断

在java中,线程中断是一种重要的线程写作机制,从表面上理解,中断就是让目标线程停止执行的意思,实际上并非完全如此。stop方法停止线程的过于暴力是直接停止,jdk中提供了更好的中断线程的方法。严格的说,线程中断并不会使线程立即退出,而是给线程发送一个通知,告知目标线程,有人希望你退出了!至于目标线程接收到通知之后如何处理,则完全由目标线程自己决定,这点很重要,如果中断后,线程立即无条件退出,我们又会到stop方法的老问题。

Thread类提供打断方法

Thread提供了3个与线程中断有关的方法,这3个方法容易混淆,大家注意下:

public void interrupt() //中断线程
public boolean isInterrupted() //判断线程是否被中断
public static boolean interrupted()  //判断线程是否被中断,并清除当前中断状态

interrupt()方法是一个实例方法,它通知目标线程中断,也就是设置中断标志位为true,中断标志位表示当前线程已经被中断了。isInterrupted()方法也是一个实例方法,它判断当前线程是否被中断(通过检查中断标志位)。最后一个方法interrupted()是一个静态方法,返回boolean类型,也是用来判断当前线程是否被中断,但是同时会清除当前线程的中断标志位的状态。

清除打断状态则:打断状态true -> 未打断状态false

public static void main(String[] args) throws ExecutionException, InterruptedException {
    Thread t1 = new Thread(() -> {
        while (true) {
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("我要退出了!");
                break;
            }
        }

    }, "t1");
    t1.start();
    //睡眠主线程
    TimeUnit.SECONDS.sleep(1);
    //打断t1线程
    t1.interrupt();
}

上面代码中有个死循环,interrupt()方法被调用之后,线程的中断标志将被置为true,循环体中通过检查线程的中断标志是否为ture(this.isInterrupted())来判断线程是否需要退出了。

LockSupport类打断

Park & Unpark

基本使用

它们是LockSupport类中的方法

//暂停当前线程
LockSupport.park();
//回复某个线程的运行
LockSupport.unpark(暂停线程对象(Thread))

可以先park再unpark,也可以先unpark再park,多次调用unpark相当于调用一次;