理论概述
单线程和多线程
为什么要使用多线程呢?多线程有什么好处呢?
如果在程序中,需要读写一个文件,该文件很大,那我们执行到该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