认识Java里的线程
认识Java里的线程
Java程序天生就是多线程的
一个Java程序从main()方法开始执行,然后按照既定的代码逻辑执行,看似没有其他线程参与,但实际上Java程序天生就是多线程程序,因为执行main()方法的是一个名称为main的线程。
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
public class MultiThread {
public static void main(String[] args) {
//获取Jav线程管理MXBean
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
//不需要获取同步的monitor和synchronizer信息,仅获取线程和线程堆栈信息
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
for(ThreadInfo threadInfo : threadInfos) {
System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
}
}
}
- [6] Monitor Ctrl-Break //监控Ctrl-Break中断信号的
- [5] Attach Listener //内存dump,线程dump,类信息统计,获取系统属性等
- [4] Signal Dispatcher // 分发处理发送给JVM信号的线程
- [3] Finalizer // 调用对象finalize方法的线程
- [2] Reference Handler//清除Reference的线程
- [1] main //main线程,用户程序入口
线程的生命周期
Thread类提供了六种状态
注意在Thread类中明确说明了java只有六种状态
新建状态(NEW)
当线程对象对创建后,即进入了新建状态,如:Thread thread1 = new MyThread();
运行状态(RUNNABLE)
Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
阻塞状态(BLOCKED)
处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。
等待状态(WAITING)
进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
超时等待(TIMED_WAITING)
该状态不同于WAITING,它可以在指定的时间后自行返回。
终止状态(TERMINATED)
线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
总结
这看上去挺复杂的,状态类型也比较多。但其实在操作系统层面,Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态,即前面我们提到的休眠状态。也就是说只要 Java 线程处于这三种状态之一,那么这个线程就永远没有 CPU 的使用权。所以 Java 线程的生命周期可以简化为下图:
其中,BLOCKED、WAITING、TIMED_WAITING 可以理解为线程导致休眠状态的三种原因。那具体是哪些情形会导致线程从 RUNNABLE 状态转换到这三种状态呢?而这三种状态又是何时转换回 RUNNABLE 的呢?以及 NEW、TERMINATED 和 RUNNABLE 状态是如何转换的?
JAVA中线程状态转换
RUNNABLE 与 BLOCKED 的状态转换
只有一种场景会触发这种转换,就是线程等待 synchronized 的隐式锁。synchronized 修饰的方法、代码块同一时刻只允许一个线程执行,其他线程只能等待,这种情况下,等待的线程就会从 RUNNABLE 转换到 BLOCKED 状态。而当等待的线程获得 synchronized 隐式锁时,就又会从 BLOCKED 转换到 RUNNABLE 状态。如果你熟悉操作系统线程的生命周期的话,可能会有个疑问:线程调用阻塞式 API 时,是否会转换到 BLOCKED 状态呢?
在操作系统层面,线程是会转换到休眠状态的,但是在 JVM 层面,Java 线程的状态不会发生变化,也就是说 Java 线程的状态会依然保持 RUNNABLE 状态。JVM 层面并不关心操作系统调度相关的状态,因为在 JVM 看来,等待 CPU 使用权(操作系统层面此时处于可执行状态)与等待 I/O(操作系统层面此时处于休眠状态)没有区别,都是在等待某个资源,所以都归入了 RUNNABLE 状态。而我们平时所谓的 Java 在调用阻塞式 API 时,线程会阻塞,指的是操作系统线程的状态,并不是 Java 线程的状态。
RUNNABLE 与 WAITING 的状态转换
总体来说,有三种场景会触发这种转换。
- 获得 synchronized 隐式锁的线程调用了无参数的 Object.wait() 方法。其中,wait() 方法我们在上一篇讲解管程的时候已经深入介绍过了,这里就不再赘述。
- 调用无参数的 Thread.join() 方法。其中的 join() 是一种线程同步方法,例如有一个线程对象 thread A,当调用 A.join() 的时候,执行这条语句的线程会等待 thread A 执行完,而等待中的这个线程,其状态会从 RUNNABLE 转换到 WAITING。当线程 thread A 执行完,原来等待它的线程又会从 WAITING 状态转换到 RUNNABLE。
- 调用 LockSupport.park() 方法。其中的 LockSupport 对象,也许你有点陌生,其实 Java 并发包中的锁,都是基于它实现的。调用 LockSupport.park() 方法,当前线程会阻塞,线程的状态会从 RUNNABLE 转换到 WAITING。调用 LockSupport.unpark(Thread thread) 可唤醒目标线程,目标线程的状态又会从 WAITING 状态转换到 RUNNABLE。
RUNNABLE 与 TIMED_WAITING 的状态转换
有五种场景会触发这种转换:
- 调用带超时参数的 Thread.sleep(long millis) 方法;
- 获得 synchronized 隐式锁的线程,调用带超时参数的 Object.wait(long timeout) 方法;
- 调用带超时参数的 Thread.join(long millis) 方法;
- 调用带超时参数的 LockSupport.parkNanos(Object blocker, long deadline) 方法;
- 调用带超时参数的 LockSupport.parkUntil(long deadline) 方法。
- 这里你会发现 TIMED_WAITING 和 WAITING 状态的区别,仅仅是触发条件多了超时参数。
从 NEW 到 RUNNABLE 状态
Java 刚创建出来的 Thread 对象就是 NEW 状态,而创建 Thread 对象主要有两种方法。一种是继承 Thread 对象,重写 run() 方法。
NEW 状态的线程,不会被操作系统调度,因此不会执行。Java 线程要执行,就必须转换到 RUNNABLE 状态。从 NEW 状态转换到 RUNNABLE 状态很简单,只要调用线程对象的 start() 方法就可以了。
从 RUNNABLE 到 TERMINATED 状态
线程执行完 run() 方法后,会自动转换到 TERMINATED 状态,当然如果执行 run() 方法的时候异常抛出,也会导致线程终止。有时候我们需要强制中断 run() 方法的执行,例如 run() 方法访问一个很慢的网络,我们等不下去了,想终止怎么办呢?Java 的 Thread 类里面倒是有个 stop() 方法,不过已经标记为 @Deprecated,所以不建议使用了。正确的姿势其实是调用 interrupt() 方法。
线程优先级
现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完就会发生线程调度,并等待下次分配。线程分配到的时间片多少也决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。
在Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1-10,在线程构建的时候可以通过setPriority(int)
方法来修改优先级,默认优先级为5,优先级高的线程分配时间片的数量要多于优先级低的线程。在设置优先级时,针对频繁阻塞(休眠或IO操作)的线程需要设置较高的优先级,而偏重计算的线程则设置较低的优先级,确保处理器不会被独占。在不同JVM以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class Priority {
private static volatile boolean notStart = true;
private static volatile boolean notEnd = true;
public static void main(String[] args) throws Exception {
List<Job> jobs = new ArrayList<>(16);
for(int i = 0; i < 10; ++i) {
int priority = i < 5 ? Thread.MIN_PRIORITY : Thread.MAX_PRIORITY;
Job job = new Job(priority);
jobs.add(job);
Thread thread = new Thread(job, "Thread:" + i);
thread.setPriority(priority);
thread.start();
}
notStart = false;
TimeUnit.SECONDS.sleep(10);
notEnd = false;
for(Job job : jobs) {
System.out.println("Job Priority: " + job.priority + ", Count: " + job.jobCount);
}
}
private static class Job implements Runnable {
private int priority;
private long jobCount;
Job(int priority) {
this.priority = priority;
}
@Override
public void run() {
while (notStart) {
Thread.yield();
}
while (notEnd) {
Thread.yield();
jobCount++;
}
}
}
}
运行该实例,输出如下:
Job Priority: 1, Count: 328139
Job Priority: 1, Count: 328468
Job Priority: 1, Count: 328122
Job Priority: 1, Count: 328002
Job Priority: 1, Count: 328111
Job Priority: 10, Count: 2763533
Job Priority: 10, Count: 2776199
Job Priority: 10, Count: 2770687
Job Priority: 10, Count: 2776199
Job Priority: 10, Count: 2765352
此结果是在win10, jdk10下的输出,可以看出优先级生效了。
Job Priority: 1, Count: 3976833
Job Priority: 1, Count: 4058807
Job Priority: 1, Count: 5225471
Job Priority: 1, Count: 4941778
Job Priority: 1, Count: 4190309
Job Priority: 10, Count: 4257979
Job Priority: 10, Count: 3897556
Job Priority: 10, Count: 4107731
Job Priority: 10, Count: 4068744
Job Priority: 10, Count: 4164109
此结果是在debian9, jdk8下的输出,可以看出优先级没有生效。
从上面两个输出来看,程序正确性不能依赖线程的优先级高低。
守护线程
Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)
将线程设置为Daemon线程。我们一般用不上,比如垃圾回收线程就是Daemon线程。
Daemon属性需要在启动线程前设置,不能在启动后设置。
Daemon线程被用作完成支持性工作,但是在Java虚拟机退出时Daemon线程中的finally块并不一定会执行。在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。
也可以理解为等程序的所有的用户线程结束后,守护线程也将结束。
注意:守护线程必须在start之前设置,否则会报错。
package chapter01.daemon;
import util.ThreadUtils;
import java.util.concurrent.TimeUnit;
public class ThreadDaemon {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
while (true) {
System.out.println("1111111111111");
ThreadUtils.sleep(1, TimeUnit.SECONDS);
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("22222222222");
ThreadUtils.sleep(1, TimeUnit.SECONDS);
}
});
/* Thread thread3 = new Thread(() -> {
while (true) {
System.out.println("3333333333333");
ThreadUtils.sleep(1, TimeUnit.SECONDS);
}
});*/
thread1.setDaemon(true);
thread1.start();
thread2.start();
//thread3.start();
}
}
在构建Daemon线程时,不能靠finally块中的内容来确保执行关闭或清理资源的逻辑。
java线程的使用
线程的创建
创建线程的方式有两种
继承Thread类
package chapter01.create;
/**
* 创建一个线程并运行
*/
public class threadTest extends Thread {
@Override
public void run() {
System.out.println("线程运行");
}
public static void main(String[] args) {
threadTest threadCreate = new threadTest();
threadCreate.start();
}
}
实现 Runnable 接口
实现Runnable 接口并交给Thread进行运行
package chapter01.create;
public class RunnableTest implements Runnable {
@Override
public void run() {
System.out.println("线程运行");
}
public static void main(String[] args) {
RunnableTest runnableTest = new RunnableTest();
Thread thread = new Thread(runnableTest);
thread.start();
}
}
Thread和Runnable的区别
Thread才是Java里对线程的唯一抽象,Runnable只是对任务(业务逻辑)的抽象。Thread可以接受任意一个Runnable的实例并执行。
注意
有些面试官会说实现线程的方式有三种 Thread、Runnable 以及Callable
但是按照java源码中Thread类中的注释说的实现类的防止只有两种
我们可以看下啊Thread类的源码
线程终止
线程自然终止
要么是run执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。
stop
暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。但是这些API是过期的,也就是不建议使用的 。
不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。正因为suspend()、resume()和stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法。
interrupt(中断)
/**
* 使用Runable的中断
*/
public class ThreadInterrupted implements Runnable {
private int i = 0;
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
i++;
System.out.println("线程正在运行");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (i > 10) {
Thread.currentThread().interrupt();
}
System.out.println(Thread.currentThread().isInterrupted());
}
}
public static void main(String[] args) {
new Thread(new ThreadInterrupted()).start();
}
}
安全的中止则是其他线程通过调用某个线程A的interrupt()方法对其进行中断操作, 中断好比其他线程对该线程打了个招呼,“A,你要中断了”,不代表线程A会立即停止自己的工作,同样的A线程完全可以不理会这种中断请求。因为java里的线程是协作式的,不是抢占式的。线程通过检查自身的中断标志位是否被置为true来进行响应,
线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()来进行判断当前线程是否被中断,不过Thread.interrupted()会同时将中断标识位改写为false。
如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、thread.wait等),则在线程在检查中断标示时如果发现中断标示为true,则会在这些阻塞方法调用处抛出InterruptedException异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为false。
不建议自定义一个取消标志位来中止线程的运行。因为run方法里有阻塞调用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取消标志。这种情况下,使用中断会更好,状态位如果跨线程改变状态必须使用volatile来保证可见性。
- 一般的阻塞方法,如sleep等本身就支持中断的检查。
- 检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可以避免声明取消标志位,减少资源的消耗。
注意:处于死锁状态的线程无法被中断
状态位
package chapter01.stop;
public class ThreadFlag implements Runnable {
protected long i = 0;
private boolean flag = false;
@Override
public void run() {
while (!flag) {
i++;
if (i > 100000) {
flag = true;
}
System.out.println(i);
}
}
public static void main(String[] args) throws InterruptedException {
ThreadFlag threadFlag = new ThreadFlag();
new Thread(threadFlag).start();
}
}
状态位就是用一个变量来标识线程的运行状态,如果需要停止了就就改变状态位的状态,但是状态位一定要使用 volatile 关键字,否在可能造成多线程状态下的不可见
package chapter01.stop;
public class ThreadInvisible implements Runnable {
protected long i = 0;
/**
* 不加volatile 会造成多线程的变量不可见,判断不会停止
*/
public boolean flag = false;
@Override
public void run() {
while (!flag) {
i++;
}
}
public static void main(String[] args) throws InterruptedException {
ThreadInvisible threadFlag = new ThreadInvisible();
new Thread(threadFlag).start();
Thread.sleep(1000);
threadFlag.flag = true;
}
}
stop和interrupt的区别
stop() 方法会真的杀死线程,不给线程喘息的机会,如果线程持有 ReentrantLock 锁,被 stop() 的线程并不会自动调用 ReentrantLock 的 unlock() 去释放锁,那其他线程就再也没机会获得 ReentrantLock 锁,这实在是太危险了。所以该方法就不建议使用了,类似的方法还有 suspend() 和 resume() 方法,这两个方法同样也都不建议使用了,所以这里也就不多介绍了。
而 interrupt() 方法就温柔多了,interrupt() 方法仅仅是通知线程,线程有机会执行一些后续操作,同时也可以无视这个通知。被 interrupt 的线程,是怎么收到通知的呢?一种是异常,另一种是主动检测。
只有当线程 A 处于 WAITING、TIMED_WAITING 状态时,如果其他线程调用线程 A 的 interrupt() 方法,会使线程 A 返回到 RUNNABLE 状态(中间可能存在BLOCKED状态),同时线程 A 的代码会触发 InterruptedException 异常。上面我们提到转换到 WAITING、TIMED_WAITING 状态的触发条件,都是调用了类似 wait()、join()、sleep() 这样的方法,我们看这些方法的签名,发现都会 throws InterruptedException 这个异常。这个异常的触发条件就是:其他线程调用了该线程的 interrupt() 方法。
当线程 A 处于 RUNNABLE 状态时,并且阻塞在 java.nio.channels.InterruptibleChannel 上时,如果其他线程调用线程 A 的 interrupt() 方法,线程 A 会触发 java.nio.channels.ClosedByInterruptException 这个异常;而阻塞在 java.nio.channels.Selector 上时,如果其他线程调用线程 A 的 interrupt() 方法,线程 A 的 java.nio.channels.Selector 会立即返回。
上面这两种情况属于被中断的线程通过异常的方式获得了通知。还有一种是主动检测,如果线程处于 RUNNABLE 状态,并且没有阻塞在某个 I/O 操作上,例如中断计算圆周率的线程 A,这时就得依赖线程 A 主动检测中断状态了。如果其他线程调用线程 A 的 interrupt() 方法,那么线程 A 可以通过 isInterrupted() 方法,检测是不是自己被中断了。
run和start的区别
Thread类是Java里对线程概念的抽象,可以这样理解:我们通过new Thread()其实只是new出一个Thread的实例,还没有操作系统中真正的线程挂起钩来。只有执行了start()方法后,才实现了真正意义上的启动线程。
start()方法让一个线程进入就绪队列等待分配cpu,分到cpu后才调用实现的run()方法,start()方法不能重复调用,如果重复调用会抛出异常。
而run方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方法并没有任何区别,可以重复执行,也可以被单独调用。
其他的线程相关方法
sleep方法
package chapter01.method;
import util.ThreadUtils;
import java.util.concurrent.TimeUnit;
public class ThreadSleep {
public static void main(String[] args) {
sleep1();
sleep2();
}
public static void sleep1() {
Thread thread = new Thread(() -> {
System.out.println("xxxxxxxxxxxxxxxxxx");
ThreadUtils.sleep(1, TimeUnit.SECONDS);
});
//thread.setPriority(100);
thread.start();
}
public static void sleep2() {
Thread thread = new Thread(() -> {
System.out.println("xxxxxxxxxxxxxxxxxx");
ThreadUtils.sleep(1, TimeUnit.SECONDS);
});
//thread.set
thread.start();
}
}
使当前线程(即调用该方法的线程)暂停执行一段时间,让其他线程有机会继续执行,但它并不释放对象锁,也不释放占用的资源。也就是说如果有synchronized同步快,其他线程仍然不能访问共享数据。注意该方法要捕捉异常。
例如有两个线程同时执行(没有synchronized)一个线程优先级为MAX_PRIORITY,另一个为MIN_PRIORITY,如果没有Sleep()方法,只有高优先级的线程执行完毕后,低优先级的线程才能够执行;但是高优先级的线程sleep(500)后,低优先级就有机会执行了。
总之,sleep()可以使低优先级的线程得到执行的机会,当然也可以让同优先级、高优先级的线程有执行的机会。
join方法
package chapter01.method;
import util.ThreadUtils;
import java.util.concurrent.TimeUnit;
public class ThreadJoin {
public static void main(String[] args) {
Thread thread1 = new Thread(()->{
for(int i=0;i<10;i++) {
System.out.println("111111111111111");
ThreadUtils.sleep(1, TimeUnit.SECONDS);
}
});
Thread thread2 = new Thread(()->{
try {
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<10;i++){
System.out.println("2222222222222");
ThreadUtils.sleep(1, TimeUnit.SECONDS);
}
});
thread1.start();
thread2.setDaemon(true);
thread2.start();
}
}
把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行 。
比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B
注意: t.join()方法只会使主线程进入等待池并等待t线程执行完毕后才会被唤醒。并不影响同一时刻处在运行状态的其他线程。
yield方法
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
yield()是将线程从运行状态变更为就绪状态,不会变为等待/睡眠/阻塞状态。
注意:yeid方法是不释放资源的。
package chapter01.method;
import util.ThreadUtils;
import java.util.concurrent.TimeUnit;
public class ThreadYield {
public static void main(String[] args) {
Thread thread1 = new Thread(()->{
// Thread.yield();
System.out.println("1111111111");
ThreadUtils.sleep(1, TimeUnit.SECONDS);
});
Thread thread2 = new Thread(()->{
System.out.println("22222222222");
ThreadUtils.sleep(1, TimeUnit.SECONDS);
});
thread1.start();
thread2.start();
}
}