深入学习掌握JUC并发编程系列(二) -- 梳理Java线程的创建和状态

一、创建和运行线程

  • 匿名内部类(Anonymous Classes):没有名字的在类内部定义的新的类(无需创建新的类,减少代码冗余)
    • 条件:
      • 必须继承一个父类或者实现一个接口
      • 父类或者父接口当中的方法只使用一次
    • 创建方式:通过创建子类对象
new 父类名或者父接口名() {
// 方法的重写
@Override
public void method(){
// 重写方法的语句
 }
};
注意事项:
	1. 匿名内部类在创建对象时,只能使用唯一一次(当需要多次创建对象,而且类的内容是一样的话,建议单独定义实现类)
	2. 匿名对象,在调用方法时,只能使用唯一一次(当需要同一个对象,调用多次方法,建议给对象起个名字)
	3. 匿名内部类省略了实现类/子类名称,匿名对象省略了对象名
  • lambda表达式:创建只有一个抽象方法的接口的实例,省略new接口名和抽象方法的写法
    • 条件:函数式接口(Functional Interface):接口中只有一个抽象方法
    • 写法:(形式参数)->{ 执行语句};
    • 可选:
      • 参数圆括号可选:一个参数无需圆括号,多个参数需要圆括号
      • 大括号可选:主体包含一个语句,不需要使用大括号
      • 返回关键字可选:主体只有一个返回值则编译器会自动返回值,带大括号需要指定返回值

1. 创建线程方法

  • 直接使用Thread:线程创建和执行任务合并在一起
    • new一个Thread对象
    • 使用匿名内部类创建Thread子类
    • 重写run()方法
    • 启动线程
// 创建线程对象
Thread t = new Thread("线程名字") {
    public void run() {
        // 要执行的任务
    }
};
// 启动线程
t.start();
  • 使用Runnable配合Thread:将创建线程和要执行的任务分开
    • 创建Runnable对象
    • 使用匿名内部类,实现Runnable接口
    • 将Runnable对象作为参数传递给Thread构造方法
    • 创建线程对象
    • 启动线程
Runnable task = new Runnable() {
    public void run(){
        // 要执行的任务
    }
};
// 创建线程对象
Thread t = new Thread( task, "线程名字" );
// 启动线程
t.start();
  • 使用 lambda 表达式精简代码(Java8之后支持)
// 创建任务对象
Runnable task = () -> {log.debug("running"); };
// 参数1 :任务对象;参数2:线程名字
Thread t = new Thread(task, "t1");
t.start();
 
//直接将lambda表达式作为创建线程对象的参数传入
Thread t = new Thread(() -> {log.debug("running"); }, "t1");
t.start();

2. 原理

  • Thread与Runnable的关系
    • 方法一:在子类中重写了父类(Thread类)的run()方法
    • 方法二:通过传入Runnable对象,调用父类中Runnable对象的run()方法
private Runnable target;
@Override
public void run() {
    if (target != null) {
        target.run();
    }
}
  • 用 Runnable 更容易与线程池等高级 API 配合(推荐使用Runnable对象,而不是直接使用Thread对象)
  • 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活(推荐使用组合的关系,而不是继承关系)
  • 方法三(类似方法二)
    • 使用FutureTask配合Thread:
      • 创建Callable对象,重写call()方法(可通过lambda表达式简化)
      • 以Callable对象为参数创建FutureTask对象
      • 将FutureTask对象作为参数传递给Thread构造方法,创建线程对象
      • 调用get()方法,获取返回结果
// 创建任务对象
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
 //要执行的任务
 return 100;
});
// task是任务对象
Thread t1 = new Thread(task, "t1");
t1.start();
// 主线程阻塞,同步等待task执行完毕的返回结果
Integer result = task.get();
log.debug("返回结果是:{}", result);

  • 原理:
    • FutureTask间接实现了Runnable接口,所以可以将任务对象(Callable)作为参数传入Thread构造方法中
    • 还间接实现了Future接口,通过该接口中的get()方法可以获取任务执行结果
    • 接收Callable类型的参数,相比Runnable,可以抛出异常,还带有返回结果(重写call()方法)

二、查看进程线程的方法

  • Windows:
    • 通过任务管理器查看
    • 控制台(cmd)命令:tasklist(查看进程) taskkill(杀死进程)
  • Linux:
    • ps -fe:查看所有正在运行的进程( | grep java:筛选)
    • ps -fT -p PID:查看某个进程的所有线程
    • Kill PID:杀死进程
    • top:动态显示所有正在运行的进程(按cpu占用率排序)
    • top -H -p PID:动态显示某个进程的所有线程
  • Java:
    • jdk自带的命令:jps、jstack(查看所有进程、查看某个java进程的所有线程)
    • 图形化工具:jconsole(连接java进程,查看所有线程信息)

三、线程运行的原理

1. 任务调度器

  • 为操作系统服务(不受JVM控制),为Runnable状态的线程分配CPU时间片
  • 基于线程优先级或者线程等待时间

2. 栈与栈帧

  • 线程启动后,虚拟机就会分配一块栈内存
  • 每个栈由多个栈帧(frame)组成,栈帧对应着方法调用
  • 每个线程只能有一个活动栈帧,位于栈顶(当前栈帧,对应着正在运行的方法)

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

  • 定义:CPU不再执行当前线程,转而执行另一个线程
  • 产生原因:
    • 被动:
      • 线程的CPU时间片用完(任务调度器)
      • 垃圾回收(暂停用户线程,执行gc线程)
      • 更高优先级的线程需要运行
    • 主动:线程自己调用了sleep、yield、wait、join、pack、lock、synchronized等方法
  • 程序计数器:当发生线程上下文切换时,可以保存当前线程的状态(记录行号指示器)
  • 避免频繁的线程上下文切换(影响性能)

