一、 引言
1. 进程
通俗的来说,进程就是我们打开电脑后使用的一个个的应用程序。比如:打开QQ会启动一个进程,打开浏览器又会启动另一个进程,进程和我们的程序一一对应。关于进程特点,如下:
- 每个进程对应一定的内存地址空间,并且只能使用它自己的内存空间,各个进程间互不干扰,并且,进程保存了程序每个时刻的运行状态,这样就为进程切换提供了可能
- 一个进程可以包含多个线程
- 进程让操作系统的并发性成为可能,但是要注意,一个进程虽然包括多个线程,但是这些线程是共同享有进程占有的资源和地址空间的
- 进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位。
2. 线程
在出现了进程以后,操作系统的性能得到了大大的提升。虽然进程的出现解决了操作系统的并发问题,但是人们仍然不满足,人们逐渐对实时性有了要求。因为一个进程在一个时间段内只能做一件事情,如果一个进程有多个子任务,只能逐个地去执行这些子任务。所以,线程就应运而生,特点如下:
- 线程让进程的内部并发成为可能
- 线程允许在同一个进程中同时存在多个程序控制流,线程会共享进程范围内的资源,但每个线程都有各自的 程序计数器 、栈 以及 局部变量 等;
在 Java
中,一个应用程序对应着一个JVM
实例(JVM
进程)。Java
采用的是 单线程编程模型 ,即在我们自己的程序中如果没有主动创建线程的话,只会创建一个线程,通常称为主线程。但是要注意,虽然只有一个线程来执行任务,不代表JVM
中只有一个线程,JVM
实例在创建的时候,同时会创建很多其他的线程(比如垃圾收集器线程)。
3. 使用线程会带来哪些问题?
(1). 安全性问题
在线程安全性的定义中,最核心的概念就是正确性。当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么这个类就是线程安全的。
(2). 活跃性问题
活跃性问题关注的是:某件正确的事情最终会发生。导致活跃性的问题包括死锁、饥饿等。
(3). 性能问题
性能问题关注的是:正确的事情能够尽快发生。性能问题包括多个方面,例如响应不灵敏,吞吐率过低,资源消耗过高等。
在多线程程序中,当线程调度器临时挂起活跃线程并转而运行另一个线程时,就会频繁出现上下文切换操作(Context Switch
),这种操作会导致 CPU 时间更多的花在线程调度上而非线程的运行上。
4. 线程的调度策略
如果处于就绪状态的线程获得了CPU,就会开始执行线程的run()
方法的任务,则该线程就会处于运行状态。如果计算机只有一个CPU。那么在任何时刻只有一个线程处于运行状态,当然在一个多处理器的机器上,将会有多个线程并行执行;当线程数大于处理器数时,依然会存在多个线程在同一个CPU上轮换的现象。
当一个线程开始运行后,它不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就执行结束了)。线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。对于采用抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务;当该时间段用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。在选择下一个线程时,系统会考虑线程的优先级。
所有现代的桌面和服务器操作系统都采用抢占式调度策略,但一些小型设备如手机则可能采用协作式调度策略,在这样的系统中,只有当一个线程调用了它的sleep()
或yield()
方法后才会放弃所占用的资源,也就是必须由该线程主动放弃所占用的资源。
二、 线程的生命周期
Java
中 Thread
类 的各种操作与线程的生命周期密不可分,所以了解清楚线程的生命周期,有助于更好的理解Thread
类中的各个方法。
一般来说,线程从最初的创建到最终的消亡,要经历创建(new)、就绪(runnable)、运行(running)、阻塞(blocked)、等待(waiting)、时间等待(time waiting) 和 消亡(dead/terminated)等状态。在给定的时间点上,一个线程只能处于一种状态。而在线程的生命周期中,各个状态的切换,需要上下文的切换,即通过存储和恢复CPU状态使得其能够从中断点恢复执行。在线程生命周期中,调用各个方法时,要特别关注两个问题:
- 客户端调用某个方法后,是否会释放锁
- 客户端调用某个方法后,是否会交出CPU
1. 线程的各个状态
-
新建状态(new)
当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态。此时仅由 JVM 为其分配内存,并初始化其成员变量的值
-
就绪状态(runnable)
当线程对象调用了
start()
方法之后,该线程处于就绪状态。JVM会为其创建方法调用栈和程序计数器,等待调度运行 -
运行状态(running)
如果处于就绪状态的线程获得了CPU,开始执行
run()
方法的线程执行体,则该线程处于运行状态 -
阻塞状态(blocked)
当处于运行状态的线程失去所占用资源之后,便进入阻塞状态
-
消亡(dead)
当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行
2. 线程生命周期分析
当我们需要线程来执行某个子任务时,就必须先创建一个线程。但是线程创建之后,并不会立即进入就绪状态,因为线程的运行需要一些条件(比如程序计数器、Java栈、本地方法栈等),只有线程运行需要的所有条件满足了,才进入就绪状态。当线程进入就绪状态后,不代表立刻就能获取CPU执行时间,也许此时CPU正在执行其他的事情,因此它要等待。当得到CPU执行时间之后,线程便真正进入运行状态。线程在运行状态过程中,可能有多个原因导致当前线程不能继续运行下去,比如用户主动让线程睡眠(睡眠一定的时间之后再重新执行)、用户主动让线程等待,或者被同步块阻塞,此时就对应着多个状态:time waiting(睡眠或等待一定的时间)、waiting(等待被唤醒)、blocked(阻塞)。当由于突然中断或者子任务执行完毕,线程就会被消亡。
实际上,Java
只定义了六种线程状态,分别是 New
、 Runnable
、 Waiting
(主动等待)、Timed Waiting
(主动睡眠)、Blocked
、Terminated
。各状态直接的转换如下图所示:
三、 何为上下文切换?
以单核CPU为例,CPU在一个时刻只能运行一个线程。CPU在运行一个线程的过程中,转而去运行另外一个线程,这个叫做线程 上下文切换。
由于可能当前线程的任务并没有执行完毕,所以在切换时需要保存线程的运行状态,以便下次重新切换回来时能够紧接着之前的状态继续运行。
举个简单的例子:比如,一个线程A正在读取一个文件的内容,正读到文件的一半,此时需要暂停线程A,转去执行线程B,当再次切换回来执行线程A的时候,我们不希望线程A又从文件的开头来读取。
因此需要记录线程A的运行状态,那么会记录哪些数据呢?因为下次恢复时需要知道在这之前当前线程已经执行到哪条指令了,所以需要记录程序计数器的值,另外比如说线程正在进行某个计算的时候被挂起了,那么下次继续执行的时候需要知道之前挂起时变量的值时多少,因此需要记录CPU寄存器的状态。所以,一般来说,线程上下文切换过程中会记录程序计数器、CPU寄存器状态等数据。
实质上, 线程的上下文切换就是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行,这正是有程序计数器所支持的。
虽然多线程可以使得任务执行的效率得到提升,但是由于在线程切换时同样会带来一定的开销代价,并且多个线程会导致系统资源占用的增加,所以在进行多线程编程时要注意这些因素。
总结:上下文切换要做哪些事?
答:需要程序计数器来记录该线程执行到哪条指令,需要寄存器来记录该当前线程挂起时,当前变量的值,以便下次恢复时线程继续执行。
四、 线程的创建
1. 线程的创建方式
在 Java
中,创建线程一般有两种方式:继承 Thread
类和实现 Runnable
接口。其中,Thread
类本身就实现了 Runnable
接口,而使用继承 Thread
类的方式创建线程的最大局限就是不支持多继承。特别需要注意两点
- 实现多线程必须重写
run()
方法,即在run()
方法中定义需要执行的子任务 run()
方法不需要用户来调用
线程创建的代码示例:
public class ThreadTest {
public static void main(String[] args) {
//使用继承Thread类的方式创建线程
new Thread(){
@Override
public void run() {
System.out.println("Thread");
}
}.start();
//使用实现Runnable接口的方式创建线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Runnable");
}
});
thread.start();
//JVM 创建的主线程 main
System.out.println("main");
}
}
Output: (代码的运行结果与代码的执行顺序或调用顺序无关)
Thread
main
Runnable
创建好自己的线程类之后,就可以创建线程对象了,然后通过start()
方法去启动线程。注意,run()
方法中只是定义需要执行的任务,并且其不需要用户来调用。当通过 start()
方法启动一个线程之后,若线程获得了CPU执行时间,便进入run()
方法体去执行具体的任务。如果用户直接调用run()
方法,即相当于在主线程中执行run()
方法,跟普通的方法调用没有任何区别,此时并不会创建一个新的线程来执行定义的任务。
五、Thread 类详解
Thread
类实现了 Runnable
接口,在 Thread
类中,有一些比较关键的属性,比如name
是表示Thread
的名字,可以通过Thread
类的构造器中的参数来指定线程名字,priority
表示线程的优先级(最大值为10,最小值为1,默认值为5),daemon
表示线程是否是守护线程,target
表示要执行的任务。
1. 与线程运行状态有关的方法
(1). start() 方法
start()
方法的作用:用来启动一个线程,当调用该方法后,相应线程就会进入就绪状态,该线程中的run()
方法会在某个时机被调用。
(2). run() 方法
run()
方法是不需要用户来调用的。当通过start()方法启动一个线程之后,一旦线程获得了 CPU
执行时间,便进入run()
方法体去执行具体的任务。
注意:创建线程时必须重写
run()
方法,以定义具体要执行的任务;一般来说,有两种方式可以达到重写run()
方法的效果:直接重写:直接继承Thread类并重写run()方法;
间接重写:通过Thread构造函数传入Runnable对象,实际上重写的是 Runnable对象 的run() 方法。
####(3). sleep() 方法
sleep()
方法的作用是:在指定的毫秒数内让当前正在执行的线程睡眠(即 currentThread()
方法所返回的线程),并交出 CPU
让其去执行其他的任务。当线程睡眠时间满后,不一定会立即得到执行,因为此时 CPU 可能正在执行其他的任务。所以说,调用sleep()
方法相当于让线程进入阻塞状态(blocked
)。该方法有如下特征:
- 使调用该方法的线程暂停执行一段时间,让其他线程有机会继续执行,但它并不释放对象锁。也就是说如果有
synchronized
同步块,其他线程仍然不能访问共享数据,注意该方法要捕捉异常 sleep()
是帮助其他线程获得运行机会的最好方法,但是如果当前线程获取到的有锁,sleep()
不会让出锁- 线程睡眠到期自动苏醒,并返回到可运行状态(就绪),不是运行状态
- 睡眠线程在苏醒之后,并不会立即执行,所以
sleep()
中指定的时间是线程不会运行的最短时间,sleep()
方法不能作为精确的时间控制
(4). yield() 方法
yield()
方法的作用:会让当前线程交出CPU资源,让CPU去执行其他的线程。但是,yield()
不能控制具体的交出CPU的时间。需要注意的是:
yield()
方法只能让拥有相同优先级的线程有获取 CPU 执行时间的机会- 调用
yield()
方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新得到 CPU 的执行 - 它同样不会释放锁
public class MyThread extends Thread {
@Override
public void run() {
long beginTime = System.currentTimeMillis();
int count = 0;
for (int i = 0; i < 50000; i++) {
Thread.yield(); // 将该语句注释后,执行会变快
count = count + (i + 1);
}
long endTime = System.currentTimeMillis();
System.out.println("用时:" + (endTime - beginTime) + "毫秒!");
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
####(5). join() 方法
假如在main
线程中调用thread.join()
方法,则main
线程会等待thread
线程执行完毕或者等待一定的时间。详细地,如果调用的是无参join()
方法,则等待thread
执行完毕;如果调用的是指定了时间参数的join()
方法,则等待一定的时间。join()
方法有三个重载方法:
public final void join() throws InterruptedException {...}
public final synchronized void join(long millis) throws InterruptedException {...}
public final synchronized void join(long millis, int nanos) throws InterruptedException {...}
以 join(long millis)
方法为例,其内部调用了Object
的wait()
方法,如下图:
根据以上源代码可以看出,join()
方法是通过wait()
方法 (Object
提供的方法) 实现的。当 millis == 0 时,会进入 while(isAlive())
循环,并且只要子线程是活的,宿主线程就不停的等待。 wait(0)
的作用是让当前线程(宿主线程)等待,而这里的当前线程是指 Thread.currentThread()
所返回的线程。所以,虽然是子线程对象(锁)调用wait()
方法,但是阻塞的是宿主线程。
看下面的例子,当 main
线程运行到 thread1.join()
时,main
线程会获得线程对象thread1
的锁(wait 意味着拿到该对象的锁)。只要 thread1
线程存活, 就会调用该对象锁的wait()
方法阻塞 main
线程,直至 thread1
线程 退出才会使 main
线程 得以继续执行。
//示例代码
public class Test {
public static void main(String[] args) throws IOException {
System.out.println("进入线程"+Thread.currentThread().getName());
Test test = new Test();
MyThread thread1 = test.new MyThread();
thread1.start();
try {
System.out.println("线程"+Thread.currentThread().getName()+"等待");
thread1.join();
System.out.println("线程"+Thread.currentThread().getName()+"继续执行");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println("进入线程"+Thread.currentThread().getName());
try {
Thread.currentThread().sleep(5000);
} catch (InterruptedException e) {
// TODO: handle exception
}
System.out.println("线程"+Thread.currentThread().getName()+"执行完毕");
}
}
}
/* Output:
进入线程main
线程main等待
进入线程Thread-0
线程Thread-0执行完毕
线程main继续执行
*///~
看上面的例子,当 main
线程 运行到 thread1.join()
时,main
线程会获得线程对象thread1
的锁(wait 意味着拿到该对象的锁)。只要 thread1
线程 存活, 就会调用该对象锁的wait()
方法阻塞 main
线程。那么,main
线程被什么时候唤醒呢?事实上,有wait
就必然有notify
。在整个jdk
里面,我们都不会找到对thread1
线程的notify
操作。这就要看jvm
代码了:
//一个c++函数:
void JavaThread::exit(bool destroy_vm, ExitType exit_type) ;
//这个函数的作用就是在一个线程执行完毕之后,jvm会做的收尾工作。里面有一行代码:ensure_join(this);
该函数源码如下:
static void ensure_join(JavaThread* thread) {
Handle threadObj(thread, thread->threadObj());
ObjectLocker lock(threadObj, thread);
thread->clear_pending_exception();
java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
java_lang_Thread::set_thread(threadObj(), NULL);
//thread就是当前线程,就是刚才说的thread1线程。
lock.notify_all(thread);
thread->clear_pending_exception();
}
至此,thread1
线程对象锁调用了notifyall
,那么main
线程也就能继续跑下去了。
由于 join
方法 会调用 wait
方法 让宿主线程进入阻塞状态,并且会释放线程占有的锁,并交出CPU执行权限。结合 join
方法的声明,有以下三条:
join
方法同样会让线程交出CPU执行权限join
方法同样会让线程释放对一个对象持有的锁- 如果调用了
join
方法,必须捕获InterruptedException
异常或者将该异常向上层抛出
(6). interrupt() 方法
interrupt()
的作用:单独调用interrupt()
方法可以使得 处于阻塞状态的线程 抛出一个异常,也就是说,它可以用来中断一个正处于阻塞状态的线程;另外,通过interrupted()
方法 和isInterrupted()
方法可以停止正在运行的线程。interrupt
方法在 JDK
中的定义为:
interrupted() 和 isInterrupted()方法在 JDK 中的定义分别为:
下面看一个例子:
public class Test {
public static void main(String[] args) throws IOException {
Test test = new Test();
MyThread thread = test.new MyThread();
thread.start();
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
}
thread.interrupt();
}
class MyThread extends Thread{
@Override
public void run() {
try {
System.out.println("进入睡眠状态");
Thread.currentThread().sleep(10000);
System.out.println("睡眠完毕");
} catch (InterruptedException e) {
System.out.println("得到中断异常");
}
System.out.println("run方法执行完毕");
}
}
}
/* Output:
进入睡眠状态
得到中断异常
run方法执行完毕
*///~
从这里可以看出,通过interrupt()
方法可以中断处于阻塞状态的线程。那么能不能中断处于非阻塞状态的线程呢?看下面这个例子:
public class Test {
public static void main(String[] args) throws IOException {
Test test = new Test();
MyThread thread = test.new MyThread();
thread.start();
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {}
thread.interrupt();
}
class MyThread extends Thread{
@Override
public void run() {
int i = 0;
while(i<Integer.MAX_VALUE){
System.out.println(i+" while循环");
i++;
}
}
}
}
运行该程序会发现,while
循环会一直运行直到变量i的值超出Integer.MAX_VALUE
。所以说,直接调用interrupt()
方法不能中断正在运行中的线程。但是,如果配合isInterrupted()/interrupted()
能够中断正在运行的线程,因为调用interrupt()
方法相当于将中断标志位置为true
,那么可以通过调用isInterrupted()/interrupted()
判断中断标志是否被置位来中断线程的执行。比如下面这段代码:
public class Test {
public static void main(String[] args) throws IOException {
Test test = new Test();
MyThread thread = test.new MyThread();
thread.start();
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
}
thread.interrupt();
}
class MyThread extends Thread{
@Override
public void run() {
int i = 0;
while(!isInterrupted() && i<Integer.MAX_VALUE){
System.out.println(i+" while循环");
i++;
}
}
}
}
但是,一般情况下,不建议通过这种方式来中断线程,一般会在MyThread
类中增加一个 volatile
属性 isStop
来标志是否结束 while
循环,然后再在 while
循环中判断 isStop
的值。例如:
class MyThread extends Thread{
private volatile boolean isStop = false;
@Override
public void run() {
int i = 0;
while(!isStop){
i++;
}
}
public void setStop(boolean stop){
this.isStop = stop;
}
}
那么,就可以在外面通过调用setStop
方法来终止while
循环。
####(7). stop()方法
stop()
方法已经是一个废弃的方法,它是一个不安全的方法。因为调用 stop()
方法会直接终止run
方法的调用,并且会抛出一个ThreadDeath
错误,如果线程持有某个对象锁的话,会完全释放锁,导致对象状态不一致。所以, stop()
方法基本是不会被用到的。
2. 线程的暂停与恢复
(1). 线程的暂停/恢复方法在 JDK 中的定义
暂停线程意味着此线程还可以恢复运行。在 Java
中,可以使用 suspend()
方法暂停线程,使用 resume()
方法恢复线程的执行,但是这两个方法已被废弃,因为它们具有固有的死锁倾向。如果目标线程挂起时在保护关键系统资源的监视器上保持有锁,则在目标线程重新开始以前,任何线程都不能访问该资源。如果重新开始目标线程的线程想在调用 resume
之前锁定该监视器,则会发生死锁。
实例方法suspend()
在类Thread
中的定义:
实例方法 resume()
在类Thread
中的定义:
(2). 死锁
具体地,在使用suspend()
和resume()
方法时,如果使用不当,极易造成公共的同步对象的独占,使得其他线程无法得到公共同步对象锁,从而造成死锁。下面举两个示例:
// 示例 1
public class SynchronizedObject {
public synchronized void printString() { // 同步方法
System.out.println("Thread-" + Thread.currentThread().getName() + " begins.");
if (Thread.currentThread().getName().equals("a")) {
System.out.println("线程a suspend 了...");
Thread.currentThread().suspend();
}
System.out.println("Thread-" + Thread.currentThread().getName() + " is end.");
}
public static void main(String[] args) throws InterruptedException {
final SynchronizedObject object = new SynchronizedObject(); // 两个线程使用共享同一个对象
Thread a = new Thread("a") {
@Override
public void run() {
object.printString();
}
};
a.start();
new Thread("b") {
@Override
public void run() {
System.out.println("thread2 启动了,在等待中(发生“死锁”)...");
object.printString();
}
}.start();
System.out.println("main 线程睡眠 " + 5 +" 秒...");
Thread.sleep(5000);
System.out.println("main 线程睡醒了...");
a.resume();
System.out.println("线程 a resume 了...");
}
}
/* Output:
Thread-a begins.
线程a suspend 了...
thread2 启动了,在等待中(发生死锁)...
main 线程睡眠 5 秒...
main 线程睡醒了...
线程 a resume 了...
Thread-a is end.
Thread-b begins.
Thread-b is end.
*///:~
在示例 2 中,特别要注意的是,println()
方法实质上是一个同步方法。如果 thread
线程刚好在执行打印语句时被挂起,那么将会导致 main
线程中的字符串“main end!”
迟迟不能打印。其中,println()
方法定义如下:
// 示例 2
public class MyThread extends Thread {
private long i = 0;
@Override
public void run() {
while (true) {
i++;
System.out.println(i);
}
}
public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(1);
thread.suspend();
System.out.println("main end!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3. 线程常用操作
(1). 获得代码调用者信息
currentThread()
方法返回代码段正在被哪个线程调用的信息。
下面的例子给出了currentThread()
方法的使用方式:
public class CountOperate extends Thread {
public CountOperate() {
super("Thread-CO"); // 线程 CountOperate 的名字
System.out.println("CountOperate---begin");
System.out.println("Thread.currentThread().getName()=" + Thread.currentThread().getName());
System.out.println("this.getName()=" + this.getName());
System.out.println("CountOperate---end");
}
@Override
public void run() {
System.out.println("run---begin");
System.out.println("Thread.currentThread().getName()=" + Thread.currentThread().getName());
System.out.println("this.getName()=" + this.getName());
System.out.println("run---end");
}
public static void main(String[] args) {
CountOperate c = new CountOperate();
Thread t1 = new Thread(c);
t1.setName("A");
t1.start();
c.start();
}
}
/* Output:(输出结果不唯一)
CountOperate---begin ....... 行 1
Thread.currentThread().getName()=main ....... 行 2
this.getName()=Thread-CO ....... 行 3
CountOperate---end ....... 行 4
run---begin ....... 行 5
Thread.currentThread().getName()=A ....... 行 6
run---begin ........行 7
Thread.currentThread().getName()=Thread-CO ....... 行 8
this.getName()=Thread-CO ....... 行 9
run---end ....... 行 10
this.getName()=Thread-CO ....... 行 11
run---end ....... 行 12
*///:~
首先来看前四行的输出。我们知道 CountOperate
继承了 Thread
类,那么 CountOperate
就得到了 Thread
类的所有非私有属性和方法。CountOperate
构造方法中的 super(“Thread-CO”);
意味着调用了父类Thread
的构造器Thread(String name)
,也就是为 CountOperate
线程 赋了标识名。由于该构造方法是由main()
方法调用的,因此此时Thread.currentThread()
返回的是main
线程;而 this.getName()
返回的是CountOperate
线程的标识名。
其次,在main
线程启动了t1
线程之后,CPU
会在某个时机执行类CountOperate
的run()
方法。此时,Thread.currentThread()
返回的是t1
线程,因为是t1
线程的启动使run()
方法得到了执行;而 this.getName()
返回的仍是CountOperate
线程的标识名,因为此时this
指的是传进来的CountOperate
对象(具体原因见上面对run()
方法的介绍),由于它本身也是一个线程对象,所以可以调用getName()
得到相应的标识名。
在main
线程启动了CountOperate
线程之后,CPU
也会在某个时机执行类该线程的run()
方法。此时,Thread.currentThread()
返回的是CountOperate
线程,因为是CountOperate
线程的启动使run()
方法得到了执行;而this.getName()
返回的仍是CountOperate
线程的标识名,因为此时this指的就是刚刚创建的CountOperate
对象本身,所以得到的仍是 “Thread-CO ”。
(2). 判断线程是否处于活动状态
方法isAlive()
的功能是判断调用该方法的线程是否处于活动状态。其中,活动状态指的是线程已经 start
(无论是否获得CPU
资源并运行) 且尚未结束。
下面的例子给出了isAlive()
方法的使用方式:
public class CountOperate extends Thread {
public CountOperate() {
System.out.println("CountOperate---begin");
System.out.println("Thread.currentThread().getName()="
+ Thread.currentThread().getName()); // main
System.out.println("Thread.currentThread().isAlive()="
+ Thread.currentThread().isAlive()); // true
System.out.println("this.getName()=" + this.getName()); // Thread-0
System.out.println("this.isAlive()=" + this.isAlive()); // false
System.out.println("CountOperate---end");
}
@Override
public void run() {
System.out.println("run---begin");
System.out.println("Thread.currentThread().getName()="
+ Thread.currentThread().getName()); // A
System.out.println("Thread.currentThread().isAlive()="
+ Thread.currentThread().isAlive()); // true
System.out.println("this.getName()=" + this.getName()); // Thread-0
System.out.println("this.isAlive()=" + this.isAlive()); // false
System.out.println("run---end");
}
public static void main(String[] args) {
CountOperate c = new CountOperate();
Thread t1 = new Thread(c);
System.out.println("main begin t1 isAlive=" + t1.isAlive()); // false
t1.setName("A");
t1.start();
System.out.println("main end t1 isAlive=" + t1.isAlive()); // true
}
}
(3). 获取线程唯一标识
方法 getId()
的作用是取得线程唯一标识,由JVM
自动给出。
// 示例
public class Test {
public static void main(String[] args) {
Thread runThread = Thread.currentThread();
System.out.println(runThread.getName() + " " + runThread.getId());
}
}
/* Output:
main 1
*///:~
(4). getName()和setName()
用来得到或者设置线程名称。如果不手动设置线程名字,JVM
会为该线程自动创建一个标识名,形式为: Thread-数字。
(5). getPriority()和setPriority()
在操作系统中,线程可以划分优先级,优先级较高的线程得到的CPU
资源较多,也就是CPU
优先执行优先级较高的线程。设置线程优先级有助于帮助 “线程规划器” 确定在下一次选择哪个线程来获得CPU
资源。特别地,在 Java中,线程的优先级分为 1 ~ 10 这 10 个等级,如果小于 1 或大于 10,则
JDK
抛出异常 IllegalArgumentException
,该异常是 RuntimeException
的子类,属于不受检异常。JDK
中使用 3 个常量来预置定义优先级的值,如下:
public static final int MIN_PRIORITY = 1;
public static final int NORM_PRIORITY = 5;
public static final int MAX_PRIORITY = 10;
- 线程优先级的继承性
在 Java
中,线程的优先级具有继承性,比如 A 线程启动 B 线程, 那么 B 线程的优先级与 A 是一样的。
class MyThread2 extends Thread {
@Override
public void run() {
System.out.println("MyThread2 run priority=" + this.getPriority());
}
}
public class MyThread1 extends Thread {
@Override
public void run() {
System.out.println("MyThread1 run priority=" + this.getPriority());
MyThread2 thread2 = new MyThread2();
thread2.start();
}
public static void main(String[] args) {
System.out.println("main thread begin priority="
+ Thread.currentThread().getPriority());
Thread.currentThread().setPriority(6);
System.out.println("main thread end priority="
+ Thread.currentThread().getPriority());
MyThread1 thread1 = new MyThread1();
thread1.start();
}
}
/* Output:
main thread begin priority=5
main thread end priority=6
MyThread1 run priority=6
MyThread2 run priority=6
*///:~
- 线程优先级的规则性和随机性
线程的优先级具有一定的规则性,也就是CPU
尽量将执行资源让给优先级比较高的线程。特别地,高优先级的线程总是大部分先执行完,但并不一定所有的高优先级线程都能先执行完。
(6). 守护线程 (Daemon)
在 Java
中,线程可以分为两种类型,即用户线程和守护线程。守护线程是一种特殊的线程,具有“陪伴”的含义:当进程中不存在非守护线程时,则守护线程自动销毁,典型的守护线程就是垃圾回收线程。任何一个守护线程都是整个JVM
中所有非守护线程的保姆,只要当前JVM
实例中存在任何一个非守护线程没有结束,守护线程就在工作;只有当最后一个非守护线程结束时,守护线程才随着JVM
一同结束工作。 在 Thread
类中,方法 setDaemon()
的定义为:
public class MyThread extends Thread {
private int i = 0;
@Override
public void run() {
try {
while (true) {
i++;
System.out.println("i=" + (i));
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.setDaemon(true); //设置为守护线程
thread.start();
Thread.sleep(3000);
System.out.println("main 线程结束,也意味着守护线程 thread 将要结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/* Output: (结果不唯一)
i=1
i=2
i=3
main 线程结束,也意味着守护线程 thread 将要结束
*///:~
六. 总结
1. 对于上述线程的各项基本操作,其 所操作的对象满足:
- 若该操作是静态方法,也就是说,该方法属于类而非具体的某个对象,那么该操作的作用对象就是
currentThread()
方法所返回Thread
对象 - 若该操作是实例方法,也就是说,该方法属于对象,那么该操作的作用对象就是调用该方法的
Thread
对象
2. 对于上述线程的各项基本操作有:
-
线程一旦被阻塞,就会释放 CPU
-
当线程出现异常且没有捕获处理时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象
-
对于一个线程,CPU 的释放 与 锁的释放没有必然联系
3. Thread类 中的方法调用与线程状态关系如下图:
当发生如下情况时,线程将会进入阻塞状态:
-
线程调用
sleep()
方法主动放弃所占用的处理器资源 -
线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
-
线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。关于同步监视器的知识、后面将存更深入的介绍
-
线程在等待某个通知(notify)
-
程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法;
当前正在执行的线程被阻塞之后,其他线程就可以获得执行的机会。被阻塞的线程会在合适的时候重新进入就绪状态,注意是就绪状态而不是运行状态。也就是说,被阻塞线程的阻塞解除后,必须重新等待线程调度器再次调度它。
针对上面几种情况,当发生如下特定的情况时可以解除上面的阻塞,让该线程重新进入就绪状态:
- 调用sleep()方法的线程经过了指定时间
- 线程调用的阻塞式IO方法已经返回
- 线程成功地获得了试图取得的同步监视器
- 线程正在等待某个通知时,其他线程发出了个通知
- 处于挂起状态的线程被调甩了
resume()
恢复方法
补充说明: wait()和notify()、notifyAll()
这三个方法用于协调多个线程对共享数据的存取,所以必须在synchronized
语句块内使用。synchronized
关键字用于保护共享数据,阻止其他线程对共享数据的存取,但是这样程序的流程就很不灵活了,如何才能在当前线程还没退出synchronized
数据块时让其他线程也有机会访问共享数据呢?此时就用这三个方法来灵活控制。
wait()方法使当前线程暂停执行并释放对象锁标识,让其他线程可以进入synchronized
数据块,当前线程被放入对象等待池中。
当调用notify()
方法后,将从对象的等待池中移走一个任意的线程并放到锁标志等待池中,只有锁标志等待池中线程能够获取锁标志;如果锁标志等待池中没有线程,则notify()
不起作用。
notifyAll()则从对象等待池中移走所有等待那个对象的线程并放到锁标志等待池中。
注意 这三个方法都是java.lang.Object
的方法。