Java多线程基础学习,Thread解读、java线程的状态、同步和异步、两阶段终止模式

理论概述

单线程和多线程

为什么要使用多线程呢?多线程有什么好处呢?

        如果在程序中,需要读写一个文件,该文件很大,那我们执行到该io操作时,cpu就会等待该io操作执行完才会继续运行下面的代码,进程调度相信大家都知道,操作系统会并发的调度使用每一个进程,使得cpu能给被充分使用,线程也是一样的,为了不让cpu傻等io操作我们可以多开一个线程,把这耗时的io操作放在另一个线程之中执行,这样cpu就能继续往下执行,这就是多线程的最大意义——榨干cpu资源。这里是并发的概念。

        还有一种情况就是,我现在程序中全部是计算型的,同样也可以使用多线程进行优化,如一个比较耗时的方法,我可以把这个方法拆散,每个线程都执行一小段代码,达到并行计算,前提是我现在的电脑必须是多核的支持多线程这样干才有意义。反之我的cpu只支持单线程,那么想一想,现在只能并发的执行这几个拆分了的方法,且都是计算型的都需要占用cpu的情况下,那岂不是增加了线程上下文切换的额外开销吗?那还不如直接使用单线程呢。

因此多线程的使用是要分情况的,只有合理的使用多线程才能增加程序的运行效率

同步和异步

同步:就差不多是单线程的执行,A调用B这个方法,A必须等到B运行结束才能继续执行;还有如网络通信中,发送了一个请求消息给服务器,同步的意思就是必须等服务器给我返回响应消息了我才能接着运行。

异步:A如果需要调用B这个方法,异步的话就是A只做调用B这个方法的动作,A只需要告诉你我要调用B了,好了,我已经告诉你了,你B可以开始运行了,然后A接着向下执行,不管B方法的具体执行。可以看出,这是需要多线程支持的。使用Ajax也是这么个理,本身就是异步请求,通过开一个线程达到,不影响当前线程的运行而去请求其他数据,响应返回时通过回调方法接收。

注意:同步在多线程中还有另外一层意思,是让多个线程步调一致

Java线程基本操作

线程对象的创建与启动

        Java默认启动一个主线程来执行我们的代码,如果你想在你的代码里开启另外的线程,使用Thread类,Thread的构造方法接收一个Runnable接口(该接口只有一个run方法需要实现)的实现类对象,Runnable中的run方法是该线程需要执行的代码逻辑,线程创建好后并不是直接运行的,需要通过Thread对象的start()方法启动该线程(让该线程能给被os的任务调度器调度执行)。如下是最基本的多线程创建和启动。

Runnable:

//创建线程对象
Thread t=new Thread(new Runnable() {
    @Override
    public void run() {
        //该线程的代码逻辑
    }
});
//启动线程
t.start();

源码分析:

 这是Thread的构造方法,继续点进init()。

 init里无非就是赋值一些优先级名字和Runnable什么的把这些保存到Thread线程对象的属性里。

然后我们start启动线程的方法是调用一个native方法,用来再操作系统层面开启线程,该线程最终执行的代码逻辑是在Thread对象中的run方法。

 该run()方法又调用了刚刚构造函数里赋值进来的Runnable里的run方法,大致流程就是这样。

根据这一解析,所以我们可以另辟蹊径,不传入Runnable对象,我们就直接重写Thread里的run方法,直接写我们的代码逻辑也是可以的,如下,差不太多。

Thread t=new Thread(){
    @Override
    public void run() {
        //代码部分
    }
};
//启动线程
t.start();

FutureTask:

我们还可以配合FutureTask<V>来创建线程对象,因为FutrueTask继承了一个RunnableFuture的接口,该接口继承了Runnable,所以FutrueTask也是Runnable的子实现类,可以作为任务对象。 

RunnableFuture是Runnable的升级版,它额外继承了Futrue接口,Future接口如下,他有下面一些的方法被FutureTask实现了。

 FutureTask类的构造方法是传入一个Callable<V>类的对象,如下,Callable类似Runnable,作为线程的任务单位,但是Runnable的run方法没有返回值且不能抛异常。Callable的call()方法是可以的,返回值类型是你传入的泛型……

 归根结底,线程Thread对象一定会执行它里面属性的run方法,我们找到FutureTask类的run()方法,如下就是它里面的大致流程。

演示:

