Daemon、Interrupt、Wait/Notify、Join、ThreadLocal、Pipe
关于线程
现代操作系统在运行一个程序时,会为其创建一个进程。现代操作系统调度的最小单元是线程,也叫轻量级进程。
在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等,并且能够访问共享的内存变量。
Daemon
daemon线程是一种支持型线程,当一个Java虚拟机不存在非daemon线程的时候,Java虚拟机就会退出。
一个生动的例子
public class DaemonExample {
public static void main(String[] args) {
Thread thread = new Thread(new DaemonRunner(), "DaemonRunner");
thread.setDaemon(true);
thread.start();
}
static class DaemonRunner implements Runnable {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(10);
} catch (Exception e) {
} finally {
System.out.println("DaemonThread finally run.");
}
}
}
}
运行Daemon程序,可以看到终端没有任何输出。
Interrupt
中断可以理解为线程的一个标识位属性,它表示一个运行中的程序是否被其他程序进行了中断操作。中断好比其他线程对该线程打了个招呼。其他线程通过调用该线程的interrupt()方法对其进行中断操作。
线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。
如果该线程处于终结状态,即使该线程被中断过,调用该线程对象的isInterrupted()时依旧会返回flase。
许多声明抛出InterruptedException的方法在抛出InterruptedException之前,Java虚拟机会先将该线程的中断标识位清除,此时调用isInterrupted()方法将会放回false。
一个生动的例子
public class InterruptExample {
public static void main(String[] args) throws InterruptedException {
Thread sleepThread = new Thread(new SleepRunner(), "SleepThread");
sleepThread.setDaemon(true);
Thread busyThread = new Thread(new BusyRnner(), "BusyThread");
busyThread.setDaemon(true);
sleepThread.start();
busyThread.start();
TimeUnit.SECONDS.sleep(5);
sleepThread.interrupt();
busyThread.interrupt();
System.out.println("BusyThread interrupted is " + busyThread.isInterrupted());
SleepUtils.second(30);
}
static class SleepRunner implements Runnable {
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread() + " sleep...");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
System.out.println("SleepThread interrupted is " + Thread.currentThread().isInterrupted());
System.out.println(Thread.currentThread() + " interrupted.");
}
}
}
}
static class BusyRnner implements Runnable {
@Override
public void run() {
while (true) {
}
}
}
}
可以看到,抛出InterruptException的线程SleepThread,其中断标志位为false,而一直忙碌运作的线程BusyThread,中断标识位为true。
Wait/Notify
相关方法如下:
方法名称 | 描述 |
---|---|
notify() | 通知一个在对象上等待的线程,试其从wait()方法放回 |
notifyAll() | 通知所有等待在该对象上的线程 |
wait() | 进入WAITING状态,只有等待另外线程的通知或被中断才会放回 |
wait(long) | 超时等待一段时间,如果没有通知就返回 |
wait(long, int) | 对超时时间更细粒度的控制,可以达到纳秒 |
一个线程A调用了对象O的wait方法进入等待状态,而另一个线程B调用了对象O的notify()或notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,继续执行后续操作。
一个生动的例子
public class WaitNotifyExample {
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(2);
Thread notifyThread = new Thread(new Notify(), "NotifyThread");
notifyThread.start();
}
static class Wait implements Runnable {
@Override
public void run() {
synchronized (lock) {
while (flag) {
try {
System.out.println(Thread.currentThread() + " flag is true. wait @ "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
lock.wait();
} catch (InterruptedException e) {
}
}
System.out.println(Thread.currentThread() + " flag is flase. running @ "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
}
}
}
static class Notify implements Runnable {
@Override
public void run() {
synchronized (lock) {
System.out.println(Thread.currentThread() + " hold lock. notify @ "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
lock.notifyAll();
flag = false;
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
}
}
synchronized (lock) {
System.out.println(Thread.currentThread() + " hold lock again. sleep @ "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
}
}
}
}
}
- 使用wait()、notify()和notifyAll时需要先对调用对象加锁。
- 调用wait()方法后,线程状态由running变为waiting,释放锁并放置到等待队列中。
- 从wait()方法返回的前提是获得了调用对象的锁。
运行过程如下图所示:
等待/通知的经典范式
等待方遵循如下原则:
- 获取对象的锁
- 如果条件不满足,调用wait()方法,被通知后仍要检查条件
- 条件满足则执行对应的逻辑
对应的伪代码如下:
synchronized(对象) {
while(条件不满足) {
对象.wait();
}
对应的处理逻辑
}
通知方遵循如下原则:
- 获得对象的锁
- 改变条件
- 通知对象上的线程
对应的伪代码如下:
synchronized(对象) {
改变条件
对象.notify(); / 对象.notifyAll();
}
Join
如果一个线程A执行了thread.join()方法语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。
线程Thread除了提供join()方法之外,还提供了join(long)和join(long, int)两个具备超时特性的方法。
一个生动的例子
public class JoinExample {
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(2);
System.out.println(Thread.currentThread().getName() + " terminate.");
}
static class Domino implements Runnable {
private Thread thread;
public Domino(Thread thread) {
this.thread = thread;
}
@Override
public void run() {
try {
thread.join();
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + " terminate.");
}
}
}
输出如下:
main terminate.
0 terminate.
1 terminate.
2 terminate.
3 terminate.
4 terminate.
5 terminate.
6 terminate.
7 terminate.
8 terminate.
9 terminate.
可以看出每个线程终止的前提是前驱线程的终止,每个线程等待前驱线程终止后,才从join()方法返回。
ThreadLocal
线程本地存储为使用相同变量的每个不同的线程都创建不同的存储。因此,如果你有5个线程都要使用变量x所表示的对象,那么线程本地存储会生成5个用于x不同的存储块。
一个生动的例子
public class ThreadLocalExample {
public static final ThreadLocal<Integer> X_THREADLOCAL = new ThreadLocal<>();
public static void increment() {
X_THREADLOCAL.set(0);
for (int i = 0; i < 5; i++) {
int value = X_THREADLOCAL.get();
System.out.println(Thread.currentThread().getName() + " : " + value);
X_THREADLOCAL.set(value + 1);
}
}
public static void main(String[] args) throws InterruptedException {
ThreadLocalExample.increment();
new Thread(new StaticThreadLocal(), "staticThreadLocal-1").start();
new Thread(new StaticThreadLocal(), "StaticThreadLocal-2").start();
}
static class StaticThreadLocal implements Runnable {
@Override
public void run() {
ThreadLocalExample.increment();
}
}
}
运行这个程序时,可以看到每个单独的线程都被分配了自己的存储,因为它们每个都需要跟踪自己的计数值,即便只有一个X_THREADLOCAL对象。
Pipe
管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它们主要用于线程之间的数据传输,传输的媒介是内存。
管道输入/输出主要包括了4种具体实现:PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种面向字节,后两种面向字符。
一个生动的例子
package top.leagle.artofconcurrency.chapter4;
import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;
import java.util.concurrent.TimeUnit;
public class PipeExample {
public static void main(String[] args) throws IOException {
Sender sender = new Sender();
Receiver receiver = new Receiver(sender);
Thread senderThread = new Thread(sender, "SenderThrad");
Thread receiverThread = new Thread(receiver, "ReceiverThread");
senderThread.start();
receiverThread.start();
}
static class Sender implements Runnable {
private PipedWriter out = new PipedWriter();
@Override
public void run() {
try {
for (char c = 'A'; c <= 'C'; c++) {
out.write(c);
TimeUnit.SECONDS.sleep(1);
}
} catch (IOException e) {
System.out.println(e + " Sender write exception");
} catch (InterruptedException e) {
System.out.println(e + " Sender sleep interrupted");
}
}
public PipedWriter getPipedWriter() {
return out;
}
}
static class Receiver implements Runnable {
private PipedReader in;
public Receiver(Sender sender) throws IOException {
in = new PipedReader(sender.getPipedWriter());
}
@Override
public void run() {
try {
while (true) {
System.out.println("Read " + (char) in.read());
}
} catch (IOException e) {
System.out.println(e + " Receiver read exception");
}
}
}
}
参考
- Java并发编程的艺术[书籍]