简介
Thread类可以用来创建多线程,并指定要多线程执行的业务逻辑(重写run方法)
创建多线程
启动多线程总共有3步:
- 创建线程(new Thread类及其子类)
- 指定任务(Thread的run方法或Runnable的run方法中指定要执行的任务)
- 启动线程(Thread的start方法)
线程的生命周期
- 新建(New)/初始状态状态:新建一个线程对象。
- 就绪/可运行(Runnable)状态:线程对象创建后,其他线程(一般是main方法的主线程)调用该对象的start方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
- 运行状态(Running):就绪状态的线程获得了CPU使用权,并执行程序代码。
- 阻塞状态(Blocked):线程因为某种原因放弃CPU的使用权,暂时停止运行。直到线程再次进入就绪状态,才有机会转到运行状态。阻塞的情况分为三种:
- 等待阻塞:运行的线程执行wait方法,JVM会把该线程放入等待池中。wait会释放持有的锁。
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
- 其他阻塞:运行的线程执行sleep或join或yield方法,或者发出了I/O请求,JVM会把该线程置为阻塞状态。当sleep的状态超时、join等待线程终止或超时、以及I/O处理完毕时,线程重新转入就绪状态。这种方式不会释放持有的锁。
- 死亡状态(Dead):线程执行完成,或者因为异常退出run方法,该线程结束生命周期。
start方法与run方法
run()
方法是Thread类(或子类)中的一个普通方法,如果直接调用run方法,只是用主线程去执行了普通方法start()
方法是native方法,调用之后会启动1个线程(让线程变成就绪状态,等待 CPU 调度后执行),该线程会去执行run方法。
所以启动多线程一定要调用start方法,如果直接调用run方法,只是用主线程去执行了普通的run方法。
调用run方法和start方法对比
java
public class PrintStoryExtends extends Thread{
String text;
long interval;
public PrintStoryExtends(String text, long interval){
this.text = text;
this.interval = interval;
}
public void run(){
try{
System.out.println("执行这段代码的线程名字是:" + Thread.currentThread().getName());
printStory(text, interval);
System.out.println(Thread.currentThread().getName() + "执行结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
protected void printStory(String text, long interval) throws InterruptedException {
for (char ch : text.toCharArray()){
Thread.sleep(interval);
System.out.print(ch);
}
System.out.println("----");
}
}
//main函数所在类
public class PrintStoryAppMain {
public static final String text = "今天又是阳光明媚的一天,某小胖7点钟起床洗漱完成,奔向地铁站去往浦东图书馆,在龙阳路换乘时,发现今天有好多人啊," +
"还都是背着小书包的,某小胖心里想:从地铁站就开始卷了嘛,等下一下车要奔跑哇。";
public static void main(String[] args) {
System.out.println("程序执行开始,执行线程的名字叫做:" + Thread.currentThread().getName());
for (int i = 1; i <= 2; i++){
Thread thread = new PrintStoryExtends(text, 200 * i);
thread.start();;
}
System.out.println("启动线程结束,名字叫做:" + Thread.currentThread().getName());
}
}
在main方法中new PrintStoryExtends之后,调用start方法,会创建一个线程(一定要调用start方法,才会创建线程,如果直接调用run方法,只是用主线程去执行了普通的run方法),该线程执行run方法中的业务逻辑。
- 直接thread.run()的执行结果
css
程序执行开始,执行线程的名字叫做:main
执行这段代码的线程名字是:main
今天又是阳光明媚的一天,某小胖7点钟起床洗漱完成,奔向地铁站去往浦东图书馆,在龙阳路换乘时,发现今天有好多人啊,还都是背着小书包的,某小胖心里想:从地铁站就开始卷了嘛,等下一下车要奔跑哇。----
main执行结束
执行这段代码的线程名字是:main
今天又是阳光明媚的一天,某小胖7点钟起床洗漱完成,奔向地铁站去往浦东图书馆,在龙阳路换乘时,发现今天有好多人啊,还都是背着小书包的,某小胖心里想:从地铁站就开始卷了嘛,等下一下车要奔跑哇。----
main执行结束
启动线程结束,名字叫做:main
Process finished with exit code 0
- 调用thread.start()的执行结果
css
程序执行开始,执行线程的名字叫做:main
启动线程结束,名字叫做:main
执行这段代码的线程名字是:Thread-0
执行这段代码的线程名字是:Thread-1
今今天又天是阳又光明是媚的阳一天光,某明小胖媚7点的钟起一床洗天漱完,成,某奔向小地铁胖站去7往浦点东图钟书馆起,床在龙洗阳路漱换乘完时,成发现,今天奔有好向多人地啊,铁还都站是背去着小往书包浦的,东某小图胖心书里想馆:从,地铁在站就龙开始阳卷了路嘛,换等下乘一下时车要,奔跑发哇现。----
Thread-0执行结束
今天有好多人啊,还都是背着小书包的,某小胖心里想:从地铁站就开始卷了嘛,等下一下车要奔跑哇。----
Thread-1执行结束
start方法详细分析
在Thread 类的顶部,有个native的registerNatives本地方法,该方法主要的作用就是注册一些本地方法供 Thread 类使用,如start0(),stop0() 等等,可以说,所有操作本地线程的本地方法都是由它注册的 . 这个方法放在一个static语句块中,这就表明,当该类被加载到JVM中的时候,它就会被调用,进而注册相应的本地方法。
csharp
private static native void registerNatives();
static {
registerNatives();
}
}
本地方法registerNatives是定义在Thread.c文件中的。Thread.c是个很小的文件,定义了各个操作系统平台都要用到的关于线程的公用数据和操作
arduino
JNIEXPORT void JNICALL
Java_Java_lang_Thread_registerNatives (JNIEnv *env, jclass cls){
(*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}
static JNINativeMethod methods[] = {
......
{"start0", "()V",(void *)&JVM_StartThread},
{"stop0", "(" OBJ ")V", (void *)&JVM_StopThread},
......
};
到此,可以容易的看出Java线程调用start的方法,实际上会调用到JVM_StartThread方法,那这个方法又是怎样的逻辑呢。实际上,我们需要的是(或者说 Java 表现行为)该方法最终要调用Java线程的run方法,事实的确如此。在jvm.cpp中,有如下代码段:
scss
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
......
native_thread = new JavaThread(&thread_entry, sz);
......
这里JVM_ENTRY是一个宏,用来定义JVM_StartThread 函数,可以看到函数内创建了真正的平台相关的本地线程,其线程函数是 thread_entry,代码如下所示。
scss
static void thread_entry(JavaThread* thread, TRAPS) {
HandleMark hm(THREAD);
Handle obj(THREAD, thread->threadObj());
JavaValue result(T_VOID);
JavaCalls::call_virtual(&result,obj,
KlassHandle(THREAD,SystemDictionary::Thread_klass()),
vmSymbolHandles::run_method_name(),
vmSymbolHandles::void_method_signature(),THREAD);
}
可以看到调用了vmSymbolHandles::run_method_name方法,这是在 vmSymbols.hpp用宏定义的:
kotlin
class vmSymbolHandles: AllStatic {
... template(run_method_name,"run")
...
}
至于run_method_name是如何声明定义的,因为涉及到很繁琐的代码细节,本文不做赘述。感兴趣的读者可以自行查看JVM的源代码。
综上所述,Java线程的创建调用过程如上图所示,首先 , Java线程的start方法会创建一个本地线程(通过调用JVM_StartThread),该线程的线程函数是定义在jvm.cpp中的thread_entry,由其再进一步调用run方法。
可以看到Java线程的run方法和普通方法其实没有本质区别,直接调用run方法不会报错,但是却是在当前线程执行,而不会创建一个新的线程。
yield()方法(没在业务代码中见到过)
源码
java
public static native void yield();
作用
Thread类的静态方法,调用后能让当前线程释放CPU的时间片,进入就绪状态(而非阻塞状态),让与该线程有同等优先级的其他线等待线程获取CPU执行权,也有可能当前线程又重新获得CPU时间片进入运行状态,调用yield方法不会使线程释放它所持有的对象的同步锁。
代码实战
- 线程的优先级相同
csharp
public class YieldMethod {
public static void main(String[] args) {
Runnable runnable = () -> {
for (int i = 0; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "_____" + i);
if (i % 20 == 0) {
// 每执行完20个让出CPU
Thread.yield();
}
}
};
new Thread(runnable, "栈长").start();
new Thread(runnable, "小蜜").start();
}
}
上述代码有以下两种可能的结果:
结果1:栈长让出了 CPU 资源,小蜜成功上位。
lua
栈长-----29
栈长-----30
小蜜-----26
栈长-----31
结果2:栈长让出了 CPU 资源,栈长继续运行。
lua
栈长-----28
栈长-----29
栈长-----30
栈长-----31
- 线程的优先级不同
java
public class YieldMethod {
public static void main(String[] args) {
Runnable runnable = () -> {
for (int i = 0; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "_____" + i);
if (i % 20 == 0) {
// 每执行完20个让出CPU
Thread.yield();
}
}
};
Thread thread1 = new Thread(runnable, "栈长");
thread1.setPriority(Thread.MIN_PRIORITY);
Thread thread2 = new Thread(runnable, "小蜜");
thread2.setPriority(Thread.MAX_PRIORITY);
thread1.start();
thread2.start();
}
}
因为给小蜜加了最高优先权,栈长加了最低优先权,即使栈长先启动,那小蜜还是有很大的概率比栈长先会输出完的。 实际执行结果如下:
java
栈长_____0
小蜜_____0
栈长_____1
栈长_____2
栈长_____3
栈长_____4
栈长_____5
小蜜_____1
...
小蜜_____22
yield 和 sleep 的异同
1)yield, sleep 都能暂停当前线程,sleep 可以指定具体休眠的时间,而 yield 则依赖 CPU 的时间片划分。
2)yield, sleep 两个在暂停过程中,如已经持有锁,则都不会释放锁资源。
3)yield 不能被中断,而 sleep 则可以接受中断。
sleep()方法
中断方法
stop()方法已经不推荐使用了。
void interrupt()
源码
scss
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
- 向线程发送中断请求,将线程的中断状态置为true,但不会中断(停止)这个正在运行的线程,类似"set"。
- 当线程处于阻塞状态(比如调用了sleep方法),此时再调用该线程的interrupt()方法,会抛出InterruptedException中断异常,不会将线程的中断状态再置为true。
boolean isInterrupted
源码
java
public boolean isInterrupted() {
return isInterrupted(false);
}
/**
* Tests if some Thread has been interrupted. The interrupted state
* is reset or not based on the value of ClearInterrupted that is
* passed.
*/
private native boolean isInterrupted(boolean ClearInterrupted);
- 调用之后,返回线程的中断状态,调用这个方法不会改变线程的中断状态,类似"get"。
static boolean interrupted()
源码
java
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
private native boolean isInterrupted(boolean ClearInterrupted);
- 调用之后,返回线程的中断状态,并将线程的中断状态设置为false,类似:set+get
总结
以上三个方法不能真正将一个线程中断,只是改变线程的中断状态的值,改变状态之后,再由实际代码判断是否要停止当前线程。因此这个中断状态可以被用来当做标志位(开关),我们可以通过if或者while这种判断语句来决定线程中断状态置为true后该怎么处理。
上述三种中断方法的实际用处
比如:
java
public class MyThread extends Thread {
public void run() {
while(!this.isInterrupted()) {
System.out.println("当前线程没有被中断,继续干活");
}
System.out.println("当前线程被中断,停止干活");
}
}
public class MyThreadMain {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.interrupt();
}
}
线程启动,5s之后,主线程(执行main方法的线程)将线程t的中断状态设为true,线程没有立刻停止?过一会停止之后打印:。。。
执行结果
当前线程没有被中断,继续干活
当前线程没有被中断,继续干活
当前线程没有被中断,继续干活
当前线程没有被中断,继续干活
当前线程没有被中断,继续干活
当前线程没有被中断,继续干活
当前线程没有被中断,继续干活
当前线程没有被中断,继续干活
当前线程没有被中断,继续干活
当前线程被中断,停止干活
当Mythread线程t处于休眠状态的时候,主线程调用interrupt方法时,会抛出InterruptedException。举例如下:
java
public class MyThread extends Thread{
@Override
public void run() {
while (!this.isInterrupted())
{
System.out.println("当前线程没有被中断,继续干活");
try {
Thread.sleep(1000);
} catch (InterruptedException e)
{
this.interrupt();
}
}
System.out.println("当前线程被被中断,停止干活");
}
}
等待线程join()
线程是一个随机调度的过程,等待线程就是控制两个线程的结束顺序,因为线程调度是随机的,我们无法控制线程的开始顺序,但是能通过方法控制线程的结束顺序。
通常是主线程里调用该方法,t.join()。主线程等待t线程执行完成之后再执行。
代码实战
- 主线程里没有调用join方法的情况
java
public class MyThread extends Thread {
public void run() {
String currentThreadName = Thread.currentThread().getName();
System.out.println(currentThreadName + "开始运行");
for (int i = 0; i < 20; i++) {
System.out.println(currentThreadName +"正在运行" + String.valueOf(i));
}
}
}
public class MyThreadMainAsync {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
System.out.println("主线程开始执行");
for (int i = 0; i < 20; i++) {
System.out.println("主线程正在运行" + String.valueOf(i));
}
}
}
执行结果:
java
主线程开始执行
Thread-0开始运行
主线程正在运行0
主线程正在运行1
主线程正在运行2
主线程正在运行3
Thread-0正在运行0
主线程正在运行4
Thread-0正在运行1
主线程正在运行5
Thread-0正在运行2
...
Thread-0正在运行10
主线程正在运行6
Thread-0正在运行11
...
Thread-0正在运行19
主线程正在运行7
...
主线程正在运行19
- 主线程里调用了join方法
修改上述的MyThreadMain方法即可
java
public class MyThreadMain {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程开始执行");
for (int i = 0; i < 20; i++) {
System.out.println("主线程正在运行 " + String.valueOf(i));
}
}
}
运行结果:
java
Thread-0开始运行
...
Thread-0正在运行19
主线程开始执行
主线程正在运行 0
...
主线程正在运行 19
设为守护线程:setDaemon()方法
Java中的线程分为前台和后台线程,代码里手动创建的线程,Main线程都是前台的线程,其它的JVM自带的线程都是后台的线程,也就是守护线程。
也可以通过setDaemon来手动设置成后台线程
前台线程和后台线程的特点:
前台线程会阻止进程的结束,前台线程的工作没有做完,进程是不能结束的,后台线程不会阻止进程结束,后台工作没做完,进程是可以结束的
java
public class MyThreadMain {
public static void main(String[] args) {
Thread t = new MyThread();
t.setDaemon(true);
t.start();
System.out.println("主线程开始执行");
for (int i = 0; i < 20; i++) {
System.out.println("主线程正在运行 " + String.valueOf(i));
}
}
}
执行结果:
vbnet
Thread-0正在运行10
Thread-0正在运行11
Thread-0正在运行12
Thread-0正在运行13
Thread-0正在运行14
Process finished with exit code 0
可以看到:线程t还没执行结束,进程就结束了。