上一篇我们已经了解进程和线程的相关概念CPU线程调度的基本原理,这回我们来看看java是怎么支持多线程的。
任何一个程序都必须要创建线程,特别是Java。不管任何程序都必须启动一个main函数的主线程; Java Web开发里面的定时任务、定时器、JSP和 Servlet、异步消息处理机制,远程访问接口RM等都离不开线程。
下面用一个例子来看看启动一个main方法,jvm会创建多少个线程
public static void main(String[] args) {
//Java 虚拟机线程系统的管理接口
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
//获取线程和线程堆栈信息
ThreadInfo[] threadInfoArray = threadMXBean.dumpAllThreads(false, false);
// 遍历线程信息,仅打印线程ID和线程名称信息
for (ThreadInfo threadInfo : threadInfoArray) {
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 //主线程,用户程序入口
创建启动线程是离不开两个类的,一个java.lang.Thread类,一个是java.lang.Runnable接口
在日常开发中我们有两种方法可以创建和启动新的线程
1.实现Runnable接口
private static class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("我实现了Runnable接口");
}
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
还有这种匿名内部类的简化写法
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("我是匿名内部类版本,我实现了Runnable接口");
}
}).start();
}
jdk1.8以后,Lambda表达式写法
new Thread(() -> System.out.println("我是Lambda表达式版本,我实现了Runnable接口"))
2.继承Thread类
private static class MyThread extends Thread {
@Override
public void run() {
System.out.println("我继承了Thread类");
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
上面不是说到,创建和启动一个线程,都离不开这两个类吗,但是继承Thread这种方式,并没有用到Runnable接口?
实际上在Thread类中已经实现了Runnable接口,我们重写其实还是Runnable的run方法。
不难看出run方法是没有返回值的,如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。而自从Java 1.5开始,就提供了Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。
Callable位于java.util.concurrent包下,它也是一个接口,在它里面也只声明了一个方法,只不过这个方法叫做call(),这是一个泛型接口,call()函数返回的类型就是传递进来的V类型。
Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的FutureTask。
FutureTask类实现了RunnableFuture接口,RunnableFuture继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
private static class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("子线程在进行计算");
Thread.sleep(3000);
return 1000;
}
}
public static void main(String[] args) {
MyCallable myCallable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<Integer>(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
System.out.println("主线程在执行任务");
try {
System.out.println("运行结果" + futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("所有任务执行完毕");
}
Java线程在运行的生命周期中可能处于下表所示的6种不同的状态,在给定的一个时刻,线程只能处于其中的一个状态。
初始状态(new) | 线程被实例化但还没有但还没有调用start()方法 | |
运行(runnable) | 就绪(ready) | Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running) |
运行中(running) | ||
阻塞状态(blocked) | 线程因为某种原因放弃了cpu 使用权,也即让出了cpu 时间片,暂时停止运行。 | |
等待状态(waiting) | 表示当前线程需要等待其他线程做出一些特定的动作(通知或中断) | |
超时等待(time_waiting) | 该状态不同于waiting,它可以在指定时间后自行返回 | |
终止状态(terminater) | 表示当前线程已执行完毕 |
run方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方法并没有任何区别,可以重复执行,也可以被单独调用。
Thread类是Java里对线程概念的抽象,可以这样理解:我们通过new Thread()其实只是new出一个Thread的实例,还没有操作系统中真正的线程挂起钩来。只有执行了start()方法后,才实现了真正意义上的启动线程。
start()方法让一个线程进入就绪队列等待分配CPU,分到CPU后才调用实现的run()方法start()方法不能重复调用,如果重复调用会抛出异常。
使当前线程让出CPU占有权,但让出的时间是不可设定的。也不会释放锁资源。
所有执行yield()的线程有可能在进入到就绪(ready)状态后会被操作系统再次选中马上又被执行。
把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程A中调用了线程B的Join()方法,A线程进入等待(waiting)状态,直到线程B执行完毕后,才会继续执行线程A。
Thread.sleep(long millis)和Thread.sleep(long millis,int nanos)静态方法强制当前正在执行的线程休眠(即暂停执行)进入超时等待(time-waiting)状态,该方法不会使当前线程释放锁。
sleep()是一个静态方法,一般直接用Thread类名.直接调用,sleep方法只能让当前线程睡眠。调用某一个线程类的对象t.sleep(),睡眠的不是t,而是当前线程。
在苏醒之后是不会返回到running状态,会返回到就绪(ready)状态,还需要等待CPU调度执行。所以线程实际等待时间往往大于sleep设置的时间。
Java中调用wait方法或者sleep方法都可以让线程进入waiting或者time-waiting状态,但是
wait是Object中的方法,而sleep则是Thread中的方法。
sleep可以在任何地方使用,而wait只可以在synchronized方法或synchronized块中使用。
sleep方法只会让出当前线程持有的时间片,而wait方法除了让出时间片还会让出当前线程持有的锁。
wait()和notify()系列都是Object对象的方法,调用该方法的线程进入 等待(waiting)状态,只有等待另外线程的通知或被中断才会返回.需要注意,调用wait()方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新去竞争锁,锁竞争到后才会执行wait方法后面的代码。
超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回
对于超时时间更细粒度的控制,可以达到纳秒
通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该线程获取到了对象的锁,没有获得锁的线程重新进入 等待(waiting)状态。调用notify()系列方法后,对锁无影响,线程只有在synchronized同步代码执行完后才会自然而然的释放锁,所以notify()系列方法一般都是synchronized同步代码的最后一行。
通知所有等待在该对象上的线程,在实际开发中尽可能用notifyAll(),谨慎使用notify(),因为notify()只会唤醒一个线程,我们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程。
是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify()/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
等待和通知的标准范式
等待方遵循如下原则。
synchronized(对象){
while (条件不满足){
对象.wait()
}
对应的处理逻辑
}
通知方遵循如下原则。
synchronized(对象){
改变条件
对象.notifyAll();
}
在调用wait()、notify()系列方法之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法、notify()系列方法,进入wait()方法后,当前线程释放锁,在从wait()返回前,线程与其他线程竞争重新获得锁, 执行notify()系列方法的线程退出调用了notifyAll()的synchronized代码块的时候后,他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出synchronized代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。