public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {

    //这里的泛型就是它的返回值类型
    FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {
            log.debug("call...");
            Thread.sleep(2000);
            //返回Integer类型的返回值
            return 100;
        }
    });
    Thread t = new Thread(futureTask);
    t.start();
    //同步等待(阻塞)futureTask的结果,会一直等
    Integer res = futureTask.get();
    //超时等待,如果超过5秒,结果还没出来就抛出异常不等了
    Integer res1 = futureTask.get(5000, TimeUnit.SECONDS);
    //判断futureTask是否完成
    boolean done = futureTask.isDone();
    //取消该futureTask的任务
    boolean cancel = futureTask.cancel(true);
    //是否已经被取消
    boolean cancelled = futureTask.isCancelled();

    log.debug("{}", res);
}

FutureTask主要是完成线程间的通信,如果A线程需要当B线程执行完时返回一个返回值,则FutureTask是不戳的选择。

线程运行原理

        java虚拟机栈就是线程运行的原理,可以看这一篇来对虚拟机栈有一个初步的了解,每个线程都有一个虚拟机栈,虚拟机栈会保存线程运行时的上下文数据,且线程每运行一次方法都会在虚拟机栈中压入一个栈帧,栈帧就是一次方法调用,栈顶的栈帧就是当前正在运行的方法,称为活动栈帧。

        

线程上下文切换:

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

上下文切换靠程序计数器保存状态信息,程序计数器是java中的概念,它的作用是几率下一条jvm指令的执行地址,下次能给找到运行到哪里了。

java的线程实现是操作系统1:1的内核级线程

线程常见方法

  • start():启动一个新线程,在新线程运行run方法中的方法。start方法只是让线程进入就绪,里面代码不一定运行(cpu的时间片还没分给他),每个线程对象的start对象只能调用一次
  • run():新线程启动后会调用的方法。直接run不会开线程,就是单纯的方法调用
  • join()当前线程阻塞,等待某个线程运行结束,才执行当前线程。把本线程变成同步的了,同步等待join方法的调用者线程。
  • join( long n):同上,但是它最多等待n毫秒,超时了就溜了。
  • getId():获取线程长整形唯一的id
  • setPriority():设置线程的优先级,优先级有1-10,默认优先级为5,大优先级能提高线程被cpu调度的几率,但是如果cpu不看你的优先级,那么设置优先级是没有的,谁叫java线程是内核级线程呢?
  • getState():获取线程状态信息,下面会讲到
  • isInterrupted():判断是否被打断,不会清除打断标记
  • isAlive():判断线程是否运行完了,一个线程运行完了,状态就会设置为死亡状态
  • interrupt():打断线程,如果被打断线程正在sleep, wait,join会导致被打断的线程抛出InterruptedException,并清除打断标记;如果打断的正在运行的线程,则会设置打断标记;park的线程被打断,也会设置打断标记。

    • interrupt不会真正的打断线程,只是请求而已。

    • 被打断的线程(实际正常运行,除非正在sleep这些) 通过打断标记获取判断是否有其他线程要打断本线程

  • interrupted()static方法,判断当前线程是否被打断,会清除打断标记
  • currentThread()static方法,获取当前正在执行的线程
  • sleep(long n)static方法,让当前执行的线程休眠n毫秒,让出cpu时间片,写在哪个线程就是哪个线程睡眠
  • yield()static方法,提示线程调度器让出当前线程对cpu的使用,主要是为了测试和调试

过时方法:stop,suspend暂停线程,resume恢复线程运行

一些方法解读:

1.sleep

  • 调用sleep会让当前线程从Running状态变为TimedWaiting(阻塞)状态
  • 其他线程可以使用interrupt()方法打断正在睡眠的线程,这是sleep()的线程会抛出InterruptedException异常
  • 睡眠结束的线程未必会立刻得到执行,因为睡眠行了还是需要争抢cpu时间片
  • sleep(long time)的单位为毫秒,可以使用TimeUtil的sleep()方法指定单位,增强可读性,如:TimeUnit.SECONDS.sleep(2),中间可以加个单位,功能不变
        Thread t = new Thread(()->{
            try {
                log.debug("t1...");
                //当前线程睡2000ms,会让出cpu时间片
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                log.debug("睡眠被打断");
            }
        },"t1");
        t.start();
        //需要让主线程睡眠一段时间,让t线程启动起来
        Thread.sleep(1000);
        //叫醒t线程
        t.interrupt();
        log.debug("{}",t.getState());
//        19:23:54.687 [t1]    t1...
//        19:23:55.694 [main]  TIMED_WAITING
//        19:23:55.694 [t1]    睡眠被打断

2.yeild 

  • yield()static方法,提示线程调度器让出当前线程对cpu的使用,主要是为了测试和调试

    • 当前线程从Running状态变为RUNNABLE就行状态,然后调度其他线程

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

yield和线程的优先级都是依靠操作系统的任务调度器的

