1. 多线程的创建
创建线程只有一种方式那就是构造Thread
类,而实现线程的执行单元则有两种方式,第一种是重写Thread
的run
方法,第二种是实现Runnable
接口的run
方法,并且将Runnable
实例用作构造Thread
的参数。
重写Thread
类的run
方法和实现Runnable
接口的run
方法还有一个很重要的不同,那就是Thread
类的run
方法是不能共享的,也就是 A 线程不能把 B 线程的run
方法当作自己的执行单元,而使用Runnable
接口则很容易就能实现这一点,使用同一个Runnable
的实例构造不同的Thread
实例。
2. 线程的生命周期
- NEW
- RUNNABLE
- RUNNING
- BLOCKED
- TERMINATED
NEW: 创建状态
- 使用
new
关键字创建的对象
RUNNABLE:可执行状态
- 具备执行的资格,但并没有真正的执行起来而是在等待CPU的调度
- NEW状态通过
start
方法进入RUNNABLE状态 - RUNNABLE的线程只能意外终止或者进入RUNNING状态
RUNNING:运行状态
- 可直接进入TERMINATED状态,比如调用了
stop
方法或者判断某个逻辑标识 - 进入BLOCKED状态,比如调用了
sleep
,或者wait
方法而加入了waitSet
中 - 进行某个阻塞的IO操作,比如因特网数据的读写而进入了BLOCKED状态
- 获取某个锁资源,从而加入到该锁的阻塞队列中而进入了BLOCKED状态
- 由于CPU的调度器轮询使该线程放弃执行,进入RUNNABLE状态
- 线程主动调用
yield
方法,放弃CPU执行权,进入RUNNABLE状态
BLOCKED:阻塞状态
- 直接进入TERMINATED状态,比如调用了
stop
方法或者意外死亡(JVM Crash) - 线程阻塞的操作结束,比如读取了想要的数据字节进入到RUNNABLE状态
- 线程完成了指定时间的休眠,进入到了RUNNABLE状态
Wait
中的线程被其他线程notify/notifyAll
唤醒,进入到RUNNABLE状态- 线程获取到了某个锁资源,进入RUNNABLE状态
- 线程在阻塞过程中被打断,比如其他线程调用了
interrupt
方法,进入RUNNABLE状态
TERMINATED:终止状态
一个线程的最终状态
- 线程运行正常结束,结束生命周期
- 线程运行出错意外结束
- JVM Crash,导致所有的线程都结束
3. Thread.run()
和 Thread.start()
方法的区别
Thread.run()
顺序执行,相当于执行run()
函数,并没有启动子线程,他只是main
线程的一个方法
Thread.start()
是启动子线程- 当线程运行结束之后,再执行
Thread.run()
方法,会直接返回
执行Thread.start()
,会抛出java.lang.IllegalThreadStateException
异常,说明线程状态是不可逆的
package com.example.demo.thread;
import java.text.SimpleDateFormat;
import java.util.concurrent.TimeUnit;
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(()->{
try {
System.out.println("Thread name: " + Thread.currentThread().getName() + "-thread1");
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread thread2 = new Thread(()->{
try {
System.out.println("Thread name: " + Thread.currentThread().getName() + "-thread2");
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("thread1 run method start time is " + dateFormat(System.currentTimeMillis()));
thread1.run();
System.out.println("thread1 run method end time is " + dateFormat(System.currentTimeMillis()));
System.out.println("thread2 run method start time is " + dateFormat(System.currentTimeMillis()));
thread2.run();
System.out.println("thread2 run method end time is " + dateFormat(System.currentTimeMillis()));
TimeUnit.SECONDS.sleep(20);
System.out.println("thread1 start method start time is " + dateFormat(System.currentTimeMillis()));
thread1.start();
System.out.println("thread2 start method start time is " + dateFormat(System.currentTimeMillis()));
thread2.start();
thread1.join();
System.out.println("thread1 start method end time is " + dateFormat(System.currentTimeMillis()));
thread2.join();
System.out.println("thread2 start method end time is " + dateFormat(System.currentTimeMillis()));
System.out.println("after thread terminal, thread1 run method start time is " + dateFormat(System.currentTimeMillis()));
thread1.run();
System.out.println("after thread terminal, thread1 run method end time is " + dateFormat(System.currentTimeMillis()));
System.out.println("after thread terminal, thread2 run method start time is " + dateFormat(System.currentTimeMillis()));
thread2.run();
System.out.println("after thread terminal, thread2 run method end time is " + dateFormat(System.currentTimeMillis()));
System.out.println("after thread terminal, thread1 start method start time is " + dateFormat(System.currentTimeMillis()));
thread1.start();
System.out.println("after thread terminal, thread1 end method start time is " + dateFormat(System.currentTimeMillis()));
}
private static String dateFormat(long time) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:mss");
return simpleDateFormat.format(time);
}
thread1 run method start time is 2019-12-10 18:16:49:1649
Thread name: main-thread1
thread1 run method end time is 2019-12-10 18:16:59:1659
thread2 run method start time is 2019-12-10 18:16:59:1659
Thread name: main-thread2
thread2 run method end time is 2019-12-10 18:17:09:1709
thread1 start method start time is 2019-12-10 18:17:29:1729
thread2 start method start time is 2019-12-10 18:17:29:1729
Thread name: Thread-0-thread1
Thread name: Thread-1-thread2
thread1 start method end time is 2019-12-10 18:17:39:1739
thread2 start method end time is 2019-12-10 18:17:39:1739
after thread terminal, thread1 run method start time is 2019-12-10 18:17:39:1739
after thread terminal, thread1 run method end time is 2019-12-10 18:17:39:1739
after thread terminal, thread2 run method start time is 2019-12-10 18:17:39:1739
after thread terminal, thread2 run method end time is 2019-12-10 18:17:39:1739
after thread terminal, thread1 start method start time is 2019-12-10 18:17:39:1739
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:708)
at com.example.demo.thread.ThreadDemo.main(ThreadDemo.java:56)
Process finished with exit code 1
4. Thread API
4.1 线程 sleep
public static native void sleep(long millis) throws InterruptedException
public static void sleep(long millis, int nanos) throws InterruptedException
sleep
方法会使当前线程进入指定毫秒数的休眠,暂停执行,虽然给定了一个休眠的时间,但是最终要以系统的定时器和调度器的精度为准,休眠有一个非常重要的特性,那就是其不会放弃monitor锁的所有权
推荐使用TimeUnit.SECONDS.sleep(10);
代替Thread.sleep(10000)
4.2 线程 yield
yield
方法属于一种启发式的方法,其会提醒调度器我愿意放弃当前的CPU资源,如果CPU的资源不紧张,则会忽略这种提醒。
调用yield
方法会使当前线程从 RUNNING 状态切换到 RUNNABLE 状态
yield
方法只是一个提示(hint),CPU 调度器并不会担保每次都能满足 yield
提示
区别:
sleep
会导致当前线程暂停指定的时间,没有CPU时间片的消耗yield
只是对CPU调度器的一个提示,如果CPU调度器没有忽略这个提示,它会导致线程上下文的切换
-sleep
会使线程短暂 block,会在给定的时间内释放CPU资源yield
会使RUNNING状态的线程进入RUNNABLE状态(如果CPU调度器没有忽略这个提示的话)sleep
几乎百分之百地完成了给定时间的休眠,而yield
的提示并不能一定担保。- 一个线程
sleep
另一个线程调用interrupt
会捕获到中断信号,而yield
则不会
4.3 设置线程优先级
public final void setPriority(int newPriority) //设定优先级
public final int getPriority() //获取优先级
设置线程的优先级是一个 hint 操作
- 对于 root 用户,它会 hint 操作系统你想要设置的优先级别,否则它会被忽略
- 如果 CPU比较忙,设置优先级可能会获得更多的CPU时间片,但是闲时优先级的高低几乎不会有任何作用
线程的优先级为 1 - 10
线程默认的优先级和它的父类保持一致,一般情况下都是5,因为main线程的优先级就是5
4.4 获取线程ID
public long getId() //获取线程的唯一ID
线程的ID在整个JVM进程中都是唯一的,并且是从0开始递增
4.5 获取当前线程
public static Thread currentThread()
用于返回当前执行线程的引用
Thread.currentThread().getName();
4.6 设置线程上下文类加载器
public ClassLoader getContextClassLoader() //获取线程上下文的类加载器
public void setContextClassLoader(ClassLoader cl) // 设置该线程的类加载器
破坏了双亲委派原则
4.7 线程 interrupt
public void interrupt()
public static boolean interrupted()
public boolean isInterrupted()
4.7.1 interrupt
以下方法的调用会使得当前线程进入阻塞状态,而调用当前线程的 interrupt
方法,就可以打断阻塞
Object
的wait
方法Object
的wait(long)
方法Object
的wait(long, int)
方法Thread
的sleep(long)
方法Thread
的sleep(long, int)
方法Thread
的join()
方法Thread
的join(long)
方法Thread
的join(long, int)
方法InterruptibleChannel
的io
操作Selector
的wakeup
方法- 其他方法
上述方法会使当前线程进入阻塞状态,若另外的一个线程调用被阻塞线程的 interrupt 方法,则会打断这种阻塞,因此这种方法有时会被称为可中断方法,记住,打断一个线程并不等于该线程的生命周期结束,仅仅是打断了当前线程的阻塞状态。
一旦线程在阻塞情况下被打断,都会抛出一个称为InterruptedException
的异常,这个异常就像一个 signal(信号)一样通知当前线程被打断了
4.7.2 isInterrupted
isInterrupted
是Thread
的一个成员方法,它主要判断当前线程是否被中断,该方法仅仅是对 interrupt
标识的一个判断,并不会影响标识发生任何改变
4.7.3 interrupted
interrupted
是一个静态方法,它也用于判断当前线程是否被中断,调用该方法会直接擦除掉线程的interrupt 标识,需要注意的是,如果当前线程被打断了,那么第一次调用 interrupted
方法会返回true
,并且立即擦除了 interrupt
标识,第二次包括以后的调用永远都会返回false
,除非在此期间线程又一次的被打断。
4.7.4 interrupt 注意事项
如果一个线程设置了 interrupt
标识,那么接下来的可中断方法会立刻中断
4.8 线程 join
与sleep
一样也是一个可中断的方法,也就是说,如果有其它线程执行了对当前线程的interrupt
操作,它也会捕获到中断信号,并且擦除线程的 interrupt
标识。
public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException
public final synchronized void join(long millis, int nanos) throws InterruptedException
join 某个线程 A,会使当前线程 B进入等待,直到线程 A结束生命周期,或者到达给定的时间,那么在此期间B线程是处于 BLOCKED 的,而不是A线程。
join 会使当前线程永远的等待下去,直到期间被另外的线程中断,或者join 的线程执行结束,当然也可以使用join的另外两个重载方法,指定毫秒数,在指定的时间到达之后,当前线程也会退出阻塞。
如果一个线程已经结束了生命周期,那么调用它的join方法的当前线程不会被阻塞,直接继续运行
package com.example.demo.thread;
import java.text.SimpleDateFormat;
import java.util.concurrent.TimeUnit;
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(()->{
try {
System.out.println("Thread name: " + Thread.currentThread().getName() + "-thread1 start");
TimeUnit.SECONDS.sleep(10);
System.out.println("thread end");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("start: " + dateFormat(System.currentTimeMillis()));
thread1.start();
TimeUnit.SECONDS.sleep(20);
System.out.println("join start: " + dateFormat(System.currentTimeMillis()));
thread1.join();
System.out.println("join end: " + dateFormat(System.currentTimeMillis()));
}
private static String dateFormat(long time) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:mss");
return simpleDateFormat.format(time);
}
}
//result
start: 2019-12-11 11:50:07:5007
Thread name: Thread-0-thread1 start
thread end
join start: 2019-12-11 11:50:27:5027
join end: 2019-12-11 11:50:27:5027
4.9 关闭一个线程
4.9.1 正常关闭
- 线程结束生命周期正常结束
- 捕获中断信号关闭线程:检查线程
interrupt
标识,或者线程中有可中断方法时捕获中断信号 - 使用
volatile
开关控制:因为interrupt
标识很有可能被擦除,或者逻辑单元中不会调用任何可中断方法,所以使用volatile
修饰的开关 flag 关闭线程也是一种常用的方法
4.9.2 异常退出
在一个线程的执行单元中,是不允许抛出checked
异常的,不论Thread
中的run
方法还是Runnable
中的run
方法,如果线程在运行过程中需要捕获checked
异常并且判断是否还有运行下去的必要,那么此时可以将checked
异常封装成unchecked
异常(RuntimeException
)抛出而结束线程的生命周期。
4.9.3 进程假死
假死就是进程虽然存在,但没有日志输出,程序不进行任何的作业,看起来就像死了一样,但事实上他是没有死的,程序之所以出现这样的情况,绝大部分的原因就是某个线程阻塞了,或者线程出现了死锁的情况。这时可以借助jstack、jconsole、jvisualvm
等工具来分析