Java-Thread总结

说起线程,就会想到进程。进程是程序运行资源分配的最小单位。进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、 磁盘 IO 等,同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程 之间是相互独立的。进程是具有一定独立功能的程序关于某个数据集合上的一次 运行活动,进程是系统进行资源分配和调度的一个独立单位。

进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一 个进程。显然,程序是死的、静态的,进程是活的、动态的。进程可以分为系统进 程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就 是处于运行状态下的操作系统本身,用户进程就是所有由你启动的进程。

线程是 CPU 调度的最小单位,必须依赖于进程而存在

线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的、 能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中 必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其 他的线程共享进程所拥有的全部资源。

线程无处不在

任何一个程序都必须要创建线程,特别是 Java 不管任何程序都必须启动一个 main 函数的主线程; Java Web 开发里面的定时任务、定时器、JSP 和 Servlet、异 步消息处理机制,远程访问接口RM等,任何一个监听事件, onclick的触发事件等都 离不开线程和并发的知识。

Java中的线程

线程的创建

Java中创建线程有两种方法

  1. 继承Thread

    1
    2
    3
    4
    5
    6
    7
    public class UseThread extends Thread{
    @Override
    public void run() {
    super.run();
    System.out.println("I am extende Thread");
    }
    }
  2. 实现Runnable接口

    1
    2
    3
    4
    5
    6
    public class UseRunnable implements Runnable{
    @Override
    public void run() {
    System.out.println("I am implements Runnable");
    }
    }

线程的调用执行

1
2
3
4
5
6
7
8
public static void main(String[] args) throws InterruptedException, ExecutionException {
UseThread useThread = new UseThread();
useThread.start();

UseRunnable useRunnable = new UseRunnable();
new Thread(useRunnable).start();

}

线程的状态

线程状态流转示意

image-20210620231638101

如何终止线程?

查看Thread的API,发现有stop()方法,那stop可以终止线程吗?看注解标记,已经被废弃了。

上代码看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main(String[] args) throws Exception{
Thread thread1 = new Thread(){
@Override
public void run() {
super.run();
try {
while (true){
System.out.println("running...sleep before");
Thread.sleep(50);
System.out.println("running...sleep after");
}
}catch (InterruptedException exception){
exception.printStackTrace();
}
}
};
thread1.setName("A");
thread1.start();

Thread.sleep(1000);

thread1.stop();
System.out.println("stop...");
}

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
....
running...sleep before
running...sleep after
running...sleep before
running...sleep after
running...sleep before
running...sleep after
running...sleep before
running...sleep after
running...sleep before
running...sleep after
running...sleep before
running...sleep after
running...sleep before
stop...

根据实测,线程的执行确实被中断了,但是仔细查看输出,发现终止时只执行了 running…sleep before 也就是说sleep后面的代码并未执行,直接被暴力终止了。并未给用户留下收尾处理的机会。这也是为什么在Java的API中被标记废弃的原因,过于暴力的终止可能会造成一些不可预知的错误。

当stop被调用后,可能会发生以下情况:

  1. 直接停掉run中的执行,包括在catch或finally语句中,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭。
  2. 会立即释放该线程所持有的所有的锁,导致数据得不到同步的处理,出现数据不一致的问题。

那么我们改如何优雅的停掉线程执行呢?继续查Thread的API,发现有个interrupt()方法,看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void main(String[] args) throws Exception{
Thread thread1 = new Thread(){
@Override
public void run() {
super.run();
while (!isInterrupted()){
try {
System.out.println("running...sleep before");
Thread.sleep(50);
System.out.println("running...sleep after");
}catch (InterruptedException exception){
exception.printStackTrace();
}
}

}
};
thread1.setName("A");
thread1.start();

Thread.sleep(1000);

thread1.interrupt();
System.out.println("interrupt...");
}

一运行发现,翻车了。。。。怎么停不下来呢?

原因就在于调用interrupt后会抛出InterruptedException,在catch InterruptedException异常时将interrupt标识位重置为false了,所以while一直满足执行。

所以,还需要在catch中增加 interrupt()调用。

1
2
3
4
5
6
7
8
9
10
while (!isInterrupted()){
try {
System.out.println("running...sleep before");
Thread.sleep(50);
System.out.println("running...sleep after");
}catch (InterruptedException exception){
exception.printStackTrace();
interrupt();
}
}