小知识点

在我们开发时,我们的服务器可能需要用到死循环,一直监听某一件事,但是如果你用的是while(true)这个壳子,中间没有别的代码了,那么你的这段代码会一直空转浪费cpu资源,cpu需要一直计算、计算……,cpu会忙不过来的,因为没有一点间隙,影响其他的线程工作。

我们可以再while(true)中加上sleep(推荐)或者yield来防止这种空转的行为:

while (true){
    Thread.sleep(50);
    Thread.yield();
}

每次循环的时候,就让该线程睡眠50ms,这50ms可以很大的解放cpu。

可以用wait或条件变量达到类似的效果,不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景,sleep适用于无需锁同步的场景

 3.join

join()让当前线程等待某一个线程执行完毕再继续执行,用sleep也可以实现线程间的等待,但是sleep到底要睡多久呢?这个时间不好把握,所以使用join方法更好,该join()方法需要放在要等待的线程的start()之后,如果线程没有start() join会无效哦。

join方法有happens-before规则:如果线程A执行操作ThreadB.join()成功返回,那么线程B中的任意操作happens-before 线程A从ThreadB.join()操作成功返回。具体看上一篇

join(long timeout),可以指定超时时间,等够多少时间就不等了,直接往下运行。

4.interrupt

①打断sleep(),wait(),join()这种处于阻塞的线程

Thread t = new Thread(()->{
    try {
        log.debug("t1...");
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        log.debug("睡眠被打断");
    }
},"t1");
t.start();
TimeUnit.SECONDS.sleep(1);
t.interrupt();
//查看打断标记,结果为false
log.debug("{}",t.isInterrupted());
//20:25:47.487 [t1]     t1...
//20:25:48.495 [t1]     睡眠被打断
//20:25:48.495 [main]   false

代码如上:在主线程打断t线程之前,需要睡眠一下,让t线程start完成,因为interrupt没happens-before规则。

当t还在睡眠时被主线程打断,然后t线程打断标记会变为true,但是因为sleep、wait、join操作被打断时出现异常后,会自动把打断标记设置为false,就相当于情况打断标记。

②打断正常运行的线程

Thread t = new Thread(() -> {
    log.debug("t1...");
        while (true){

        }
}, "t1");
t.start();
TimeUnit.SECONDS.sleep(1);
t.interrupt();
//查看打断标记,结果为false
log.debug("{}", t.isInterrupted());
//20:32:21.759 [t1]    t1...
//20:32:22.758 [main]  true

该线程会死循环,因为该打断不意味着结束线程,只是给该线程的打断标记设置为true,所以我们可以在死循环里加上判断,判断当前线程是否被打断了。如下:就能给正常退出了。

while (true){
    if (Thread.currentThread().isInterrupted()){
        break;
    }
}

两阶段终止模式

Two Phase Termination,是多线程的一个设计模式。指在一个线程中优雅的终止另一个线程,给他一个料理后事的机会

错误思路:

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

如下图,当我们需要设计一个后台监控线程,每两秒执行一次监控任务, 我们希望可以随时终止该监控程序。

 根据上图我们设计的监控线程如下:

Thread t = new Thread(() -> {
    while (true) {
        //如果被打断了,就料理后事,退出循环
        if (Thread.currentThread().isInterrupted()) {
            log.debug("退出监控程序");
            break;
        }
        try {
            //执行监控任务
            log.debug("监控..."); //情况1
            //停2s
            Thread.sleep(2000);//情况2
        } catch (InterruptedException e) {
            //在睡眠时被打断了,一定记住要重新设置打断标记
            Thread.currentThread().interrupt();
        }
    }
}, "monitor");
t.start();
//等5s后打断监控线程
Thread.sleep(5000);
//打断监控程序
t.interrupt();

//21:08:22.903 [monitor]   监控...
//21:08:24.918 [monitor]   监控...
//21:08:26.933 [monitor]   监控...
//21:08:27.901 [monitor]   退出监控程序

解读:我们主线程打断监控线程可能有两种情况,情况①当监控线程执行监控任务时被打断,下一次循环时就会直接退出循环;情况②当监控线程在睡眠时被打断,那么会发生异常,进入异常后打断标记会被清除,因此我们需要重新把自己打断,下一次循环时才会正常结束。

Thread.currentThread().isInterrupted()可以替换为Thread.interrupted(),该方法是静态方法,用于查看本线程是否被打断,但是该方法判断完后,会清除打断标记,但是在本程序清不清除无所谓,都能结束

5.park

