线程是一个动态执行的过程,它也有一个从产生到死亡的过程
1.线程终止状态
- 使用退出标志,使线程正常退出,也就是当 run() 方法完成后线程中止。
- 使用 stop() 方法强行终止线程,但是不推荐使用这个方法,该方法已被弃用。
- 使用 interrupt 方法中断线程。
使用标记终止线程:
public class ServerThread extends Thread {
//volatile修饰符用来保证其它线程读取的总是该变量的最新的值
public volatile boolean flag = false;
@Override
public void run() {
ServerSocket serverSocket = new ServerSocket(8080);
while(!flag){
serverSocket.accept(); //阻塞等待客户端消息
...
}
}
public static void main(String[] args) {
ServerThread t = new ServerThread();
t.start();
...
t.flag = true; //修改标志位,退出线程
}
}
使用 interrupt() 中断线程:
现在我们知道了使用 stop() 方式停止线程是非常不安全的方式,那么我们应该使用什么方法来停止线程呢?答案就是使用 interrupt() 方法来中断线程。
需要明确的一点的是:interrupt() 方法并不像在 for 循环语句中使用 break 语句那样干脆,马上就停止循环。调用 interrupt() 方法仅仅是在当前线程中打一个停止的标记,并不是真的停止线程。
也就是说,线程中断并不会立即终止线程,而是通知目标线程,有人希望你终止。至于目标线程收到通知后会如何处理,则完全由目标线程自行决定。这一点很重要,如果中断后,线程立即无条件退出,那么我们又会遇到 stop() 方法的老问题。
事实上,如果一个线程不能被 interrupt,那么 stop 方法也不会起作用。
public class Main extends Thread{
@Override
public void run() {
super.run();
for(int i = 0; i <= 1000; i++) {
if(Thread.currentThread().isInterrupted()) {
break;
}else {
System.out.println("i=" + i);
}
}
}
public static void main(String[] args) throws Exception {
Main main=new Main();
main.start();
Thread.sleep(1); //暂停sleep 模拟网络延迟
main.interrupt();
}
2.线程礼让、插队 (yield,join)
由运行态到就绪态,停止一下后再由就绪态到运行态
- 暂停当前正在执行的线程对象,并执行其他线程。
- 意思就是调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。
- 但是yield不能立刻交出CPU,会出现同一个线程一直执行的情况,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。
- 注意调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的
public class TestThread16 {
public static void main(String[] args) {
MyThreadTest mt = new MyThreadTest();
new Thread(mt,"1").start();
new Thread(mt,"2").start();
new Thread(mt,"3").start();
}
}
class MyThreadTest implements Runnable {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
Thread.yield();
System.out.println("当前线程:" + Thread.currentThread().getName()+" ,i = "
+i);
}
}
}
无序
join运行态到阻塞态(释放锁),线程执行完毕后阻塞态到就绪态
等待该线程终止。意思就是如果在主线程中调用该方法时就会让主线程休眠,让调用该方法的线程run方法先执行完毕之后在开始执行主线程,不使用join方法的话主和子一起执行,main开始,main结束,子线程方法。本质是对Object类的wait方法做了包装。
public class Main extends Thread{
public String name;
private static int ticket = 99;
public Main(String name){
this.name=name;
}
@Override
public void run() {
while (true) {
if (ticket < 0) {
break;
}
System.out.println(this.name + "剩余票数:" + ticket--);
}
}
public static void main(String[] args) throws Exception {
Thread main=new Main("wwm");
main.start();
for(int i=0;i<100;i++){
System.out.println("主线程"+i);
if(ticket ==50){
main.join();
System.out.println("线程阻塞");
}
}
}
}
3.线程的优先级
- 优先级可以用从1到10的范围指定。10表示最高优先级,1表示最低优先级,5是普通优先级。
- 记住在线程开始方法被调用之前,线程的优先级应该被设定。
Java线程的优先级是一个整数,其取值范围是1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。
Thread源代码里对NORM_PRIORITY (数值为5) 的注释是“线程默认的优先级”
public static final int MIN_PRIORITY = 1;
public static final int NORM_PRIORITY = 5;
public static final int MAX_PRIORITY = 10;
其实不然。默认的优先级是父线程的优先级。在init方法里,
Thread parent = currentThread();
this.priority = parent.getPriority();
或许这么解释是因为Java程序的主线程(main方法)的优先级默认是为NORM_PRIORITY,这样不主动设定优先级的,后续创建的线程的优先级也都是NORM_PRIORITY了。
public static void main(String[] args) {
System.out.println(Thread.currentThread().getPriority());
}
其执行结果是5。
设置优先级
可以通过setPriority方法(final的,不能被子类重载)更改优先级。优先级不能超出1-10的取值范围,否则抛出IllegalArgumentException。另外如果该线程已经属于一个线程组(ThreadGroup),该线程的优先级不能超过该线程组的优先级:
public final void setPriority(int i)
{
checkAccess();
if(i > 10 || i < 1)
throw new IllegalArgumentException();
ThreadGroup threadgroup;
if((threadgroup = getThreadGroup()) != null)
{
if(i > threadgroup.getMaxPriority())
i = threadgroup.getMaxPriority();
setPriority0(priority = i);
}
}
其中setPriority0是一个本地方法。
private native void setPriority0(int i);
4.守护线程
在Java中有两类线程,分别是User Thread(用户线程)和Daemon Thread(守护线程) 。
用户线程很好理解,我们日常开发中编写的业务逻辑代码,运行起来都是一个个用户线程。而守护线程相对来说则要特别理解一下。
什么是守护线程:
所谓的守护线程,指的是程序运行时在后台提供的一种通用服务的线程。比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。
守护线程的使用与注意事项:
守护线程并非只有虚拟机内部可以提供,用户也可以手动将一个用户线程设定/转换为守护线程。
在Thread类中提供了一个setDaemon(true)方法来将一个普通的线程(用户线程)设置为守护线程。
public final void setDaemon(boolean on);
在使用的过程中,有几点需要注意:
1.thread.setDaemon(true)必须在thread.start()之前设置,否则会抛出一个IllegalThreadStateException异常。这也就意味着不能把正在运行的常规线程设置为守护线程。 这点与操作系统中的守护进程有着明显的区别,守护进程是创建后,让进程摆脱原会话的控制+让进程摆脱原进程组的控制+让进程摆脱原控制终端的控制;所以说寄托于虚拟机的语言机制跟系统级语言有着本质上面的区别。
2.在Daemon线程中产生的新线程也是Daemon的。关于这一点又是与操作系统中的守护进程有着本质的区别:守护进程fork()出来的子进程不再是守护进程,尽管它把父进程的进程相关信息复制过去了,但是子进程的进程的父进程不是init进程,所谓的守护进程本质上说就是,当父进程挂掉,init就会收养该进程,然后文件0、1和2都是/dev/null,当前目录到/。
3.不是所有的应用都可以分配给Daemon线程来进行服务的,比如读写操作或者计算逻辑。因为这种应用可能在Daemon Thread还没来得及进行操作时,虚拟机已经退出了。这也就意味着,守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
下面以一个完成文件输出的守护线程任务作为例子:
import java.io.*;
class TestRunnable implements Runnable {
public void run(){
try {
Thread.sleep(1000); // 守护线程阻塞1秒后运行
File f = new File("daemon.txt");
FileOutputStream os = new FileOutputStream(f,true);
os.write("daemon".getBytes());
} catch(IOException e1) {
e1.printStackTrace();
} catch(InterruptedException e2) {
e2.printStackTrace();
}
}
}
public class TestDemo2 {
public static void main(String[] args) throws InterruptedException {
Runnable tr = new TestRunnable();
Thread thread = new Thread(tr);
thread.setDaemon(true); // 设置守护线程(必须在thread.start()之前)
thread.start(); // 开始执行分进程
}
}
上面这段代码的运行结果是文件daemon.txt中没有daemon字符串。
但是如果把thread.setDaemon(true);这行代码注释掉,文件daemon.txt是可以被写入daemon字符串的,因为这个时候这个线程就是普通的用户线程了。
简单理解就是,JRE判断程序是否执行结束的标准是所有的前台线程(用户线程)执行完毕了,而不管后台线程(守护线程)的状态。
守护线程的应用场景
前面说了那么多,那么Daemon Thread的实际应用在那里呢?举个例子,Web服务器中的Servlet,在容器启动时,后台都会初始化一个服务线程,即调度线程,负责处理http请求,然后每个请求过来,调度线程就会从线程池中取出一个工作者线程来处理该请求,从而实现并发控制的目的。也就是说,一个实际应用在Java的线程池中的调度线程。
那么守护线程的作用是什么? (聊天室,聊天程序退出,聊天框也会随之退出)
举例, GC垃圾回收线程:就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
应用场景:(1)来为其它线程提供服务支持的情况;(2) 或者在任何情况下,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用;反之,如果一个正在执行某个操作的线程必须要正确地关闭掉否则就会出现不好的后果的话,那么这个线程就不能是守护线程,而是用户线程。通常都是些关键的事务,比方说,数据库录入或者更新,这些操作都是不能中断的。
5.线程中的常用其他方法
isAlive() 判断线程是否活着,及线程是否还未终止(默认true)
getName() 获取线程的名字
setName() 设置线程的名字
currentThread() 取得当前正在运行的线程对象
总结
从我的理解,守护线程就是用来告诉JVM,我的这个线程是一个低级别的线程,不需要等待它运行完才退出,让JVM喜欢什么时候退出就退出,不用管这个线程。
在日常的业务相关的CRUD开发中,其实并不会关注到守护线程这个概念,也几乎不会用上。
但是如果要往更高的地方走的话,这些深层次的概念还是要了解一下的,比如一些框架的底层实现。