Java 进程中每一个线程对应着一个Thread实例。线程的描述信息在Thread的实例属性中得到保存,供JVM进行线程管理和调度时使用。
虽然一个进程可以有很多个线程,但是在一个CPU内核上,同一时刻只能有一个线程是正在执行的,该线程被叫做当前线程。
✨如何创建Java线程
这里介绍三种创建Java线程的方法:
-
通过继承Thread 类创建线程
public class ThreadDemo extends Thread{ @Override public void run() { System.out.println(this.getName()); } public static void main(String[] args) { for(int i=0;i<8;i++) { ThreadDemo demo = new ThreadDemo(); demo.start(); } } }
执行main方法输出结果:
Thread-0 Thread-3 Thread-4 Thread-2 Thread-1 Thread-7 Thread-6 Thread-5 Process finished with exit code 0
-
实现Runnable 接口来创建线程
public class RunnableDemo implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName()); } public static void main(String[] args) { //通过实现Runnable的类创建线程 for(int i=0;i<2;i++) { Thread thread = new Thread(new RunnableDemo()); thread.start(); } //通过匿名类创建线程 for(int i=0;i<2;i++) { int finalI = i; new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + finalI); } }, "匿名线程-").start(); } //通过lambda表达式创建线程 for(int i=0;i<2;i++){ int finalI = i; new Thread(()->{ System.out.println(Thread.currentThread().getName()+ finalI); },"lambda线程-").start(); } } }
执行main方法输出结果:
Thread-0 Thread-1 匿名线程-1 匿名线程-0 lambda线程-0 lambda线程-1
-
实现 Callable 接口创建带返回值的线程
通过FutureTask 类和 Callable接口的联合使用可以创建能够获取异步执行结果的线程,步骤为:
1.创建一个Callable接口的实现类,并实现其call() 方法,编写好异步执行的具体逻辑,可以有返回值。
2.使用Callable 实现类的实例构造一个 FutureTask实例。
3.使用FutureTask实例作为Thread构造器的target入参,构造新的Thread线程实例。
4.调用Thread实例的start() 方法启动新线程,启动新线程的run()方法并发执行。其内部的执行过程为:
Thread.run() -> FutureTask.run() -> Callable.call()
5.调用FutureTask 对象的get() 方法阻塞性地获得并发线程的执行结果。
public class CallableDemo implements Callable<String> { @Override public String call() throws Exception { return "执行结果:success"; } public static void main(String[] args) throws ExecutionException, InterruptedException { CallableDemo demo = new CallableDemo(); FutureTask<String> futureTask = new FutureTask<>(demo); Thread thread = new Thread(futureTask); thread.start(); System.out.println(futureTask.get()); } }
执行main方法输出结果:
执行结果:success
✨如何停止线程
线程通过 start() 方法启动后,会在run() 方法执行结束后进入终止状态。
如果我们想终止一个正在运行的线程,很多人首先想到的就是 Thread.stop() 方法。
但是这个方法是不安全的,它会导致:
- 立即抛出 ThreadDeath异常。
- 会释放当前线程锁持有的所有的锁。
Thread中的stop方法标记了 @Deprecated
。
是不建议使用的。
我们看如下的代码:
public class ThreadStopDemo2 extends Thread{
@Override
public void run() {
try {
for (int i = 0; i < 100000; i++) {
//System.out.println("running ... " + i);
syncPrint(i);
}
System.out.println("this will not print.");
}catch (Throwable throwable){
throwable.printStackTrace();
}
}
//使用Synchronized关键字,将两个print置为原子输出
public void syncPrint(int num){
synchronized (this){
System.out.print("the first one."+num);
System.out.println(" the second one."+num);
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new ThreadStopDemo2();
thread.start();
Thread.sleep(100);
thread.stop();
}
}
执行main方法的输出结果:
//(省略之前的21986条输出)
the first one.21987 the second one.21987
the first one.21988 the second one.21988
the first one.21989java.lang.ThreadDeath
at java.lang.Thread.stop(Thread.java:858)
at ThreadStopDemo2.main(ThreadStopDemo2.java:26)
这里我们可以看出:
① 在run() 方法中,代码 System.out.println(“this will not print.”); 还没有执行就被 ThreadDeath 异常给中断了。造成业务的不完整。
② the first one.21989java.lang.ThreadDeath
并没有打印出 the second one. 21989
,我们使用Synchronized修饰了这两句print,可以使之为原子方法。但是 stop() 方法会释放Synchronized同步锁,使得 System.out.println(" the second one."+num);
还没有执行就被中断了。
所以,实际应用中,不可以使用stop() 来中断线程。
在Thread中提供来一个interrupt() 方法,用来向指定线程发送中断信号,收到该信号的线程可以使用 interrupted() 方法来判断是否被中断,具体代码如下:
public class InterruptDemo extends Thread{
@Override
public void run() {
int num = 0;
while(!Thread.currentThread().isInterrupted()){
num++;
}
System.out.println("while结束,num="+num);
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new InterruptDemo();
thread.start();
TimeUnit.SECONDS.sleep(1);
System.out.println("before interrupt:" + thread.isInterrupted());
thread.interrupt();
System.out.println("after interrupt:"+ thread.isInterrupted());
}
}
我们在执行 interrupt() 方法前后各输出一次isInterrupted(),可以到,它的值的变化情况。
执行main方法结果:
before interrupt:false
after interrupt:true
while结束,num=1953355864
这个例子可以看出,interrupt() 方法通过传递标识让运行的程序自己停止。线程在收到信号标识后,可以继续把run() 方法中的逻辑执行完成(即打印出了 “while结束,num=1953355864”)让run方法安全执行结束,完成线程中断。
如果我们使用interrupt 来中断处于阻塞状态的线程呢?
如下代码:
public class BlockedThreadInterruptDemo extends Thread{
@Override
public void run() {
while(!Thread.currentThread().isInterrupted()){
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程中断,退出while");
}
public static void main(String[] args) throws InterruptedException {
BlockedThreadInterruptDemo thread = new BlockedThreadInterruptDemo();
thread.start();
TimeUnit.MILLISECONDS.sleep(10);
System.out.println("before: "+Thread.currentThread().isInterrupted());
thread.interrupt();
System.out.println("after: "+Thread.currentThread().isInterrupted());
}
}
执行main方法后输出,线程仍属于阻塞状态,没有结束。
before: false
after: false
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:342)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at BlockedThreadInterruptDemo.run(BlockedThreadInterruptDemo.java:8)
我们可以看到,在interrupt() 执行前后,interrupted的值都是false。
这是因为,阻塞线程被其他线程使用interrupt() 唤醒时,在抛出InterruptedException异常之前,会先把线程中断状态进行复位,也就是将中断标记变成false。
这样设计的目的是想把中断权交给正在运行的线程,我们在异常处理中可以加一些逻辑,来决定是否要中断该线程。比如我们在catch代码块中再次调用 Thread.currentThread().interrupt() :
public class BlockedThreadInterruptDemo extends Thread{
@Override
public void run() {
while(!Thread.currentThread().isInterrupted()){
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
}
System.out.println("线程中断,退出while");
}
public static void main(String[] args) throws InterruptedException {
BlockedThreadInterruptDemo thread = new BlockedThreadInterruptDemo();
thread.start();
TimeUnit.MILLISECONDS.sleep(10);
System.out.println("before: "+Thread.currentThread().isInterrupted());
thread.interrupt();
System.out.println("after: "+Thread.currentThread().isInterrupted());
}
}
执行main方法输出结果,线程也中断了。
before: false
after: false
线程中断,退出while
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:342)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at BlockedThreadInterruptDemo.run(BlockedThreadInterruptDemo.java:8)
对于设计线程阻塞的方法(Thread.join()、Object.wait()、Thread.sleep() 等)都会抛出InterruptedException
异常。
如果要让一个处于阻塞状态下的线程被中断,首先该线程要被唤醒并响应中断请求,而InterruptedException就是一种响应方式。当开发者捕获了这个异常,就说明当前线程收到了中断请求,在异常中来决定如何处理这次中断:是只捕获异常不做处理?还是将异常抛出去?还是停止当前线程?
InterruptedException 在抛出异常之前会对线程中断标识进行复位,目的是让运行的线程自己决定何时中断。
✨线程状态转换
- NEW, 新建状态, 调用 new Thread() 时的状态
- RUNNABLE,运行状态,通过 start() 启动线程后的状态
- BLOCKED,阻塞状态,当线程执行synchronized代码,且未抢占到同步锁时,会成为该状态。
- WAITING,调用Object.wait() 等方法,会成为该状态
- TIMED_WAITING,超时等待状态,调用 sleep(100) ,100ms后会自动唤醒。
- TERMINATED,终止状态,线程的run() 方法中的指令执行完成后的状态。
接下来,通过jstack工具查看代码中的这些状态。
TIMED_WAITING状态
public class TimedWaitingStatus {
public static void main(String[] args) {
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
运行main方法后,在命令行中执行 jps,然后选中相应的线程id,执行jstack xxx:
PS C:\Users\86137\IdeaProjects\untitled1> jps
26976 Jps
50112
52888 TimedWaitingStatus
59404 Launcher
PS C:\Users\86137\IdeaProjects\untitled1> jstack 52888
"Thread-0" #12 prio=5 os_prio=0 tid=0x000002aab5e9e800 nid=0xceb0 waiting on condition [0x000000faa85fe000]
java.lang.Thread.State: TIMED_WAITING (sleeping)//线程状态
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:342)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at TimedWaitingStatus.lambda$main$0(TimedWaitingStatus.java:7) //从这里进入TIMED_WAITING状态
at TimedWaitingStatus$$Lambda$1/1324119927.run(Unknown Source)
at java.lang.Thread.run(Thread.java:750)
使用 jstack 可以看到,通过sleep() 方法阻塞的线程进入了TIMED_WAITING 状态。
WAITING状态
public class WaitingStatusExample {
public static void main(String[] args) {
new Thread(()->{
synchronized (WaitingStatusExample.class){
try {
WaitingStatusExample.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
使用jstack查看
PS C:\Users\86137\IdeaProjects\untitled1> jps
50112
57924 Launcher
61396 Jps
21212 WaitingStatusExample
PS C:\Users\86137\IdeaProjects\untitled1> jstack 21212
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.351-b10 mixed mode):
"Thread-0" #12 prio=5 os_prio=0 tid=0x00000154539fd800 nid=0xbb88 in Object.wait() [0x0000003d552fe000]
java.lang.Thread.State: WAITING (on object monitor)//当前线程状态
at java.lang.Object.wait(Native Method)//导致该状态的方法
- waiting on <0x000000076c59c768> (a java.lang.Class for WaitingStatusExample)
at java.lang.Object.wait(Object.java:502)
at WaitingStatusExample.lambda$main$0(WaitingStatusExample.java:6)
- locked <0x000000076c59c768> (a java.lang.Class for WaitingStatusExample)
at WaitingStatusExample$$Lambda$1/1324119927.run(Unknown Source)
at java.lang.Thread.run(Thread.java:750)
BLOCKED状态
执行main方法后,使用 jstack 查看:
PS C:\Users\86137\IdeaProjects\untitled1> jps
33600 Jps
50112
61876 BlockedStatusExample
60168 Launcher
PS C:\Users\86137\IdeaProjects\untitled1> jstack 61876
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.351-b10 mixed mode):
"thread-2" #13 prio=5 os_prio=0 tid=0x000002159b6ca800 nid=0xe05c waiting for monitor entry [0x0000007c0efff000]
java.lang.Thread.State: BLOCKED (on object monitor)//当前线程状态
at BlockedStatusExample.run(BlockedStatusExample.java:6)
- waiting to lock <0x000000076c59c078> (a java.lang.Class for BlockedStatusExample)
at java.lang.Thread.run(Thread.java:750)
BLOCKED 只有在synchronized 锁阻塞时出现。
线程运行状态流转图:
✨线程的一些基本操作
Thread.currentThread()
当前线程
静态方法获取当前正在执行的线程,简称当前线程。当前线程就是正在执行当前代码逻辑的Java线程。
Thread.sleep()
休眠
是让目前正在执行的线程休眠,让CPU去执行其他的任务。
线程状态从RUNNING -> TIMED_WAITING。
sleep() 方法会有InterruptedException 受检异常抛出,如果调用了sleep() 方法,必须进行异常检查,获取InterruptedException异常。
Thread.interrupt()
中断
Thread的stop() 是用来终止正在运行的线程,它被标记为过时方法,不建议使用。这是因为stop()方法是危险的,就像使用关闭电源的方式来关闭计算机,而不是按正常程序关机一样。在程序中,我们不能随便中断一个线程,因为我们无法知道这个线程正处于什么状态,它可能持有某个锁,强行中断会导致锁无法释放的问题;它可能在操作数据库,强行中断会导致数据不一致的问题。
所以我们使用 Thread.interrupt(),它本质不是用来中断一个线程,而是将线程设置为中断状态。它有两个作用:
① 如果线程被Object.wait()、Thread.join()、Thread.sleep() 三种方法之一阻塞,此时调用该线程的interrupt() 方法,该线程将抛出一个InterruptedException中断异常(该线程必须实现预备好处理磁异常) ,从而提早中介被阻塞状态。
② 如果此线程处于运行状态,线程就继续运行,会把线程的中断标记置为true。程序可以在适当的位置通过调用isInterrupted()方法来查看自己是否被中断,并执行退出操作。
Thread.join()
线程合并
项目中会遇到一个场景,就是需要等待某几件事情完成后才能继续往下执行,比如多个线程加载资源,需要等待多个线程全部加载完成再汇总处理。Thread.join() 就可以做这个事情。
假设有两个线程A、B,线程A执行过程中对线程B执行有依赖,具体为 A要将B的执行流程合并到自己的流程中,这就是线程合并。被动方线程B可以叫做被合并线程。
join() 方法是Thread类的一个实例方法,有三个重载版本:
//把当前线程变为TIMED_WAITING,直到被合并线程执行结束
public final void join() throws InterruptedException;
//把当前线程变为TIMED_WAITING,直到被合并线程执行结束,或者等待被合并线程执行millis的时间
public final synchronized void join(long millis) throws InterruptedException ;
//把当前线程变为TIMED_WAITING,直到被合并线程执行结束,或者等待被合并线程执行millis+nanos的时间
public final synchronized void join(long millis, int nanos) throws InterruptedException ;
join() 方法是实例方法,需要使用被合并线程的句柄去调用,如threadb.join()。执行threadb.join() 这行代码的当前线程为合并线程,进入TIMED_WAITING等待状态,让出CUP。
如果设置了被合并线程的执行时间millis(或者millis+nanos),并不能保证当前线程一定会在millis时间之后变为RUNNABLE。
如果主动方合并线程在等待时被中断,就会抛出InterruptedException异常。
上述逻辑可以参照下方示意图:
Thread.yield()
让步
yield() 方法是Thread类提供的一个静态方法,它可以让当前正在执行的线程暂停,但它不会阻塞该线程,只是让线程转入就绪状态。yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次。
操作系统是为每个线程分配一个时间片来占有CPU的,正常情况下一个线程把分配给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度。当一个线程调用yield方法时,实际就是在暗示线程调度器当前线程请求让出自己的CPU使用。当前线程让出CPU使用权后,处于就绪状态,线程调度器会从线程就绪队列中获取一个线程优先级最高的线程给与CPU使用权(也有可能会调度到刚刚让出CPU的哪个线程)。
✨守护线程
Java中的线程分为两类:用户线程和守护线程。
在JVM 启动时会调用 main 函数,main函数所在的线程就是一个用户线程。
JVM同时还启动了很多守护线程,比如垃圾回收线程。
守护线程和用户线程的区别:
- 从使用上来说,守护线程在启动之前,要通过setDaemon(true)方法进行设置。
- 从功能上来说,守护线程不影响JVM进程的退出;而用户线程在没有执行完之前,JVM进程不会退出。
守护线程的应用场景:
- 比如在JVM中垃圾回收器就采用了守护线程,如果一个程序中没有用户线线程,就不会产生垃圾,垃圾回收器就不需要工作了。
- 在一些中间件的心跳检测、事件监听等涉及定时异步执行的场景也可以使用守护线程。因为这些都是在后台不断执行的任务,当进程退出时,这些任务也不需要存在。守护线程可以自动结束自己的生命周期。
对于一些后台任务,不希望阻止JVM进程结束时,可以采用守护线程。
在Java中,线程的状态是自动继承的,如果一个线程是用户线程,它的子线程默认是用户线程;如果一个线程是守护线程,它创建的子线程默认是守护线程。
thread.setDaemon(true)必须在start() 方法启动之前调用。
✨线程上下文切换
多线程编程中,线程的个数一般都大于CPU个数,而每个CPU同一时刻只能被一个线程使用。
为了让用户感觉多个线程是在同时执行,CPU资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用CPU执行任务。当前线程使用完时间片后,就会处于就绪状态并让出CPU,让其他线程占用,这就是上下文切换。
有个问题,让出CPU的线程等下次轮到自己占有CPU时,如何知道自己之前运行到哪里来?
所以在切换线程上下文时,需要保存当前线程的执行现场,当再次执行时根据保存的执行现场信息来恢复。
由于线程上线文切换时,需要保存上一个线程的私有数据、寄存器等数据,这个过程同样会占据CPU资源,当上下文切换过于频繁时,会使得CPU不断进行切换,无法真正去做计算,最终导致性能下降。
如何减少上下文切换?
- 减少线程。
同一时刻能够运行的线程数是由CPU核心数决定的,创建过多线程,就会造成CPU时间片的频繁切换。
- 采用无锁设计解决线程竞争问题。
比如在同步锁场景中,如果存在多线程竞争,那么没抢到锁的线程会被阻塞,这个过程涉及系统调用,而系统调用会产生从用户态到内核态的切换,这个切换过程需要保存上下文信息对性能的影响。如果采用无锁设计就能解决这些问题。
- 采用CAS做自旋操作。
它是一种无锁化的编程思想,原理是通过循环重试的方式避免线程的阻塞导致的上下文切换。
以上就是本篇全部内容,感谢您的认真阅读。