什么是线程
说到线程,不得不提进程,对于进程相信大家都不陌生。比如当我们启动qq的时候,操作系统会给qq程序创建一个进程,启动桌面版微信,操作系统也会给微信创建一个进程,同理,java程序启动后,也会创建一个进程。
根据狭义的定义,进程就是正在运行程序的抽象。
话说回来,那什么是线程呢?
在某些进程内部,还需要同时执行一些子任务,比如在一个Java进程中,后台除了执行正常用户代码之外,可能还需要线程在后台执行垃圾回收,即时编译等,我们称这些子任务为线程。我们可以理解线程为一种轻量级的进程,它们有自己的程序计数器、栈以及局部变量等,可以被操作系统进行调度,而且相比进程而言,线程创建、上下文切换的代价都更小,所以现代操作系统都是以线程作为调度的最小单位。
进程和线程的关系
-
进程是系统进行资源分配基本单位,线程是系统调度的基本单位。
-
一个进程可以包含一个或多个线程。
-
同一个进程所有线程可共享该进程的资源,如内存空间等。
-
进程之间通信较为复杂
-
同一台计算机内部的进程通信,称为IPC(Inter-Process Communication)
-
不同计算机之间进程通信,需要通过网络,遵循共同的协议,如TCP/IP
-
-
线程之间通信比较方便,因为同一个进程中所有线程共享内存,比如多个线程可以访问同一个共享变量。
我们可以把进程理解成一个营业的酒店,而线程就是酒店的老板及工作人员如大堂经理、保洁阿姨、保安、厨师等。酒店的老板及工作人员,都能共用酒店的资源。一个酒店再怎么样,就算没有任何工作人员,也必须有一个老板才行。
并行和并发
cpu执行代码是一条一条顺序执行的,但是,即便是单核CPU,也可以同时运行多个任务。这是因为操作系统会让多个任务轮流交替执行,每个任务执行若干时间,执行完后切换到下一个任务执行,这个过程非常快,造成一种同时执行的假象,这种在宏观上同时执行,微观上交替执行
的现象,我们称之为***并发(Concurrency)***。
当然,对于拥有多核CPU的计算机而言,是可以允许多个CPU同时执行不同任务的,比如在CPU1执行Word,在CPU2上执行QQ音乐听歌,这种真正意义上的同时执行,我们称为***并行(Parallelism)***。
引用golang语言创造者Rob Pike的一段话:
- 并发是同一时间,应对(dealing with)多件事情的能力。
- 并行,是同一时间动手做(doing)多件事情的能力。
并发和并行的区别,有点类似于以下场景:
高速公路上设有收费站,假设有两条道路,但是收费通道只有一个,这时两个道路的车,就需要交替排队进入同一个收费入口进行收费,这种情况类似于并发,假如有两个收费入口,两条道的车都在自己的收费入口收费,这种情况类似于并行。
创建线程的方式
在java中创建并使用线程非常简单,只需要创建一个线程对象,并调用其start方法,就可以了,比如下面这样:
public class CreateThread {
public static void main(String[] args) {
Thread t1 = new Thread();
t1.start();
}
}
当我们执行这段程序的时候,jvm实际首先会创建一个主线程,用来执行main()
方法,然后在执行main()
方法的第3行代码时,会创建再次创建线程t1,在第4行通过start()
,启动线程t1。不过这个线程启动后,实际并没有执行任何代码就结束了,如果我们希望线程启动后能执行指定代码,可以通过以下两种方式:
继承Thread类并重写run方法
public class ExtendThread {
public static void main(String[] args) {
Thread t1 = new MyThread();
t1.start();
}
public static class MyThread extends Thread{
@Override
public void run() {
Debug.debug("我是线程t1");
}
}
}
上述方法可以简写成匿名内部类的形式:
public static void main(String[] args) {
Thread t1 = new Thread(){
@Override
public void run() {
Debug.debug("我是线程t1");
}
};
t1.start();
}
创建线程时传入Runnable对象
public class RunnableThread {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
Debug.debug("我是线程t2");
}
};
Thread t2 = new Thread(runnable);
t2.start();
}
}
从java8开始,这种方式可以使用lamda表达式简写
public class LambdaRunnableThread {
public static void main(String[] args) {
Thread t2 = new Thread(() -> Debug.debug("我是线程t2"));
t2.start();
}
}
那么,这两种写法更推荐哪种呢?一般而言下,更建议使用第二种方法,因为
- java里面类是单继承的,使用接口的方式,可以避开这种限制。
- 有利于任务拆分。如果一个任务需要拆分成很多小任务,不必为每个任务创建一个线程。
- 将任务的创建和执行解耦,一个线程生产任务,可以交给其他线程去执行。
run()和start()的区别
需要特别注意的是,run()
和start()
的区别,线程创建完成后,执行start()
方法,才会真正启动线程去并发执行任务,而run()
只是一个普通的实例方法,没有启动线程的作用。
public class StartAndRunTest {
public static void main(String[] args) {
Debug.debug("我是线程:{}",Thread.currentThread().getName());
Thread t2 = new Thread(() -> Debug.debug("我是线程:{}",Thread.currentThread().getName()),"t2");
t2.run();
}
}
以上代码中Thread.currentThread().getName()
会打印当前执行线程的名字。这段代码首先会打印主线程的名字,然后创建线程t2,接着启动线程t2,t2线程启动后执行其任务代码,会打印出正在执行该代码的线程名字也就是t2,其执行结果如下:
2021-03-07 20:07:05 [main] 我是线程:main
2021-03-07 20:07:05 [t2] 我是线程:t2
如果我们把代码第5行改成: t2.run()
,就会输出下面的结果了:
2021-03-07 20:07:19 [main] 我是线程:main
2021-03-07 20:07:19 [main] 我是线程:main
因为run()
方法并没有真正启动线程t1,只是在主线程中调用了t2线程的一个普通方法。
线程的常用api
名称 | 类型 | 作用 |
---|---|---|
sleep(long millis) | 静态方法 | 使当前线程休眠millis毫秒 |
yield | 静态方法 | 当前线程让出cpu |
join | 线程实例方法 | 等待直到某个线程执行完毕再执行后续代码 |
join(long millis) | 线程实例方法 | 等待某个线程执行完毕再执行后续代码,最多等待millis毫秒 |
sleep
sleep(long millis)
是一个静态方法,其作用是使当前线程进入休眠millis毫秒,比如下面这个例子,我们让线程休眠5s再执行
public class SleepTest {
public static void main(String[] args) {
new Thread(() -> {
Debug.debug("开始执行");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Debug.debug("执行结束");
},"t1").start();
}
}
执行结果如下:
2021-03-07 20:09:24 [t1] 开始执行
2021-03-07 20:09:29 [t1] 执行结束
需要注意的是,sleep()
会抛出InterruptedException异常,这个异常的作用是让我们可以中断正在休眠中的线程。
yield
yield的翻译过来是屈服、让步的意思,由此可以看出,yield()
方法的作用是让当前线程主动让出cpu,从运行状态变成就绪状态,相当于是把执行机会让给其他线程,但不一定能成功让出。打个简单的比方,就像是你在排队买车票,本来轮到你了,这时后面有个人因为时间比较赶,于是你非常绅士的把位置让给他。那么这个方法的应用场景是什么呢?看了该方法的注释,发现原来这个方法实际上很少有机会用到,主要用于代码调试,复现bug。
/**
* A hint to the scheduler that the current thread is willing to yield
* its current use of a processor. The scheduler is free to ignore this
* hint.
*
* <p> Yield is a heuristic attempt to improve relative progression
* between threads that would otherwise over-utilise a CPU. Its use
* should be combined with detailed profiling and benchmarking to
* ensure that it actually has the desired effect.
*
* </p><p> It is rarely appropriate to use this method. It may be useful
* for debugging or testing purposes, where it may help to reproduce
* bugs due to race conditions. It may also be useful when designing
* concurrency control constructs such as the ones in the
* {@link java.util.concurrent.locks} package.
*/
public static native void yield();
join
在多线程应用中,假如线程A的输入依赖于线程B的输出结果,此时,线程A就需要等待线程B执行完毕再继续执行,我们可以使用jdk提供的join()
方法来实现这种线程之间的协作。如下所示有两个join方法:
public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException
无参的join表示A线程会无限等待直到线程B执行完毕,而有参的join方法,会等待直到最大超时时间,超出这个时间后哪怕线程B还在执行,就不再继续等待。通过下面这个简单的例子,我们来验证一下join()
的作用:
public class JoinMain {
public static void main(String[] args) throws InterruptedException {
MyTask myTask = new MyTask();
Thread t = new Thread(myTask);
t.start();
//t.join();
System.out.println(myTask.result);
}
private static class MyTask implements Runnable{
private int result;
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
result = 100;
}
}
}
这段代码的执行结果是0,而不是100,因为main线程执行第7行代码的时候,线程t还处于休眠状态,此时result还是等于0,当我们去掉第7行的注释,main线程等待t线程执行完毕,设置了result的值,才执行第8行代码,结果就是100了。
守护线程
java线程可以分为守护线程和非守护线程,在java进程中,一旦非守护线程全部执行完毕,即便守护线程还没执行完,该进程也会强行终止。顾名思义,守护线程和它的名字一样,就是在后台默默的做一些系统工作,比如java进程中的垃圾回收线程、JIT线程就是守护线程,正因为如此,当系统中没有其他线程后,守护线程也就失去了存在的意义,无事可做,整个进程自然也就应该结束了。守护线程可以在创建线程后通过setDeamon()
进行设置。举个例子:
public class DaemonThreadMain {
public static void main(String[] args) {
Thread t = new Thread(()->{
Debug.debug("开始执行");
Sleep.seconds(3);//睡眠3s
Debug.debug("执行结束");
},"t1");
// t.setDaemon(true);
t.start();
Sleep.seconds(1);
Debug.debug("执行结束");
}
}
执行结果如下:
2021-03-06 20:47:59 [t1] 开始执行
2021-03-06 20:48:00 [main] 执行结束
2021-03-06 20:48:02 [t1] 执行结束
可以看出,线程t1执行耗时3s,main线程耗时1s,main线程1s后就执行完毕退出了,而t1线程作为非守护线程,在主线程结束后,依然是过了3s后才执行完毕。现在我们把第8行t.setDaemon(true)
这行代码去掉注释,最终执行结果如下:
2021-03-06 20:52:29 [t1] 开始执行
2021-03-06 20:52:30 [main] 执行结束
可以看出,由于t1是守护线程,主线程退出后,线程t1就退出而没有往下执行了。
需要注意的是,setDameon()
方法必须在线程开始也就是调用start()
执行之前调用,否则会报以下异常:
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.setDaemon(Thread.java:1359)
at com.taoge.demos.DaemonThreadMain.main(DaemonThreadMain.java:20)
附录-工具类 Sleep & Debug
sleep
/**
* Sleep 是对Thread.sleep的简单封装
*
* @author chentao
* @date 2021/3/6
*/
public final class Sleep {
public static void sleep(TimeUnit unit, long duration){
try {
unit.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void sleepInterruptibly(TimeUnit unit, long duration) throws InterruptedException{
unit.sleep(duration);
}
public static void millis(long millis){
sleep(TimeUnit.MILLISECONDS, millis);
}
public static void seconds(long seconds){
sleep(TimeUnit.SECONDS, seconds);
}
}
Debug
/**
* Debug类是对System.out.println的简单封装,便于打印出类似这样格式化的日志:
* 2021-03-07 20:11:06 [t1] 这是测试
* 包含了日期时间、线程名称和自定义的打印内容
* @author chentao
* @date 2021/3/4
*/
public class Debug {
private static SimpleDateFormat format = new SimpleDateFormat();
static {
format.applyPattern("yyyy-MM-dd HH:mm:ss");
}
public static void debug(String msg, Object... params){
for (Object param : params) {
msg = msg.replaceFirst("\\{\\}",param.toString());
}
System.out.println(format.format(new Date())+" ["+Thread.currentThread().getName()+"] "+msg);
}
}
源码地址
> 文章所有代码都放在github上
>
> github.com/ThomasChant/jucDemos