LockSupport类的park()方法,就是停住当前线程,其他线程同样可以使用interrupt打断,如果当前线程打断标记为true,则park不会生效,就像代码中有两个连续的park,此时被其他线程打断了第一个,此时打断标记就已经设置为true了,所有第二个不会生效,除非在前面清除该打断标记。

还有一种打断方法也是LockSupport类中的:unpark(Thread t)方法,该方法能够打断park的线程,且不会设置打断标记,且可以提前打断park()方法,如下:

Thread t = new Thread(() -> {
    LockSupport.park();
    log.debug("{}",Thread.currentThread().isInterrupted());
    LockSupport.park();
    log.debug("unpark...");
}, "monitor");
t.start();
//打断park
//t.interrupt();
LockSupport.unpark(t);

LockSupport就像是一个容器一样,当执行unpark时会在容器里加一个数字,当该线程执行park时,如果容器为1的话,就抵消这个1;如果之前没有unpark,当前容器为0,则进入阻塞状态,等到unpark解锁它。

守护线程

默认情况下,java这个进程需要等待所有线程运行结束才会结束

守护线程:只要其他非守护线程执行结束,也跟着强制结束。垃圾回收器是守护线程,Tomcat 中的 Acceptor和Poller线程都是守护线程,所以Tomcat 接收到shutdown命令后,不会等待它们处理完当前请求

setDaemon(boolean flag)方法用于设置线程是否为守护线程,默认不是守护线程

Thread t = new Thread(() -> {
    while (true);
}, "monitor");
t.setDaemon(true);
t.start();

如上代码,程序会立即结束,因为t线程虽然是死循环,但是它是守护线程,主线程一结束,它也会跟着结束,setDaemon源码如下:如果当前线程已经start了,再设置就会抛异常,所以要在start前设置。

线程的状态

五种状态

从操作系统层面来描述

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

【可运行状态】(就绪状态)指该线程已经被创建并关联,可以由cpu进行调度

【运行状态】获取到了cpu时间片,cpu时间片运行完后转为就绪状态

【阻塞状态】调用了阻塞api,该线程不会用到cpu,会导致线程上下文切换,进入阻塞状态,唤醒后变为可运行状态

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

六种状态

从java api层面来描述分六种状态

新建、可运行、等待、计时等待、阻塞、死亡

 //线程的六种状态enum
 public enum State {
    NEW,             //初始的状态,还没有start
    RUNNABLE,  //start后。涵盖了操作系统层面的【可运行状态】【运行状态】和【阻塞状态】
//(由于BIO导致的线程阻塞,在java里无法区分,任然认为是可运行,
//如调用InputStream的read操作,它是线程阻塞的读),Java线程中将就绪(ready)和
//运行中(running)两种状态统称为称为“运行”。
     //BLOCKED,WAITING,TIMED_WAITING都是Java API层面对【阻塞状态】的细分
    BLOCKED,         //表线程阻塞于锁 
    WAITING,         //进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
    TIMED_WAITING,     //有时限的等待
    TERMINATED;         //start完后,终止状态
}

 RUNNABLE的别名就是RUNNING

 代码演示六种状态:

Thread t1 = new Thread(() -> {
    log.debug("NEW");
});

Thread t2 = new Thread(() -> {
    while(true);    //RUNNABLE
});
t2.start();

Thread t3 = new Thread(() -> {
    synchronized (demo.class){
        try {
            Thread.sleep(20000);
            log.debug("TIMED_WAITING");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});
t3.start();

Thread t4 = new Thread(() -> {
    try {
        t3.join();
        log.debug("WAITING");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
t4.start();

Thread t5 = new Thread(() -> {
    synchronized (demo.class){//被t3锁了
        log.debug("得不倒锁,导致阻塞");//BLOCKED
    }
});
t5.start();

Thread t6 = new Thread(() -> {
    log.debug("终止状态");
});
t6.start();

log.debug("t1线程的状态{}",t1.getState());
log.debug("t2线程的状态{}",t2.getState());
log.debug("t3线程的状态{}",t3.getState());
log.debug("t4线程的状态{}",t4.getState());
log.debug("t5线程的状态{}",t5.getState());
log.debug("t6线程的状态{}",t6.getState());
//10:13:11.521 [main] demo - 214  t1线程的状态NEW
//10:13:11.523 [main] demo - 216  t2线程的状态RUNNABLE
//10:13:11.523 [main] demo - 216  t3线程的状态TIMED_WAITING
//10:13:11.523 [main] demo - 216  t4线程的状态WAITING
//10:13:11.523 [main] demo - 216  t5线程的状态BLOCKED
//10:13:11.523 [main] demo - 216  t6线程的状态TERMINATED

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值