[02] 线程的创建和常用方法
1、线程的创建
线程的创建可以通过两种方式,第一种是 Thread类,第二种是 Runnable接口:
- 继承 Thread 类,覆盖 run()
- 实现 Runnable 接口,实现 run()
然后线程的启用是通过 start() 方法,它会自动调用 run() 方法,如下例:
//继承Thread
public class MyThread extends Thread {
@Override
public void run() {
for(int i = 0; i < 100; i++) {
System.out.println("MyThread:" + i);
}
}
}
//实现Runnable
public class MyRunnableImpl implements Runnable {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("MyRunnableImpl:" + i);
}
}
}
//测试类
public class Test {
public static void main(String[] args) {
Thread thread1 = new MyThread();
Thread thread2 = new Thread(new MyRunnableImpl());
thread1.start();
thread2.start();
}
}
//输出结果示例
...
MyThread:23
MyThread:24
MyThread:25
MyRunnableImpl:0
MyThread:26
MyRunnableImpl:1
MyThread:27
MyRunnableImpl:2
...
40
1
//继承Thread
2
public class MyThread extends Thread {
3
4
public void run() {
5
for(int i = 0; i < 100; i++) {
6
System.out.println("MyThread:" + i);
7
}
8
}
9
}
10
11
//实现Runnable
12
public class MyRunnableImpl implements Runnable {
13
public void run() {
14
for (int i = 0; i < 100; i++) {
15
System.out.println("MyRunnableImpl:" + i);
16
}
17
}
18
}
19
20
//测试类
21
public class Test {
22
public static void main(String[] args) {
23
Thread thread1 = new MyThread();
24
Thread thread2 = new Thread(new MyRunnableImpl());
25
thread1.start();
26
thread2.start();
27
}
28
}
29
30
//输出结果示例
31
...
32
MyThread:23
33
MyThread:24
34
MyThread:25
35
MyRunnableImpl:0
36
MyThread:26
37
MyRunnableImpl:1
38
MyThread:27
39
MyRunnableImpl:2
40
...
可以看到,线程的运行是并行的,而不是先执行完整个 thread1 的 start() 再执行 thread2 的 start()
另外,Runnable 接口的存在主要是为了解决 Java 中不允许多继承的问题,所以我们往往通过实现 Runnable 接口,然后再将其封装到一个 Thread 类中使用(如上例 Thread thread2 = new Thread(new MyRunnableImpl()); )
事实上,我们也可以通过匿名内部类的方式,快速实现线程,如上可以修改为:
public class Test {
public static void main(String[] args) {
//Thread
Thread thread1 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("anonymous thread:" + i);
}
}
};
//Runnable
Thread thread2 = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("anonymous runnable:" + i);
}
}
});
thread1.start();
thread2.start();
}
}
x
1
public class Test {
2
public static void main(String[] args) {
3
//Thread
4
Thread thread1 = new Thread() {
5
6
public void run() {
7
for (int i = 0; i < 100; i++) {
8
System.out.println("anonymous thread:" + i);
9
}
10
}
11
};
12
//Runnable
13
Thread thread2 = new Thread(new Runnable() {
14
public void run() {
15
for (int i = 0; i < 100; i++) {
16
System.out.println("anonymous runnable:" + i);
17
}
18
}
19
});
20
21
thread1.start();
22
thread2.start();
23
}
24
}
2、线程的常用方法
2.1 线程的生命周期
要说到线程的常用方法,首先就要先了解一个线程的生命周期,如下图:
- 新建状态:使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程
- 就绪状态:当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中(排队状态),要等待JVM里线程调度器的调度
- 运行状态:如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态
- 阻塞状态:如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。阻塞又可以分为三种:
- 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态
- 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)
- 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态
- 死亡状态:一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态
2.2 线程的常用方法
2.2.1 start、run
基于线程的生命周期,那么 start() 和 run() 方法想必也就不用多说了:
- start() 启动线程,进入就绪状态(万事俱备,只欠CPU)
- run() 运行状态,执行操作
2.2.2 sleep
sleep() 用于使线程进入阻塞状态,强制当前正在执行的线程休眠,计时到后返回到就绪状态。它是Thread类的静态方法:
- Thread.sleep(long millis) 休眠 ${millis} 毫秒
- Thread.sleep(long millis, int nanos) 休眠 ${millis} 毫秒 + ${nanos} 纳秒
注意:休眠完成之后进入的是就绪状态,等待进入运行状态,这意味着 "休眠 --> 执行" 的过程会大于 sleep 方法设置的值,因为实际上还要算上就绪状态的排队时间,即真正过程是 "休眠 --> 就绪 --> 执行"
另外,sleep() 是 Thread 的静态方法,调用的是当前运行的线程,所以要保证某个线程的休眠,应将 sleep() 的调用放在线程的 run() 之中
2.2.3 yield
Thread.yield() 的作用是,暂停当前正在执行的线程,并让其回到就绪状态,以允许具有相同优先级的其他线程获得运行机会。但是,实际中无法保证 yield() 达到让步的目的,因为让步的线程在就绪状态仍可能被再次选中执行。
注意:yield() 只是将线程转到就绪状态,而不会进入阻塞状态。
2.2.4 join
join(long millis) 方法的作用是让某个线程 "霸占" 资源一段时间,如两个线程交互运行,如果 thread2.join(5000),那么 thread2 将强制霸占执行 5s,之后其他线程才有使用的机会。
2.2.5 setPriority
setPriority(int newPriority) 方法用于设置线程的优先级别,数值越高表示优先级越高。优先级的范围在 1 - 10 之间,默认为5
每个线程具有各自的优先级,线程的优先级表示该线程的重要性,如果有很多线程处于就绪状态,系统会根据优先级来决定优先使哪个线程进入运行状态。但这并不意味着低优先级的线程得不到运行,只是运行的几率比较小,如垃圾回收机制线程的优先级就比较低。另外:
- 线程优先级具有继承性,如A线程启动B线程,则B线程的优先级和A一样
- 线程优先级具有随机性,即线程优先级高的不一定每次都先执行
2.3 守护线程
多线程分类
- 用户线程:运行在前台,执行具体的任务,如程序的主线程,用户自己创建的线程
- 守护线程:运行在后台,为其他前台线程服务(守护)
它们的区别在于 "前台线程保证完成,守护线程不保证完成":
- 前台线程完成了之后,虚拟机执行的进程才会结束
- 进程结束之后(此时前台线程已经结束),也会结束后台线程,但是不论后台线程是否已经完成都会将其结束
守护线程的应用如:数据库连接池中的检测线程、JVM启动后的检测线程、垃圾回收线程等
setDaemon(true) 可以将线程设置为守护线程,必须在线程进入就绪状态之前,即必须在 start() 方法调用之前使用。