一. 前言
Thread类作为线程中最基础的类,本篇文章我们就来了解下该类的使用。
二、. Thread类
1. 继承结构及属性
首先,来看一下Thread类的继承结构及基础属性:
public
class Thread implements Runnable {
//...
private volatile char name[];
private int priority;
private Thread threadQ;
private long eetop;
/* Whether or not to single_step this thread. */
private boolean single_step;
/* Whether or not the thread is a daemon thread. */
private boolean daemon = false;
/* JVM state */
private boolean stillborn = false;
/* What will be run. */
private Runnable target;
/* The group of this thread */
private ThreadGroup group;
}
从上面可以看出,Thread类是实现了Runnable接口,其中的一些属性:name表示的是Thread的名称,priority表示线程的优先级,daemon表示是否是守护线程,stillborn表示虚拟机状态,target表示实际要执行的任务,其中线程优先级最大是10,最小是1,默认是5。
// 线程优先级
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
接下来我们主要来看一下Thread类的常用的方法。
2. 主要方法
2.1 start方法
start
方法,用来启动一个线程,当执行了start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源;注意不能重复调用start方法,调用start方法进行操作之后,Thread内部用于维护线程状态的变量threadStatus
会相应的发生变化。
// 私有变量,线程状态
private volatile int threadStatus = 0;
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
// ...
}
2.2 run方法
run
方法,首先run方法是不需要用户手动调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便会进入run方法体去执行具体的任务。所以,继承Thread类之后一定要重写run方法。不过该run方法实际上不是Thread类本身的,而是继承自Runnable接口中的run方法。
2.3 sleep方法
sleep
方法,让线程进入休眠状态,让出所占用的CPU资源,从上文我们可以知道,调用sleep方法之后,将会进入线程的TIMED_WAITING
状态,也就是休眠状态。
public static native void sleep(long millis) throws InterruptedException;
public static void sleep(long millis, int nanos) throws InterruptedException
sleep方法需要注意的一点是该方法不会释放锁,也就是说如果当前线程持有某个对象的锁(比如说添加了Synchronized关键字),然后调用了sleep方法,那其他线程将无法访问这个对象,因为虽然该线程休眠了,但该对象的锁没有释放。只有当该线程执行完成释放锁之后,其他线程才可以继续访问。
2.4 yield方法
yield
方法,调用该方法会让当前线程让出CPU资源,让CPU去执行与该线程具有相同优先级或更高优先级的线程。让出CPU资源后,线程由RUNNING
状态重回RUNNABLE
就绪状态,等待CPU的下此调用。yield方法和sleep方法有一点是相似的,就是都不会释放锁:
public static native void yield();
不过需要注意的是,yield方法的目的是让同等或更高优先级的线程能轮换执行,但这并不是绝对的,只能表示调用该方法之后,同等或更高优先级的线程有更高的机率来去执行,就和线程的优先级不是绝对的是一个道理。这里参考知乎:https://www.zhihu.com/question/35926652
2.5 join方法
join
方法,比如两个线程,当前主线程main和主线程中创建的线程thread,调用thread线程的join方法,这时候主线程会获得thread对象的锁,然后持有thread对象锁的线程会被挂起,也就是会阻塞当前主线程。接着去执行thread线程,直至thread线程中的代码执行完成或者执行一段时间之后,才会接着执行主线程。来看一个例子:
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
System.out.println("进入线程:" + Thread.currentThread().getName());
MyThread thread1 = new MyThread();
thread1.start();
System.out.println("线程" + Thread.currentThread().getName() + "等待");
thread1.join();
System.out.println("线程" + Thread.currentThread().getName() + "继续执行");
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("----------------------------");
System.out.println("进入线程: " + Thread.currentThread().getName());
try {
System.out.println("线程" + Thread.currentThread().getName() + "休眠5秒");
sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName() + "执行完毕");
System.out.println("----------------------------");
}
}
打印:
进入线程:main
线程main等待
----------------------------
进入线程: Thread-0
线程Thread-0休眠5秒
线程Thread-0执行完毕
----------------------------
线程main继续执行
例子引自:海子-Thread类的使用 ,可以看出,调用了thread1的join方法之后,main线程获取thread1对象的锁并进入阻塞状态,然后等待thread1线程执行完成之后再继续执行。join方法有三个重载方法,可以执行当前线程挂起的时间:
public final void join() throws InterruptedException {
public final synchronized void join(long millis, int nanos)
throws InterruptedException {
public final synchronized void join(long millis)
throws InterruptedException {
并且,join底层是通过Object的wait方法来实现,wait会让线程进入阻塞状态, 并且释放线程占有的锁,并交出CPU的占有资源。
不过需要注意的是,我们在main()方法里 调用了thread的join方法只会阻塞main方法所在的线程,因为join方法上的synchronized关键字的特性,哪个线程正在调用这个方法,哪个线程就会获取这个锁,然后该线程就会被挂起。所以说并不会阻塞其他相关的线程,比如:
// 添加线程测试类
class MyThread2 extends Thread {
@Override
public void run() {
System.out.println("进入线程: " + Thread.currentThread().getName());
System.out.println("线程" + Thread.currentThread().getName() + "执行完毕");
}
}
// main方法省略,其他不变
MyThread2 thread2 = new MyThread2();
MyThread thread1 = new MyThread();
thread2.start();
thread1.start();
// ...
thread1.join();
// ...
打印结果:
进入线程:main
线程main等待
进入线程: Thread-0
----------------------------
进入线程: Thread-1
线程Thread-0执行完毕
线程Thread-1休眠5秒
线程Thread-1执行完毕
----------------------------
线程main继续执行
由于线程的不确定性,打印结果会稍有不同,但MyThread2的线程并没有阻塞。另外线程挂起之后的唤醒操作,在Java源码中并没有体现,这块的实现是在JVM的源码中:
//一个c++函数:
void JavaThread::exit(bool destroy_vm, ExitType exit_type) ;
//这家伙是啥,就是一个线程执行完毕之后,jvm会做的事,做清理啊收尾工作,
//里面有一个贼不起眼的一行代码,眼神不好还看不到的呢,就是这个:
ensure_join(this);
//翻译成中文叫 确保_join(这个);代码如下:
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就是当前线程,是啥是啥?就是刚才说的b线程啊。
lock.notify_all(thread);
thread->clear_pending_exception();
}
这里JVM的源码转载自:https://www.zhihu.com/question/44621343/answer/97640972
2.6 interrupt方法
interrupt
方法,翻译为中断的意思,但interrupt方法的实际作用并不是中断线程,而是 "通知线程应该中断了" ,然后修改线程的中断状态,具体到底中断还是继续运行,应该由被通知的线程自己处理。
具体来说,当对一个线程,调用 interrupt() 时:
- 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常,仅此而已;
- 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响;
也就是说,interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行,也就是说,一个线程如果有被中断的需求,那么就可以这样做:
- 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。
- 在调用阻塞方法时正确处理InterruptedException异常;(例如,catch异常后就结束线程)
我们有两个方法来判断线程中断标志位的状态:
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
public boolean isInterrupted() {
return isInterrupted(false);
}
Thread.interrupted()
,静态方法,通过源码我们可以知道,这个方法是用于判断当前线程是否是中断状态,并且执行后会清除中断状态的标识。isInterrupted
,实例方法,同样是判断线程是否是中断状态,但判断的线程是执行该方法的线程,并且执行后并不会清除中断状态的标识。这一点,它们的源码很清晰的就说明了。
来看一个简单的例子:
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
MyThread thread1 = new MyThread();
thread1.start();
thread1.interrupt();
System.out.println("线程 " + thread1.getName() + "是否终止状态:" + thread1.isInterrupted());
}
}
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i< 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(i + " ");
}
System.out.println("线程执行完成: " + Thread.currentThread().getName());
}
}
打印结果:
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at jdk8.stream.MyThread.run(ThreadTest.java:17)
线程 Thread-0是否终止状态:true
0 1 2 3 4 线程执行完成: Thread-0
从上面我们可以看出,虽然调用了interrupt方法,但线程并没有结束,只是线程的中断状态修改为了true,并且对于sleep中的线程,也仅仅是抛出了一个异常而已。
这里参考自:https://www.zhihu.com/question/41048032/answer/89431513
2.7 一些get、set或者is方法
-
isInterrupted
方法,前面已经讲过,用于修改线程中断状态的方法;
-
public boolean isInterrupted()
-
isAlive
方法,判断线程是否存活,也就是是否已经启动但没有死掉;
-
public final native boolean isAlive();
-
setPriority
,getPriority
,设置和获取线程的优先级;
-
public final void setPriority(int newPriority)
public final int getPriority()
-
setName
,getName
,获取线程的名称;
-
public final synchronized void setName(String name)
public final String getName()
-
getThreadGroup
,获取线程所在的线程组,如果线程死掉了,那么返回null;
-
public final ThreadGroup getThreadGroup()
-
setDaemon
,isDaemon
,设置线程为守护线程以及判断线程是不是守护线程;
-
public final void setDaemon(boolean on)
public final boolean isDaemon()
-
getStackTrace
,获取当前线程的堆栈跟踪信息,返回一个StackTraceElement数组:
-
public StackTraceElement[] getStackTrace()
-
getAllStackTraces
,获取所有活动线程的堆栈跟踪映射:
-
public static Map<Thread, StackTraceElement[]> getAllStackTraces()
比如,我们想获取当前JVM中所有正在运行的线程:
Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
for (Thread thread : threadSet) {
System.out.println(thread.getName());
}
-
getId
,获取该线程的标识符,线程id是唯一的,在其生命周期内保持不变:
-
public long getId() {
return tid;
}
-
getState
,获取线程状态,前面已经了解过,线程状态对应于Thread的内部枚举类型State:
-
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
2.8 其他方法
currentThread
方法,静态方法,返回当前线程对象;
activeCount
,静态方法,返回当前线程组及其子组中活动线程的一个估计值,该方法会递归的遍历当前线程的线程组中的所有子组,返回的值只是一个估计值,因为当此方法遍历内部数据结构时,线程的数量可能会动态变化,并且可能会受到某些系统线程的存在的影响。此方法主要用于调试和监视目的。
enumerate
,静态方法,将当前线程组及其子组中的每个活动线程复制到指定的数组中。和 activeCount 方法是对应的,该方法通过调用ThreadGroup.enumerate()方法来实现。同样,该方法主要用于调试和监视目的。
可以简单的看个例子:
(代码来自:http://www.tutorialspoint.com/java/lang/thread_enumerate.htm)
public static void main(String[] args) {
Thread t = Thread.currentThread();
t.setName("Admin Thread");
System.out.println("current thread = " + t);
int count = Thread.activeCount();
System.out.println("currently active threads = " + count);
Thread th[] = new Thread[count];
// returns the number of threads put into the array
Thread.enumerate(th);
// output
for (int i = 0; i < count; i++) {
System.out.println(i + ": " + th[i]);
}
}
output:
current thread = Thread[Admin Thread,5,main]
currently active threads = 2
0: Thread[Admin Thread,5,main]
1: Thread[Monitor Ctrl-Break,5,main]
dumpStack
,静态方法,打印当前线程的堆栈跟踪信息,主要用于调试;
checkAccess
,final类型的实例方法,用于确认当前运行的线程是否具有修改该线程的权限;
holdsLock
,静态方法,当且仅当 当前线程持有指定对象上的监视锁时,返回true;
3. 已废弃方法
前面列举的都是目前版本(JDK 8)中正在使用的方法,除了这些方法之外,Thread还有一些已经废弃不推荐使用的方法。被废弃的方法大致有如下几个:stop
,destroy
,suspend
,resume
,countStackFrames
这几个。
被废弃的原因是因为这些方法基本上都是不安全的,使用的时候或多或少会出现一些额外的问题,比如stop
方法,会停止run方法的运行,然后释放掉对应的锁,这时候锁所保护的临界区就有可能出现状态不一致的情况,而这一状态有可能会暴露给其他线程,造成最终对象不一致的情况;而suspend
方法则自带出现死锁的可能性,更详细的原因官网有详细的解释:
Why Are Thread.stop, Thread.suspend, Thread.resume and Runtime.runFinalizersOnExit Deprecated?
三、Object类的几个方法
最后了,我们再来看一下Object的两个方法:notify
和wait
方法,这两个方法虽然是Object类的方法,但与Thread类是有很大的关系的,我们来了解下:
notify
,用于唤醒处于WAITING
状态的线程,如果该对象上有多个等待的线程,则任意选择其中的一个线程进行唤醒;notifyAll
,唤醒该对象上处于等待的所有线程;wait
,让当前线程进入等待状态,直到另一个线程调用此对象的notify()方法或notifyAll()方法,或者是设置了超时时间,等待超时时间结束。该方法有多个重载方法:
public final void wait() throws InterruptedException
// 超时时间如果是0,则不考虑超时时间,只能等待被唤醒
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException
不过需要注意的是,wait和notify方法在使用的时候都必须要先持有对象的锁,如果没有同步措施,对象有可能出现不确定的锁的状态。因为同一时刻,一个对象只能被一个线程占有,调用wait方法就表示将持有对象锁的线程释放锁,释放对应的CPU资源,然后进入等待状态。notify类似,如果线程在调用notify的时候没有获取到锁,那么notify有可能将其他处于waiting状态的线程唤醒,所以调用这两个方法的时候,必须先获得该对象的锁。
有关notify的API说明,可以简单看一下:
This method should only be called by a thread that is the owner of this object's monitor. A thread becomes the owner of the object's monitor in one of three ways:
- By executing a synchronized instance method of that object.
- By executing the body of a synchronized statement that synchronizes on the object.
- For objects of type Class, by executing a synchronized static method of that class.
Only one thread at a time can own an object's monitor.
而有关notify和notifyAll方法的区别,可以参考:java中的notify和notifyAll有什么区别?
本文主要参考自:
海子-Java并发编程:Thread类的使用
占小狼-JVM源码分析之Object.wait/notify实现
《Java并发编程实战》