四、Thread类常见方法

五、start()与run()

  • 相同:都是线程类的方法
  • 不同:
    • start方法表示启动线程,run方法表示线程启动后执行的代码(执行任务)
    • start方法只能执行一次,run方法可以重复调用
    • 不start,直接调用run方法是在主线程中执行了run方法,没有启动新的线程(无法实现异步处理)

六、sleep()与yield()

  • sleep:放弃当前线程的CPU时间片
    • 让当前线程状态从Running进入Timed Waiting状态(阻塞状态)
    • 其它线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException异常
    • 睡眠结束后的线程未必会立刻被执行(任务调度器分配)
    • 建议用TimeUnit类的sleep方法(包含时间单位,可读性更好)
  • yield:让出当前线程的CPU使用权
    • 让当前线程状态从Running进入Runnable状态(就绪状态)
    • 任务调度器重新分配时间片,调度线程执行
  • 区别:
    • 优先级:
      • sleep方法放弃CPU使用权,会给低优先级的线程执行机会
      • yield方法让出CPU使用权,只会给相同或更高优先级线程执行机会
    • 状态:
      • sleep方法让线程进入Timed Waiting状态(阻塞 blocked)
      • yield方法让线程进入Runnable状态(就绪 ,有机会重新被任务调度器分配时间片)
    • 异常:
      • sleep方法被打断(interrupt)后抛出InterruptedException异常
      • yield方法没有声明异常
    • 不推荐:yield依赖任务调度器(操作系统层面)实现,可移植性差

七、线程优先级

  • 线程优先级会提示(hint)调度器优先调度该线程(仅仅是一个提示,调度器可以忽略)
  • CPU比较忙,优先级高的线程会获得更多的时间片;CPU比较闲时,优先级几乎没作用
  • setPriority,优先级范围[1, 10],默认为5
  • yield和线程优先级只是给任务调度器一个提示,真正CPU时间片的分配还是由任务调度器实现

八、join()方法(同步)

  • 主线程同步等待t1线程
  • 等待多个线程结果(t1,t2),join(long n):有时效的等待(最多等待n毫秒)

九、interrupt()方法

  • 打断标记:线程是否被其它线程打断过(打断过:True),通过isInterrupted()方法获取
    • 打断阻塞状态的线程(sleep,wait,join),清空打断标记(为False),并抛出异常
    • 打断正常运行的线程,不会清空打断标记(为True),不会终止线程运行(通过打断标记,由被打断的线程自己决定是否继续运行)
private static void test2() throws InterruptedException {
    Thread t2 = new Thread(()->{
        while(true) {
            Thread current = Thread.currentThread(); //获取当前线程
            boolean interrupted = current.isInterrupted(); //获取打断标记
            if(interrupted) {
                log.debug(" 打断状态: {}", interrupted);
                break;
            }
        }
    }, "t2");
    t2.start();
    sleep(0.5);
    t2.interrupt();
}
  • 打断park线程:不会清空打断标记(为True),线程会继续运行
    • 打断标记为True时,park会失效,可以使用interrupted方法重新实现park
    • Thread.interrupted() 方法会返回打断标记,并清除打断标记(为False)

十、不推荐使用的方法

方法已过时(@Deprecated),容易破坏同步代码块,造成线程死锁

十一、主线程与守护线程

  • Java进程结束运行,需要等待所有线程执行完毕
  • 主线程:main线程
  • 守护线程(Daemon):其它非守护线程运行结束后,守护线程强制结束(即使未运行完)
    • t1.setDaemon(True):将t1线程设为守护线程(必须在start()方法前)
      垃圾回收线程就是一种守护线程
    • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程

十二、线程的状态

  • 五种状态(操作系统层面):
    • 初始状态:创建线程对象,还未与操作系统线程关联
    • 可运行状态(就绪状态):线程已经被创建(与操作系统线程关联),准备分到CPU时间片
    • 运行状态:获取CPU时间片,正在运行(当时间片用完,运行状态转为可运行状态,导致线程上下文切换)
    • 阻塞状态:调用了阻塞API,线程不会分到CPU时间片(任务调度器不考虑),导致线程上下文切换
    • 终止状态:线程执行完毕,生命周期结束
  • 六种状态(Java API层面 Thread类中的State枚举类)
    • NEW:线程刚被创建,还未调用start方法运行(对应初始状态)
    • RUNNABLE:调用start方法之后(对应可运行状态、运行状态、阻塞状态)
    • BLOCKED、WAITING、TIMED_WAITING(对应阻塞状态)
    • TERMINATED:线程执行完毕(对应终止状态)
    • t1.getState()方法:获取线程状态

总结

  • 线程创建的三种方法:Thread子类、Runnable对象、FutureTask
  • 线程重要api:start、run、sleep、join(同步等待)、interrupt(打断标记)
  • 线程状态:操作系统层面(5种)、java层面(6种)
  • 原理:
    • 线程运行流程(虚拟机栈、栈帧、线程上下文切换、程序计数器)
    • 线程创建方式(源码)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值