第一章 多线程基础
1.1 进程和线程
1.1.1 操作系统和进程
进程是操作系统动态执行的基本单元,任何一个应用程序都可以看作是一个运行时的进程。进程是运行在它自己的地址空间的自包容程序。多任务操作系统可以通过周期性地将CPU从一个进程切换到另一个进程,来实现同时运行多个进程。
1.1.2 线程和进程的关系
线程就是应用程序(进程)在运行过程中,通过操作系统向CPU发起的一个任务,这个任务只能访问当前进程的内存资源。
1.2 线程的启动
线程有两种启动方式:实现Runnable接口;继承Thread类并重写run()方法。
第一种方式:实现Runnable接口并且重写run()方法,然后再将Runnable的实现对象作为参数传递给Thread类。
代码示例如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ELRBDWuX-1663755628903)(file:///C:\Users\范元杰\AppData\Local\Temp\ksohtml16068\wps1.png)]
第二种方式:
继承Thread类并且重写run()方法,然后调用start()启动线程。代码示例如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9hHpYuei-1663755628904)(file:///C:\Users\范元杰\AppData\Local\Temp\ksohtml16068\wps2.png)]
通常情况下,实现Runnable接口然后启动线程是一个更好的选择,这可以提高程序的灵活性和扩展性,并且用Runnable接口描述任务也更容易理解。在后面的线程池调用中,也使用Runnable表示要执行的任务。
(源代码说:您只打算覆盖 run() 方法而不打算覆盖其他 Thread 方法,则应该使用 Runnable 接口。)
需要特别注意的是,执行start()方法的顺序不代表线程启动的顺序,可以设置任务的优先级,优先级高的任务可能会优先执行(多数时候是无效的)。任务被执行前,该线程处于自旋等待状态。
1.2.1线程的标识
Thread类用于管理线程,如设置线程优先级、设置Daemon属性、读取线程名字和ID、启动线程任务、暂停线程任务、中断线程等。
为了管理线程,每个线程在启动后都会生成一个唯一的标识符,并且在其生命周期内保持不变。当线程被终止时,该线程ID可以被重用。而线程的名字更加直观,但是不具有唯一性。
代码示例如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f74NePT6-1663755628904)(file:///C:\Users\范元杰\AppData\Local\Temp\ksohtml16068\wps3.png)]
1.2.2 thread和runnable
Runnable接口表示线程要执行的任务。当Runnable中的run()方法执行时,表示线程在激活状态,run()方法一旦执行完毕,即表示任务完成,则线程将被停止。线程的两种启动方式,其本质都是实现Thread类中的run()方法。而实现Runnable接口,然后传递给Thread类的方式,比Thread子类重写run()方法更加灵活。
1.2.3 run()和start()
调用Thread对象的start()方法,使线程对象开始执行任务,这会触发Java虚拟机调用当前线程对象的run()方法。调用start()方法后,将导致两个线程并发运行,一个是调用start()方法的当前线程,另外一个是执行run()方法的线程。重复调用start()方法,将会引发异常
这里要特别注意的是,Thread对象调用start()方法之后,触发了JVM底层调用run()方法。如果我们主动调用Thread对象的run()方法,并不能启动一个新线程。
1.2.4 thread源码分析
创建Thread类实例,首先会执行registerNatives()方法,它在静态代码块中加载。线程的启动、运行、生命期管理和调度等都高度依赖于操作系统,Java本身并不具备与底层操作系统交互的能力。因此线程的底层操作都使用了native方法,registerNatives()就是用C语言编写的底层线程注册方法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5VcV60Mg-1663755628905)(file:///C:\Users\范元杰\AppData\Local\Temp\ksohtml16068\wps4.png)]
无论通过Thread类的哪种构造方法去创建线程,都需要首先调用init()方法,初始化线程环境,源码如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZiQcL4Pz-1663755628905)(file:///C:\Users\范元杰\AppData\Local\Temp\ksohtml16068\wps5.png)]
在init()方法中,做了如下操作:
(1)设置线程名称。
(2)将新线程的父线程设置为当前线程。
(3)获取系统的安全管理SecurityManager,并获得线程组。SecurityManager在Java中被用来检查应用程序是否能访问一些受限资源,如文件、套接字(socket)等。它可以用在那些具有高安全性要求的应用程序中。
(线程组它表示一个线程组的集合,线程运行中,不能改变它的归属线程组;即线程一旦指定所在的线程组就不可改变,如果没有指定,默认就是main线程组)
(4)获取线程组的权限检查。
(5)在线程组中增加未启动的线程数量。
(6)设置新线程的属性,包括守护线程属性(默认继承父线程)、优先级(默认继承父线程)、堆栈大小(如果为0,则默认由JVM分配)、线程组、线程安全控制上下文(一种Java安全模式,设置访问控制权限)等。
接下来我们看一下Thread类中的start()方法,源码如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ghi62koc-1663755628905)(file:///C:\Users\范元杰\AppData\Local\Temp\ksohtml16068\wps6.png)]
(1)在线程组中减少未启动的线程数量。
(2)调用底层的native方法start()进行线程启动。
(3)最终由底层native方法调用run()执行。
(4)如果启动失败,从线程组中移除该线程,并且增加未启动线程数量。
1.3 线程状态
Java中的线程存在6种状态,分别是NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED(结束)。我们可以通过Thread类中的Thread.getState()方法获取线程在某个时期的线程状态。在给定的时间点,线程只能处于一种状态。
1.3.1 NEW状态
一个已创建但是未起动(start)的线程处于NEW状态。
public static void main(String[] args) {
Thread test001 = new test001();
System.out.println(test001.getState());
}
输出:NEW
1.3.2 RUNNABLE状态
RUNNABLE状态表示一个线程正在Java虚拟机中运行,但是这个线程是否获得了处理器分配资源并不确定。调用Thread的start()方法后,线程从NEW状态切换到了RUNNABLE状态。
public static void main(String[] args) {
Thread test001 = new test001();
System.out.println(test001.getState());
test001.start();
System.out.println(test001.getState());
}
输出:
NEW
RUNNABLE
1.3.3 BLOCKED状态
BLOCKED为阻塞状态,表示当前线程正在阻塞等待获得监视器锁。当一个线程要访问被其他线程synchronized锁定的资源时,当前线程需要阻塞等待。(意思就是有一个全局的string,被一个线程获取了之后,另一个在想获取的话,就只能等了),由下图可见,当test001和test002调用start方法后,线程的状态都会由new状态装换成RUNNABLE的状态,然后test001获取到String s的锁,之后test002在想获取锁的时候就获取不到对象锁,状态在这个时候就会装换成BLOCK阻塞状态,全部运行结束后线程就会进入TERMINATED(terminated)线程完结状态
public class main {
public static String s="0";
public static void main(String[] args) throws InterruptedException {
Thread test001 = new test001();
Thread test002 = new Thread(new test002());
test001.start();
test002.start();
for (int i = 1 ; i<=100 ; i ++){
System.out.println("002状态"+test002.getState());
System.out.println("001状态"+test001.getState());
Thread.sleep(100);
}
}
}
public class test001 extends Thread{
@Override
public void run() {
synchronized (main.s){
try {
System.out.println("test001执行");
Thread.sleep(9000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class test002 implements Runnable{
@Override
public void run() {
synchronized (main.s){
System.out.println("synchronized的002状态"+Thread.currentThread().getState());
}
}
}
输出:
002状态RUNNABLE
test001执行
002状态BLOCKED
001状态TIMED_WAITING
...
002状态BLOCKED
001状态TIMED_WAITING
synchronized的002状态RUNNABLE
002状态TERMINATED
001状态TERMINATED
...
002状态TERMINATED
001状态TERMINATED
1.3.4waiting状态
在当前线程中调用如下方法之一时,会使当前线程进入等待状态:
Object类的wait()方法(没有超时设置);
(wait()方法在进行等待的时候会释放锁,然后等调用notify()或者notifyall()方法,来让线程继续运行,注意调用wait()/notify()方法前需要先锁定object对象)
Thread类的join()方法(没有超时设置)
在t2线程中调用t1线程的join()方法,导致t2线程进入WAITING状态,直到t1线程全部执行完毕,t2线程才继续执行。
LockSupport类的park()方法。调用park()和unpark()方法前无须锁定对象。(这个方法也会阻塞当前线程,可以用.interrupt()和unpark唤醒)
1.3.5TIMED WAITING状态
Object类的wait()方法(有超时设置); 调用object.wait(3000),在指定时间内没有调用object对象的notify()或notifyAll(),就会触发超时等待结束,当前线程重新进入RUNNABLE状态。
Thread类的join()方法(有超时设置);
Thread类的sleep()方法(有超时设置);调用Thread.sleep(3000),当前线程也会进入定时等待状态。
LockSupport类的parkNanos ()方法;
LockSupport类的parkUntil()方法。
1.3.6 WAITING与BLOCKED的区别
(1)Thread.sleep()不会释放占有的对象锁,因此会持续占用CPU。
(2)Object.wait()会释放占有的对象锁,不会占用CPU。
(3)BLOCKED使当前线程进入阻塞后,为了抢占对象监视器锁,一般操作系统都会给这个线程持续的CPU使用权。
(4)LockSupport.park()底层调用UNSAFE.park()方法实现,它没有使用对象监视器锁,不会占用CPU。
1.3.7 TERMINATED状态
TERMINATED表示线程为完结状态。当线程完成其run()方法中的任务,或者因为某些未知的异常而强制中断时,线程状态变为TERMINATED。
1.4 sleep()与yield()
1.4.1 线程休眠sleep()
Thread类的sleep()方法,使当前正在执行的线程以指定的毫秒数暂时停止执行,具体停止时间取决于系统定时器和调度程序的精度和准确性。当前线程状态由RUNNABLE切换到TIMED_WAITING。调用sleep()方法不会使线程丢失任何监视器所有权,因此当前线程仍然占用CPU分片。 调用sleep()方法可能会抛出InterruptedException异常,它应该在run()方法中被捕获,因为异常无法传递到其他线程,如主线程就无法捕获子线程抛出的异常。
1.4.2 线程让步yield()
Thread类的yield()方法对线程调度器发出一个暗示,即当前线程愿意让出正在使用的处理器。调度程序可以响应暗示请求,也可以自由地忽略这个提示。可能从running状态转为runnable状态。 需要强调的是:yield()仅仅是一个暗示,没有任何机制保证它一定会被采纳。线程调度器是Java线程机制的底层对象,可以把CPU的使用权从一个线程转移到另外一个线程。如果你的计算机是多核处理器,那么分配线程到不同的处理器执行任务要依赖线程调度器
1.5 线程优先级
每个线程都有优先级。具有较高优先级的线程可能优先获得CPU的使用权。创建一个新的Thread对象时,新线程的优先级默认与创建线程的优先级一致。
我们可以通过Thread类中setPriority()方法对线程的优先级进行设置。
这里面需要注意的是,如果设置的线程优先级小于1(MIN_PRIORITY)或者大于10(MAX_PRIORITY)都将抛出IllegalArgumentException异常。
不应该过分依赖于线程优先级的设置,理论上线程优先级高的会优先执行,但实际情况可能并不明确。例如,线程调度机制还没有来得及介入时,线程可能就已经执行完了。所以优先级具有一定的“随机性”。
1.5.1 线程优先级与资源竞争
具有较高优先级的线程会优先得到调度系统资源分配。也就是说优先级高的线程可以优先竞争共享资源。当调用Thread.yield()方法时,会给线程调度器一个暗示,即优先级高的其他线程或相同优先级的其他线程,都可以优先获得CPU分片。
1.6 守护线程
1.6.1 守护线程的概念
在Java线程中有两种线程,一种是用户线程,另一种是守护线程(Daemon)。 所谓守护线程,是指在程序运行的时候在后台提供一种通用服务的线程。Daemon线程与用户线程在使用时没有任何区别,唯一的不同是:当所有用户线程结束时,程序也会终止,Java虚拟机不管是否存在守护线程,都会退出。 调用Thread对象的setDaemon()方法,可以把用户线程标记为守护者。调用isDaemon()方法可以判断线程是否是一个守护线程。
在使用守护线程时需要注意以下几点: (1)setDaemon()方法必须在start()方法之前设置,否则会抛出一个IllegalThreadState-Exception异常。不能把正在运行的常规线程设置为守护线程。 (2)在守护线程Daemon中产生的新线程也是守护线程,存在着继承性。
(3)守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
护线程,都会退出。 调用Thread对象的setDaemon()方法,可以把用户线程标记为守护者。调用isDaemon()方法可以判断线程是否是一个守护线程。
在使用守护线程时需要注意以下几点: (1)setDaemon()方法必须在start()方法之前设置,否则会抛出一个IllegalThreadState-Exception异常。不能把正在运行的常规线程设置为守护线程。 (2)在守护线程Daemon中产生的新线程也是守护线程,存在着继承性。
(3)守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
(4)守护线程通常都使用while(true)的死循环来持续执行任务。