Java-Thread总结
说起线程,就会想到进程。进程是程序运行资源分配的最小单位。进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、 磁盘 IO 等,同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程 之间是相互独立的。进程是具有一定独立功能的程序关于某个数据集合上的一次 运行活动,进程是系统进行资源分配和调度的一个独立单位。
进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一 个进程。显然,程序是死的、静态的,进程是活的、动态的。进程可以分为系统进 程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就 是处于运行状态下的操作系统本身,用户进程就是所有由你启动的进程。
线程是 CPU 调度的最小单位,必须依赖于进程而存在
线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的、 能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中 必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其 他的线程共享进程所拥有的全部资源。
线程无处不在
任何一个程序都必须要创建线程,特别是 Java 不管任何程序都必须启动一个 main 函数的主线程; Java Web 开发里面的定时任务、定时器、JSP 和 Servlet、异 步消息处理机制,远程访问接口RM等,任何一个监听事件, onclick的触发事件等都 离不开线程和并发的知识。
Java中的线程
线程的创建
Java中创建线程有两种方法
继承Thread
1
2
3
4
5
6
7public class UseThread extends Thread{
public void run() {
super.run();
System.out.println("I am extende Thread");
}
}实现Runnable接口
1
2
3
4
5
6public class UseRunnable implements Runnable{
public void run() {
System.out.println("I am implements Runnable");
}
}
线程的调用执行
1 | public static void main(String[] args) throws InterruptedException, ExecutionException { |
线程的状态
线程状态流转示意
如何终止线程?
查看Thread的API,发现有stop()方法,那stop可以终止线程吗?看注解标记,已经被废弃了。
上代码看看
1 | public static void main(String[] args) throws Exception{ |
输出
1 | .... |
根据实测,线程的执行确实被中断了,但是仔细查看输出,发现终止时只执行了 running…sleep before 也就是说sleep后面的代码并未执行,直接被暴力终止了。并未给用户留下收尾处理的机会。这也是为什么在Java的API中被标记废弃的原因,过于暴力的终止可能会造成一些不可预知的错误。
当stop被调用后,可能会发生以下情况:
- 直接停掉run中的执行,包括在catch或finally语句中,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭。
- 会立即释放该线程所持有的所有的锁,导致数据得不到同步的处理,出现数据不一致的问题。
那么我们改如何优雅的停掉线程执行呢?继续查Thread的API,发现有个interrupt()方法,看代码
1 | public static void main(String[] args) throws Exception{ |
一运行发现,翻车了。。。。怎么停不下来呢?
原因就在于调用interrupt后会抛出InterruptedException,在catch InterruptedException异常时将interrupt标识位重置为false了,所以while一直满足执行。
所以,还需要在catch中增加 interrupt()调用。
1 | while (!isInterrupted()){ |
Thread中的主要方法
suspend()和resume()
这两个方法,一个是挂起,一个是恢复,通常要成对儿出现,不然很容易发生死锁,因为suspend方法并不会释放锁,如果使用suspend的目标线程对一个重要的系统资源持有锁,那么没任何线程可以使用这个资源直到要suspend的目标线程被resumed,如果一个线程在resume目标线程之前尝试持有这个重要的系统资源锁再去resume目标线程,这两条线程就相互死锁了,也就冻结线程。
yield()
yield 即 “谦让”,也是 Thread 类的方法。它让掉当前线程 CPU 的时间片,使正在运行中的线程重新变成就绪状态,并重新竞争 CPU 的调度权。它可能会获取到,也有可能被其他线程获取到。
yield和sleep的不同:
1)yield, sleep 都能暂停当前线程,sleep 可以指定具体休眠的时间,而 yield 则依赖 CPU 的时间片划分。
2)yield, sleep 两个在暂停过程中,如已经持有锁,则都不会释放锁资源。
3)yield 不能被中断,而 sleep 则可以接受中断。
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
27public static void main(String[] args) throws Exception{
Thread thread1 = new Thread(){
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
17main 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
17child 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 | public static void main(String[] args) throws Exception{ |
输出:
1 | main running...num = 0 |
守护线程就是用来告诉JVM,我的这个线程是一个低级别的线程,不需要等待它运行完才退出,让JVM喜欢什么时候退出就退出,不用管这个线程。
在日常的业务相关的CRUD开发中,其实并不会关注到守护线程这个概念,也几乎不会用上。
但是如果要往更高的地方走的话,这些深层次的概念还是要了解一下的,比如一些框架的底层实现。