Thread中的主要方法

  1. suspend()和resume()

    这两个方法,一个是挂起,一个是恢复,通常要成对儿出现,不然很容易发生死锁,因为suspend方法并不会释放锁,如果使用suspend的目标线程对一个重要的系统资源持有锁,那么没任何线程可以使用这个资源直到要suspend的目标线程被resumed,如果一个线程在resume目标线程之前尝试持有这个重要的系统资源锁再去resume目标线程,这两条线程就相互死锁了,也就冻结线程。

  2. yield()

    yield 即 “谦让”,也是 Thread 类的方法。它让掉当前线程 CPU 的时间片,使正在运行中的线程重新变成就绪状态,并重新竞争 CPU 的调度权。它可能会获取到,也有可能被其他线程获取到。

    yield和sleep的不同:

    1)yield, sleep 都能暂停当前线程,sleep 可以指定具体休眠的时间,而 yield 则依赖 CPU 的时间片划分。

    2)yield, sleep 两个在暂停过程中,如已经持有锁,则都不会释放锁资源。

    3)yield 不能被中断,而 sleep 则可以接受中断。

  3. join()

    join()方法是Thread类中的一个方法,该方法的定义是等待该线程终止。其实就是join()方法将挂起调用线程的执行,直到被调用的对象完成它的执行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public static void main(String[] args) throws Exception{
    Thread thread1 = new Thread(){
    @Override
    public void run() {
    super.run();
    int num = 0;
    try {
    while (num < 10){
    Thread.sleep(20);
    System.out.println("child thread running...num = " + num++);
    }
    System.out.println("child thread finish-----------------------------------------");
    }catch (InterruptedException exception){
    exception.printStackTrace();
    }
    }
    };
    thread1.setName("A");
    thread1.start();
    thread1.join();
    int num = 0;
    while (num < 5){
    Thread.sleep(20);
    System.out.println("main running...num = " + num++);
    }
    System.out.println("main finish-----------------------------------------");
    }

    注释掉第20行时的输出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    main running...num = 0
    child thread running...num = 0
    main running...num = 1
    child thread running...num = 1
    main running...num = 2
    child thread running...num = 2
    child thread running...num = 3
    main running...num = 3
    main running...num = 4
    main finish-----------------------------------------
    child thread running...num = 4
    child thread running...num = 5
    child thread running...num = 6
    child thread running...num = 7
    child thread running...num = 8
    child thread running...num = 9
    child thread finish-----------------------------------------

    放开第20行注释的输出结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    child thread running...num = 0
    child thread running...num = 1
    child thread running...num = 2
    child thread running...num = 3
    child thread running...num = 4
    child thread running...num = 5
    child thread running...num = 6
    child thread running...num = 7
    child thread running...num = 8
    child thread running...num = 9
    child thread finish-----------------------------------------
    main running...num = 0
    main running...num = 1
    main running...num = 2
    main running...num = 3
    main running...num = 4
    main finish-----------------------------------------

守护线程

在Java中有两类线程,分别是User Thread(用户线程)和Daemon Thread(守护线程) 。

用户线程很好理解,我们日常开发中编写的业务逻辑代码,运行起来都是一个个用户线程。而守护线程相对来说则要特别理解一下。

什么是守护线程

在操作系统里面是没有所谓的守护线程的概念的,只有守护进程一说。但是Java语言机制是构建在JVM的基础之上的,这一机制意味着Java平台是把操作系统的底层给屏蔽了起来,所以它可以在它自己的虚拟的平台里面构造出对自己有利的机制。而Java语言或者说平台的设计者多多少少是收到Unix操作系统思想的影响,而守护线程机制又是对JVM这样的平台凑合,于是守护线程应运而生。

所谓的守护线程,指的是程序运行时在后台提供的一种通用服务的线程。比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

事实上,User Thread(用户线程)和Daemon Thread(守护线程)从本质上来说并没有什么区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。

守护线程的使用

守护线程并非只有虚拟机内部可以提供,用户也可以手动将一个用户线程设定/转换为守护线程。

在Thread类中提供了一个setDaemon(true)方法来将一个普通的线程(用户线程)设置为守护线程。

1
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还没来得及进行操作时,虚拟机已经退出了。这也就意味着,守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。

守护线程的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static void main(String[] args) throws Exception{
Thread thread1 = new Thread(){
@Override
public void run() {
super.run();
int num = 0;
try {
while (num < 10){
Thread.sleep(20);
System.out.println("child thread running...num = " + num++);
}
System.out.println("child thread finish-----------------------------------------");
}catch (InterruptedException exception){
exception.printStackTrace();
}
}
};
thread1.setName("A");
thread1.setDaemon(true);
thread1.start();
int num = 0;
while (num < 5){
Thread.sleep(20);
System.out.println("main running...num = " + num++);
}
System.out.println("main finish-----------------------------------------");
}

输出:

1
2
3
4
5
6
7
8
9
10
11
main running...num = 0
child thread running...num = 0
main running...num = 1
child thread running...num = 1
main running...num = 2
child thread running...num = 2
main running...num = 3
child thread running...num = 3
child thread running...num = 4
main running...num = 4
main finish-----------------------------------------

守护线程就是用来告诉JVM,我的这个线程是一个低级别的线程,不需要等待它运行完才退出,让JVM喜欢什么时候退出就退出,不用管这个线程。

在日常的业务相关的CRUD开发中,其实并不会关注到守护线程这个概念,也几乎不会用上。

但是如果要往更高的地方走的话,这些深层次的概念还是要了解一下的,比如一些框架的底层实现。