快速入门Java多线程将会分为三篇文章:基础篇、原理篇、JDK 工具篇
进程与线程基本概念
进程和线程的提出极大的提高了操作系统的性能。进程让操作系统的并发性成为了可能,而线程让进程的内部并发成为了可能。
进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位,即CPU分配时间的单位 。
本质区别是是否单独占有内存地址空间和其他系统资源。
上下文切换通常是计算密集型的,意味着此操作会消耗大量的 CPU 时间,故线程也不是越多越好。如何减少系统中上下文切换次数,是提升多线程性能的一个重点课题。
Java 多线程入门类和接口
Thread 与 Runnable
Runnable接口与Thread类- 由于Java“单继承,多实现”的特性,Runnable接口使用起来比Thread更灵活。
- Runnable接口出现更符合面向对象,将线程单独进行对象的封装。
- Runnable接口出现,降低了线程对象和线程任务的耦合性。
- 如果使用线程时不需要使用Thread类的诸多方法,显然使用Runnable接口更为轻量。
Callable、Future 与 FutureTask
一般我们使用Runnable 但是Runnable没有返回值,所以引出Callable、Future 接口- Callable 一般配合线程池来使用,调用线程池的 submit 方法来让一个 Callable 接口执行。
- Future和Callable可以用来给任务增加可取消性,所以可以使用Callable来代替Runnable。如果只是单纯增加可取消功能使用 Future 而不关心其结果,则可以声明Future<?>形式类型,并返回 null 作为任务结果。
- Future 类的 cancel 方法是试图取消一个线程的执行,未必能取消成功。
Future
接口有一个实现类叫FutureTask
。FutureTask
是实现的RunnableFuture
接口的,而RunnableFuture
接口同时继承了Runnable
接口和Future
接口
Future
只是一个接口,而它里面的cancel
,get
,isDone
等方法要自己实现起来都是非常复杂的。所以JDK提供了一个FutureTask
类来供我们使用。
在很多高并发的环境下,有可能Callable和FutureTask会创建多次。FutureTask能够在高并发环境下确保任务只执行一次。
/**
*
* state可能的状态转变路径如下:
* NEW -> COMPLETING -> NORMAL
* NEW -> COMPLETING -> EXCEPTIONAL
* NEW -> CANCELLED
* NEW -> INTERRUPTING -> INTERRUPTED
*/
private volatile int state;
private static final int NEW = 0;
private static final int COMPLETING = 1;
private static final int NORMAL = 2;
private static final int EXCEPTIONAL = 3;
private static final int CANCELLED = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED = 6;
线程组和线程优先级
ThreadGroup和Thread的关系就如同他们的字面意思一样简单粗暴,每个Thread必然存在于一个ThreadGroup中,Thread不能独立于ThreadGroup存在。如果在new Thread时没有显式指定,那么默认将父线程(当前执行new Thread的线程)线程组设置为自己的线程组。Java程序中对线程所设置的优先级只是给操作系统一个建议,操作系统不一定会采纳。而真正的调用顺序,是由操作系统的线程调度算法决定的
一个线程默认是非守护线程,可以通过Thread类的setDaemon(boolean on)来设置。
总结来说,线程组是一个树状的结构,每个线程组下面可以有多个线程或者线程组。线程组可以起到统一控制线程的优先级和检查线程的权限的作用。
Java线程的状态及主要转化方法
操作系统线程主要有以下三个状态:
- 就绪状态(ready):线程正在等待使用CPU,经调度程序调用之后可进入running状态。
- 执行状态(running):线程正在使用CPU。
- 等待状态(waiting): 线程经过等待事件的调用或者正在等待其他资源(如I/O)。
// Thread.State 源码
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
NEW
处于NEW状态的线程此时尚未启动。这里的尚未启动指的是还没调用Thread实例的start()方法。调用start不可以两次,threadStatus变量会被改变,不为 0 直接抛异常
RUNNABLE
表示当前线程正在运行中。处于RUNNABLE状态的线程在Java虚拟机中运行,也有可能在等待其他系统资源(比如I/O)。
Java线程的RUNNABLE状态其实是包括了传统操作系统线程的ready和running两个状态的。
BLOCKED
阻塞状态。处于BLOCKED状态的线程正等待锁的释放以进入同步区。
WAITING
等待状态。处于等待状态的线程变成RUNNABLE状态需要其他线程唤醒(notify,notifyAll)。
调用如下3个方法会使线程进入等待状态:
- Object.wait():使当前线程处于等待状态直到另一个线程唤醒它;
- Thread.join():等待线程执行完毕,底层调用的是Object实例的wait方法;
- LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度。
TIME_WAITING
超时等待状态。线程等待一个具体的时间,时间到后会被自动唤醒。
调用如下方法会使线程进入超时等待状态:
- Thread.sleep(long millis):使当前线程睡眠指定时间;
- Object.wait(long timeout):线程休眠指定时间,等待期间可以通过notify()/notifyAll()唤醒;
- Thread.join(long millis):等待当前线程最多执行millis毫秒,如果millis为0,则会一直执行;
- LockSupport.parkNanos(long nanos): 除非获得调用许可,否则禁用当前线程进行线程调度指定时间;
- LockSupport.parkUntil(long deadline):同上,也是禁止线程进行调度指定时间;
TERMINATED
终止状态。此时线程已执行完毕。
Java线程间的通信
在Java中,锁的概念都是基于对象的,所以我们又经常称它为对象锁。线程和锁的关系,我们可以用婚姻关系来理解。一个锁同一时间只能被一个线程持有。也就是说,一个锁如果和一个线程“结婚”(持有),那其他线程如果需要得到这个锁,就得等这个线程和这个锁“离婚”(释放)。- 直接用synchronized上锁,线程需要不断地去尝试获得锁,如果失败了,再继续尝试。这可能会耗费服务器资源。
- 而等待/通知机制是另一种方式。Java多线程的等待/通知机制是基于
Object
类的wait()
方法和notify()
,notifyAll()
方法来实现的。需要注意的是等待/通知机制使用的是使用同一个对象锁,如果你两个线程使用的是不同的对象锁,那它们之间是不能用等待/通知机制通信的。 - 使用volatile信号量进行通信
- 管道是基于“管道流”的通信方式。JDK提供了
PipedWriter
、PipedReader
、PipedOutputStream
、PipedInputStream
。其中,前面两个是基于字符的,后面两个是基于字节流的。 - sleep方法是Thread类的一个静态方法。它的作用是让当前线程睡眠一段时间。sleep方法是不会释放当前的锁的,而wait方法会
- wait可以指定时间,也可以不指定;而sleep必须指定时间。
- wait释放cpu资源,同时释放锁;sleep释放cpu资源,但是不释放锁,所以易死锁。
- wait必须放在同步块或同步方法中,而sleep可以在任意位置
- ThreadLocal是一个本地线程副本变量工具类。内部是一个弱引用的Map来维护。严格来说,ThreadLocal类并不属于多线程间的通信,而是让每个线程有自己”独立“的变量,线程之间互不影响。它为每个线程都创建一个副本,每个线程可以访问自己内部的副本变量。如果开发者希望将类的某个静态变量(user ID或者transaction ID)与线程状态关联,则可以考虑使用ThreadLocal。
- InheritableThreadLocal类与ThreadLocal类稍有不同,Inheritable是继承的意思。它不仅仅是当前线程可以存取副本值,而且它的子线程也可以存取这个副本值。