1、创建线程
1.1 Thread
首先,看下JDK对Thread的定义:
thread是程序中可以执行的线程。JVM允许一个程序同时有多个并发执行的线程。任何一个线程都会有一个优先级,优先级高的线程会优先于优先级低的线程执行。默认情况,线程的初始优先级和创建它的线程优先级保持一致;任何一个线程都可能被设置为守护线程,当且仅当创建新线程的线程为守护线程时,新线程为守护线程。
当JVM启动之后,通常会有一个非守护线程(它通常调用某个指定类的main方法)。JVM会持续的执行线程直到出现以下两种情况:
- 调用Runtime类的exit()方法,并且安全管理器允许进行exit操作。
- 所有的非守护线程已经消亡,无论是正常执行完成,或者发生异常退出。
其次,看下JDK提供的创建线程的方式:
JDK推荐两种创建可执行任务线程的方式。一种是定义一个继承了Thread的类,JDK的示例代码如下:
//定义一个继承Thread的类
class PrimeThread extends Thread {
long minPrime;
PrimeThread(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
//创建线程类并执行
PrimeThread p = new PrimeThread(143);
p.start();
创建线程的另一种方式是定义一个实现了Runnable接口的类,然后在该类中实现run()方法。这个类的实例可以在创建Thread时作为参数传递进去,然后启动创建的Thread实例。JDK的示例代码如下:
//定义一个实现Runnable接口的类
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
//创建一个实例作为参数传递给新创建的线程,然后启动线程
PrimeRun p = new PrimeRun(143);
new Thread(p).start();
最后,看下JDK对线程其他描述:
出于标识的目的,每个线程都有一个名称。但是不要求线程的名称唯一,就是说多个线程可以有相同的名称。当创建线程时未能指定名称,JVM会为其生成一个名称。除非另有说明,往Thread类的构造函数或者方法中传递null都会抛出NullpointerException。
以上是JDK对Thread类的描述,比较简单。值得注意的是,看Thread的源码时发现Thread类其实也实现了Runnable接口,下面看下Runable的描述。
1.2 Runnable
看下JDK对Runnable接口的描述:
当一个类的实例要被一个线程执行的时候,可以通过实现Runnable接口达到。这个类中必须实现Runnable接口的run()方法,此接口旨在为那些活跃的线程执行代码任务提供公共协议。实现Runnable的类可以通过实例化一个线程实例并将其自身作为一个目标传入而不需要对Thread进行子类化来运行。在大多数情况下,程序员只是打算重写run()方法儿不打算重写Thread的其他方法,这时就应该使用Runnable接口。这一点很重要,因为除非程序员需要修改或者增强类的行为,否则不应该对类进行子类化。
1.3 一点儿思考
现在我们已经知道了创建可执行任务线程的两种方式,一种继承Thread类,另一种实现Runnable接口。通过查看源码我们不难发现JDK设计的核心还是Thread类,Runnable接口是一种设计层面的优化补充,使得程序员可以更简单、更灵活的达到目的。为什么这么说呢?
- Thread是多线程的一个核心类,是一个比较庞大的类。通常我们创建线程只为执行相应的任务,我们的重点是在执行的任务上,所以我们的编码核心是任务(run()方法),这种情况下使用实现Runnable接口的方式显然更简洁合适,除非我们要修改或者增强Thread类。
- java是单继承多实现机制,如果我们使用继承Thread类的方式,那编写的类将难以继承其他的类。实现Runnable接口的方式显然比子类化Thread的方式更灵活。
另外,看源码时发现Thread启动方法start()中核心代码start0()是native方法,看不到源码,但是不难猜测到这里面执行的内容应该包括线程的创建、资源的分配、线程的启动等方法,也就是说线程的执行的核心部分是JDK的底层帮我们实现,而且对开发者隐藏了实现细节。
2. 线程的生命周期
上面是一张java生命周期的简图,图中可以看出java的生命周期有五个状态,我们看下这些状态在JDK中的定义。
public enum State {
/**
* new状态代表线程刚被创建但是尚未启动
*/
NEW,
/**
* runnable状态代表线程正在虚拟机中运行,但是可能正在等待操作系统的资源,如处理器
*/
RUNNABLE,
/**
* blocked状态代表线程被阻塞,等待着获取monitor lock以进入或者重新进入同步代码块/方法
*/
BLOCKED,
/**
* 线程执行了下面方法中的其中之一会进入waitting状态:
* Object.wait with no timeout
* Thread.join with no timeout
* LockSupport.park
* 一个处于waiting状态的线程等待着其他线程执行一个特定的动作
* 例如,一个调用Object.wait()方法进入waiting状态的线程等待
* 另一个线程调用Object.notify()或者Object.notifyAll()
* 一个调用Thread.join()方法进入waiting状态的线程等待着一个
* 特定的线程退出。
*/
WAITING,
/**
* 线程执行了下面方法中的其中之一会进入waitting状态:
* Thread.sleep
* Object.wait with timeout
* Thread.join with timeout
* LockSupport.parkNanos
* LockSupport.parkUntil
*/
TIMED_WAITING,
/**
* 线程已经完成执行
*/
TERMINATED;
}
3. 线程的基本操作
3.1 线程中断
在Java中,线程中断是一种重要的线程协作机制。它并不会使线程立即退出,而是给线程发送一个通知,目标线程接收到通知后自行决定如何处理。这种处理方式是为了避免线程立即无条件退出造成的数据不一致等问题。线程中断相关的三个方法和JDK注释如下:
/*
* 除非当前线程中断自己(这总是允许的),否则将调用该线程的checkAccess方法,
* checkAccess可能导致抛出SecurityException。
* 这个线程在调用Object的wait()、wait(long)或wait(long, int)方法、
* 或join()、join(long)、join(long, int)、sleep(long)或sleep(long, int)
* 方法处于阻塞状态时,调用interrupt() 方法将会抛出InterruptedException异常,然后中
* 断状态将被清除。
* 如果一个线程阻塞在InterruptibleChannel通道的IO操作上,调用interrupt() 方法
* InterruptibleChannel通道将会被关闭,中断状态也会被设定,并且抛出
* java.nio.channels.ClosedByInterruptException异常。
* 如果线程阻塞在java.nio.channels.Selector上,调用interrupt() 方法,线程的中
* 断状态将被设置,它将立即从选择操作返回,可能带有非零值,就像调用了选择器的
* wakeup 方法一样。
* 如果前面的约束都不存在,那么这个线程的中断状态将被设置。
* interrupt()作用在非活动的线程上是没有作用的
public void interrupt()
/*
* 测试此线程是否已被中断。线程的中断状态不受此方法的影响。
* 如果一个线程中断被忽略,因为在中断时一个线程不是活动的,
* 那么这个方法将返回false
*/
public boolean isInterrupted()
/*
*测试当前线程是否已被中断。此方法清除线程的中断状态。换句
* 话说,如果这个方法连续被调用两次,那么第二次调用将返回
* false(除非当前线程在第一次调用清除了它的中断状态之后,
* 在第二次调用检查它之前再次被中断)。
* 如果一个线程中断被忽略,因为在中断时一个线程不是活动的,那么这个方法将返回false。
public static boolean interrupted()
3.2 线程休眠
Thread.sleep()方法会让当前线程休眠若干时间,看下JDK的定义和注释。
/**
* 该方法会使当前正在执行的线程休眠(暂时停止执行)指定的毫秒数。
* 线程休眠时不释放任何锁
* 其他任何线程中断该休眠线程时,该休眠线程会抛出InterruptedException。同样,
* 抛出InterruptedException异常会清理掉线程的中断状态
*/
public static native void sleep(long millis) throws InterruptedException;
3.3 等待线程结束 join
join解决了线程间协作的一种情况:一个线程要等待另一个线程执行结束之后才能执行。看下JDK的定义和注释。
public final void join() throws InterruptedException {
join(0);
}
/**
* 等待线程执行完成或者指定的等待时间耗尽。指定的mills参数为零时,表示无限等待直至线程
* 执行完成。
* 该方法的实现是基于this.isAlive条件下的this.wait方法的循环调用。当一个线程执行完毕,
* this.notifyAll方法将会被调用。推荐不要在线程实例上同时再使用wait、notify或者notifyAll
* 方法。
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
3.4 线程谦让 yield
yield是Thread的一个静态方法,一旦执行,它会使当前当前线程释放CPU。但是要注意,让出CPU不代表当前线程不执行了。当前线程CPU之后,还会进行CPU资源的争夺,当时能不能争夺成功,就不一定了。先看下JDK的源码和注释:
/**
* 告诉调度器当前线程愿意产生它当前对处理器的使用。调度器可以忽略这个提示。
* Yield是一种启发式的尝试,旨在提高线程之间的相对进度,否则会过度使用CPU。
* 它的使用应该结合详细的剖析和基准测试,以确保它有预期的效果。
* 使用这种方法很少是合适的。它对于调试或测试目的可能很有用,因为它可以帮助
* 重现由于竞争条件造成的bug。
*
public static native void yield();
sleep()和yeild()方法,同样都是当前线程出让处理器资源,而他们之间不同的是。sleep()出让的资源其他线程都可以去竞争,而yeild()出让的处理器资源只允许与当前线程具有相同优先级的线程能够争夺。
3.5 wait 和 notify
为了支持多线程之间的协作,JDK提供了两个重要的方法:wait()和notify()。和前面几个方法不同的是,wait()和notify()方法是Object的实例方法。
- wait() Object类的一个方法。假如某线程内触发了某个Object实例的wait()方法会使当前线程进入等待,直至其他线程调用了同一Object实例的notify或者notifyAll方法。线程在调用某个Object实例的wait()方法之前必须已经拥有了该Object实例的monitor(可以理解为锁)。当线程内触发了Object.wait()方法时,该线程将会释放其持有的Object的monitor。等到其他线程中触发了Object.notify()或者Object.notifyAll()方法,该线程再尝试获得Object的monitor,获得Object的monitor之后,该线程得以继续往下执行。
- wait(long mileSeconds) Object类的一个native方法,wait()方法的本质是对该方法的调用wait(0),也因此该方法的使用包含了wait()的全部功能。线程在调用某个>Object实例的wait(long mileSeconds)方法之前也必须拥有该Object实例的monitor。当线程内触发了Object.wait(long mileSeconds)方式时,该线程会进入休眠状态,直到其他线程触发了同一Object实例的notify()或者notifyAll()方法,再或者休眠时间达到mileSeconds参数指定的时间。
该方法会是当前线程将自己放入Object实例的等待集合当中,并且会出让所有Object实例相关的锁,直到出现以下四种情况:
- 1.其他线程触发了同一Object实例的notify()方法,而且恰巧该休眠线程正好被选中唤醒
- 2.其他线程触发了同一Object实例的notifyAll()方法
- 3.该休眠线程被其他线程中断
- 4.指定时间已经过去。这里注意,如果指定参数为0,则不需要考虑此规则。
线程被唤醒之后,其将会从Object实例的等待集合中删除并且进入可调度状态。然后它会和其他线程一同竞争Object实例的同步锁,一旦获取成功之后,线程关于Object实例的同步权都会恢复至wait(long mileSeconds)方法调用之前,线程从wait()方法中返回,可以往下继续进行。
-
notify() 依然是一个native方法。触发Object某个实例的notify()方法时会随机唤醒一个等待该Object实例锁的线程(线程调用了某个wait方法)。线程被唤醒并不代表线程可以往下执行,而是需要等到触发Object实例的notify()方法的线程出让了Object实例的相关锁并且在所得抢夺过程中获得了锁才可以继续往下执行。
-
notifyAll() 也是一个native方法。触发Object某个实例的notify()方法时会唤醒所有等待该Object实例锁的线程。其他功能和notify()方法一样。
wait和notify小结
- 当调用wait时,首先需要确保调用了wait方法的线程已经持有了对象的锁。
- 当调用wait时,该线程就是释放掉这个对象的锁,然后进入到等到状态。
- 当线程调用了wait方法后进入等待状态时,它就可以等待其它线程调用相同对象的notify或notifyAll方法来使得自己被唤醒。
- 一旦这个线程被其他线程唤醒后,该线程就会与其他线程一同开始竞争这个对象的锁(公平竞争);只有该线程获取到了这个对象的锁后,线程才会继续往下执行
- 调用wait方法的代码片段需要放在一个synchronized块或是synchronized方法中,这样才可以确保线程在调用wait方法前已经取得了对象的锁。
- 当调用对象的notify方法时,它会随机唤醒该对象等待集合(wait set)中的任意一个线程,当一个线程被唤醒后,它就会与其他线程一同竞争对象的锁。
- 当调用对象的notifyAll方法时,它会唤醒该对象等待集合(wait set)中的所有线程,这些线程被唤醒后,又会开始竞争对象的锁。
- 在某一时刻,只有唯一一个线程可以拥有对象的锁。
4.守护线程daemon
守护线程是一种特殊的线程,在后台完成一些系统性的服务,比如垃圾回收线程、JIT线程就可以理解为守护线程。与之对应的是守护线程,用户线程可以认为是系统的工作线程。如果用户线程全部结束,也就意味着程序无事可做了。守护线程的守护对象也就不复存在,那么整个应用程序就自然应该结束。因此,当一个java应用内,只有守护线程时,java虚拟机就会自然退出。
学习参考书籍文章 《java高并发程序设计》
juejin.cn/post/684490…