线程简介
什么是线程
线程是操作系统中调度的最小单元,也叫轻量级进程。在一个进程中可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。
查看线程命令:jps -l
为什么使用多线程
-
更多的处理器核心
一个线程在一个时刻只能运行在一个处理器核心上。使用多线程技术,将计算机逻辑分配到多个处理器核心上,就可以显著减少程序的处理时间,并且随着更多处理器核心的加入而变得更有效率。
-
更快的响应时间
使用多线程技术可以将数据一致性不强的操作派发给其他线程处理(也可以使用消息队列),这些业务操作可以来缩短响应时间。
-
更好的编程模型
线程优先级
现代操作系统多采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完后会发生线程调度,并等待下次分配。
线程分配到的时间片的多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。
线程优先级高的线程分配时间片的数量要多于优先级低的线程。也就是使用了更多的处理器资源。
偏好设置:
- 频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级
- 偏重计算(需要较多CPU时间或者偏运算)的线程设置较低的优先级,确保处理器不会被独占
注意:有些操作系统会忽略线程优先级的设定。
为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。
线程优先级的高低不能保证线程的调用顺序,优先级高的线程获得资源调度的概率大,而优先级低的并非后调用。
线程的状态
状态名称 | 说明 |
---|---|
初始(NEW) | 新创建了一个线程对象,但还没有调用start()方法 |
运行(RUNNABLE) | Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行” |
阻塞(BLOCKED) | 表示线程阻塞于锁 |
等待(WAITING) | 进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断) |
超时等待(TIMED_WAITING) | 该状态不同于WAITING,它可以在指定的时间后自行返回 |
终止(TERMINATED) | 表示该线程已经执行完毕 |
Daemon 线程
Daemon 线程(亦称守护线程)是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。
当一个 JVM 中不存在非 Daemon 线程时,JVM 将会退出。
通过调用 Thread.setDaemon(true)
将线程设置为 Daemon 线程。
Daemon 属性需要在启动线程之前设置,不能在启动线程之后设置。
public class Daemon {
public static void main(String[] args) {
Thread thread = new Thread(new DaemonRunner(), "DaemonRunner");
thread.setDaemon(true);
thread.start();
}
private static class DaemonRunner implements Runnable {
@Override
public void run() {
try {
SleepUtils.second(10);
}finally {
// 不会打印,因为在main 线程执行结束后,JVM 中不存在非 Daemon 线程,虚拟机会退出。
System.out.println("DaemonThread finally run.");
}
}
}
}
在构建 Daemon 线程时,不能依靠 finally 块中的内容来确保执行关闭或者清理资源的逻辑
启动和终止线程
构造线程
在运行线程前首先要构造一个线程对象,设置相应的属性,如线程所属的线程组、线程优先级、是否是 Daemon 线程等信息。
// java.lang.Thread 中初始化线程部分代码
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
// 当前线程就是该线程的父线程
Thread parent = currentThread();
this.group = g;
// deamon、priority 属性与父线程一致
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
this.target = target;
setPriority(priority);
// 分配一个唯一线程 ID
tid = nextThreadID();
}
线程初始化后储存在堆内存中。
启动线程
线程对象在初始化完成之后,调用 start() 方法就可以启动该线程。线程 start() 的含义是:当前线程(即 parent 线程)同步告知 Java 虚拟机,只要线程规划器空闲,应立即调用 start() 方法的线程。
理解中断
中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。
通过调用线程的 interrupt() 方法对其进行中断操作,通过 isInterrupted() 来进行判断其是否被中断,也可以调用静态方法 Thread.interrupted() 对当前线程的中断标识位进行复位。
在Java中,有许多声明抛出InterruptedException 的方法(例如Thread.sleep(long millis)方法),这些方法在抛出InterruptedException 之前,Java虚拟机会先将该线程的中断标识位清除,然后抛出InterruptedException,此时调用isInterrupted() 方法将返回false。
/**
* 线程中断操作
* SleepThread 线程一直睡眠,BusyThread 线程一致运行
*/
public class Interrupted {
public static void main(String[] args) throws InterruptedException {
Thread sleepThread = new Thread(new SleepRunner(), "SleepThread");
sleepThread.setDaemon(true);
Thread busyThread = new Thread(new BusyRunner(), "BusyThread");
busyThread.setDaemon(true);
sleepThread.start();
busyThread.start();
// 休眠 5 秒,让 sleepThread 和 busyThread 充分运行
TimeUnit.SECONDS.sleep(5);
sleepThread.interrupt();
busyThread.interrupt();
System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted());
System.out.println("BusyThread interrupted is " + busyThread.isInterrupted());
// 防止 sleepThread 和 busyThread 立刻退出
SleepUtils.second(2);
}
private static class SleepRunner implements Runnable {
@Override
public void run() {
while (true){
SleepUtils.second(10);
}
}
}
private static class BusyRunner implements Runnable {
@Override
public void run() {
while (true){
}
}
}
}
// 输出
SleepThread interrupted is false
BusyThread interrupted is true
从结果可以看出,抛出InterruptedException 的线程SleepThread,其中断标识位被清除了,而一直运行的线程BusyThread的中断标识位没有被清除。
过期的suspend()、resume()和stop()
三个方法的作用分别是对线程的暂停,恢复和停止。
三个方法均已被弃用。
暂停和恢复操作可以用线程等待和通知来替代。
安全的终止线程
中断状态是线程的一个标识位,而中断操作可以用来取消或停止任务。另外可以利用一个boolean变量来控制是否需要停止任务并终止线程。
public static void main(String[] args) throws InterruptedException {
Runner one = new Runner();
Thread countThread = new Thread(one, "CountThread");
countThread.start();
// 睡眠1秒,main线程对CountThread进行中断,使CountThread能够感知中断而结束
TimeUnit.SECONDS.sleep(1);
countThread.interrupt(); // 线程中断操作
Runner two = new Runner();
countThread = new Thread(two, "CountThread");
countThread.start();
// 睡眠1秒,main线程对Runner two进行取消,使CountThread能够感知on为false而结束
TimeUnit.SECONDS.sleep(1);
two.cancel(); // 线程取消操作
}
private static class Runner implements Runnable {
private long i;
private volatile boolean on = true;
@Override
public void run() {
while (on && !Thread.currentThread().isInterrupted()) {
i++;
}
System.out.println("Count i = " + i);
}
void cancel() {
on = false;
}
}
线程间通信
volatile 和 synchronized 关键字
volatile
volatile 关键字可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。
过多的使用volatile会降低程序执行的效率。
synchronized
synchronized 关键字可以修饰方法或者以同步块的形式来进行使用,确保多线程在同一时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。
同步块的实现使用了monitorenter 和 monitorexit指令,而同步方法则是依靠方法修饰符上的 ACC_SYNCHRONIZED 来完成。无论哪种实现方式,本质是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是在同一时刻只能由一个线程获取由synchronized所保护对象的监视器。
没有获取到监视器的线程会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态。
等待/通知机制
等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。
// WaitThread 是消费者,NotifyThread 是生产者。
public class WaitNotify {
static boolean flag = true;
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread waitThread = new Thread(new Wait(), "WaitThread");
waitThread.start();
TimeUnit.SECONDS.sleep(1);
Thread notifyThread = new Thread(new Notify(), "NotifyThread");
notifyThread.start();
}
private static class Wait implements Runnable {
@Override
public void run() {
// 加锁,拥有lock的monitor
synchronized (lock) {
// 当条件不满足时,继续wait,同时释放了lock的锁
while (flag) {
try {
System.out.println(Thread.currentThread() + "flag is true. wait @ " +
new SimpleDateFormat("HH:mm:ss").format(new Date()));
lock.wait(); // 等待,释放掉lock的锁
} catch (Exception e) {
e.printStackTrace();
}
}
// 条件满足时,完成工作
System.out.println(Thread.currentThread() + "flag is false. running @ " +
new SimpleDateFormat("HH:mm:ss").format(new Date()));
}
}
}
private static class Notify implements Runnable {
@Override
public void run() {
// 加锁,拥有lock的monitor
synchronized (lock) {
// 获取lock的锁,然后进行通知,通知时不会释放lock的锁
// 直到当前线程释放了lock后,WaitThread才能从wait方法中返回
System.out.println(Thread.currentThread() + "hold lock. notify @ " +
new SimpleDateFormat("HH:mm:ss").format(new Date()));
lock.notifyAll();
flag = false;
SleepUtils.second(5);
}
// 再次加锁
synchronized(lock){
System.out.println(Thread.currentThread() + "hold lock again. sleep @ "+
new SimpleDateFormat("HH:mm:ss").format(new Date()));
SleepUtils.second(5);
}
}
}
}
注意:
- 使用wait()、notify()和notifyAll()时需要先对调用对象加锁。
- 调用wait()方法后,线程状态由 RUNNING 变为 WAITING,并将当前线程放置到对象的等待队列。
- notify()或着notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要等待调用notify()或notifyAll()的线程释放锁后,等待线程才有机会从wait()返回。
- notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则移动全部的等待线程,被移动的线程状态由WAITING变为BLOCKED。
- 从wait()方法返回的前提时获得了调用对象的锁。
管道输入/输出流
管道输入/输出流主要用于线程之间的数据传输,而传输的媒介是内存。
实现类:PipedOutputStream、PipedInputStream、PipedReader、PipedWriter。
public class Piped {
public static void main(String[] args) throws IOException {
PipedWriter out = new PipedWriter();
PipedReader in = new PipedReader();
// 必须将输出流和输入流进行连接,否则会抛出IOException
out.connect(in);
Thread printThread = new Thread(new Print(in), "PrintThread");
printThread.start();
int receive;
try {
while ((receive = System.in.read()) != -1) {
out.write(receive);
}
} finally {
out.close();
}
}
private static class Print implements Runnable {
private PipedReader in;
Print(PipedReader in) {
this.in = in;
}
@Override
public void run() {
int receive;
try {
while ((receive = in.read()) != -1) {
System.out.print((char) receive);
}
} catch (IOException ignored) {
}
}
}
}
// 输入一组字符串,然后原样输出
Hello Piped;
Hello Piped;
对于Piped类型的流,必须进行绑定,也就是调用connect()方法。
Thread.join()
如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。
public class Join {
public static void main(String[] args) throws InterruptedException {
Thread previous = Thread.currentThread();
for (int i = 0; i < 10; i++) {
// 每个线程拥有前一个线程的引用,需要等待前一个线程终止,才能从等待中返回。
Thread thread = new Thread(new Domino(previous), String.valueOf(i));
thread.start();
previous = thread;
}
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + " terminate.");
}
private static class Domino implements Runnable {
private Thread thread;
Domino(Thread thread) {
this.thread = thread;
}
@Override
public void run() {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " terminate.");
}
}
}
参考:《Java并发编程的艺术》