# 线程
本篇讲解 Java 的线程基础。
谈起线程的状态(生命周期),操作系统层面上和 Java 层面上是不同的:
操作系统:初始状态(NEW),可运行状态(READY),运行状态(RUNNING),等待(WAITING),终止状态(TERMINATED)。
Java:新建(New),可运行(Runable),阻塞(Blocking),无限等待(Waiting),限时等待(Timed Waiting),死亡(Terminated)。
在操作系统篇中,我们提到的拿不到锁休眠(阻塞),等待条件变量进入等待队列休眠,在操作系统层面都是 WAITING。
注意:
- Java 没有 Running 状态,Runnable 就包括了 Running 和 Ready 以及部分 wait。
- 阻塞和等待的区别在于,阻塞是被动的,线程拿不到锁就会阻塞,但是等待是主动的,通过调用
sleep
或wait
实现。
# 传入任务
首先要构建一个线程,在 Thread
中的 init(..)
方法,会将当前线程设置为父线程,新线程是由父线程进行空间分配,子线程继承了父线程是否为 Daemon
,优先级,可继承的 ThreadLocal
等。
在创建线程对象时,可以传入 Runnable
或者 Callable
,或者继承 Thread
,重写 run
方法。但是实现接口比继承好得多。
new Thread(()->System.out.println("hello world")).start(); |
# 基础机制
守护线程:当所有非守护线程结束后,程序也就会终止,同时杀死所有守护线程。使用
setDaemon(true)
将线程设置为守护线程。如果守护线程中有finally
,并且在执行finally
前所有的非守护线程都结束了,虚拟机就会停止,不会执行finally
语句。sleep()
:休眠当前线程,如果该线程持有锁,不会释放锁。yield()
:如果当前线程已经完成生命周期最重要的部分,可以切换给其他线程执行。该方法只是给调度器一个建议。
# 线程中断
中断理解为线程的一个标识位属性,表示一个运行的线程是否被其他线程进行了中断操作,打上中断标识不会立即中断线程,而是需要线程自己来处理这个中断标识。
一个线程执行完毕会自动结束,如果运行过程中发生异常也会提前结束。有时在外部我们就是希望中断某一个线程,在 Java 中专门设计了一个 InterruptedException
来提前结束线程。
如果线程处于阻塞,限期等待或者无限期等待,此时调用 interrupt()
就会触发异常从而结束线程。但是不能中断 I/O 阻塞和 synchronized
锁阻塞。
@Override | |
public void run() { | |
try { | |
Thread.sleep(8000); | |
System.out.println("Thread run"); | |
} catch (InterruptedException e) { | |
e.printStackTrace(); | |
} | |
} | |
thread1.start(); | |
Thread.sleep(1000); | |
thread1.interrupt(); |
当一个线程内部是一个循环,且没有执行 sleep
等会抛出中断异常的操作,而我们又希望在外部控制线程中断,可以使用 interrupted()
方法:
// 这种设计限制还是比较大的 | |
public void run() { | |
while(!interrupted()) { | |
// .. | |
} | |
} |
注意:
sleep()
如果要抛出中断异常,会先清空中断标记,再抛出异常。interrupted
会清空中断标记,而isInterrupted()
不会。如果一个线程处于终结状态,
isInterrupted
也返回false
。
# 线程通信
- 使用
volatile
和synchronized
两个关键字,前者保证共享变量的实时更新,保证了可见性,底层原理是总线嗅探机制;后者可以实现同步代码块,保证同一时间只能有一个线程处于同步代码块,保证线程对变量访问的可见性和排他性。
需要注意的是,之前的例子,多个线程将
count
变量累加 10000,不能用volatile
修饰count
来保证线程安全。该关键字不保证线程安全。假设一下,在多 CPU 里面,已经有两个线程将要同时执行关于count
的写入指令,volatile
对此是无法避免的。
- 还有一种通信方式就是等待 / 通知机制:该部分类似于之前讲的条件变量那部分的内容,父线程等待子线程结果(Java 中也可以使用
join
方法)。wait()
:进入等待队列,线程放弃锁和时间片,满足某条件,才会被唤醒(notify/notifyAll
)。在之前的操作系统篇提到,wait
应该在获得锁后才允许被调用。wait()
还有两个重载函数,传入参数为等待时间。notify/notifyAll()
:通知等待队列中的线程从wait
返回,后者则是将队列中所有线程都唤醒。全部唤醒,还记得覆盖条件的概念吗?
其实所谓的唤醒,是将等待队列中的线程放到了同步队列中,因为线程调用了
notify/notifyAll
后还没有释放锁。
- 管道输入 / 输出流:使用
PipedOutputStream/PipedInputStream/PipedReader/PipedWriter
类
// 管道输入输出主要用于线程之间的数据传输 | |
// 下面代码改编自《Java 并发编程的艺术》,书上用的是字符流,但是中文会出现乱码,所以我改成字节流了 | |
public class Main { | |
public static void main(String[] args) throws Exception { | |
PipedOutputStream out = new PipedOutputStream(); | |
PipedInputStream in = new PipedInputStream(); | |
// 输入输出流连接,不然会抛出 IOException | |
out.connect(in); | |
Thread printThread = new Thread(new Print(in), "PrintThread"); | |
printThread.start(); | |
int receive = 0; | |
byte[] buf = new byte[100]; | |
try { | |
while((receive = System.in.read(buf)) != -1) { | |
out.write(buf,0 ,receive); | |
} | |
} finally { | |
out.close(); | |
} | |
} | |
static class Print implements Runnable { | |
private PipedInputStream in; | |
byte[] buf = new byte[100]; | |
public Print(PipedInputStream in) { | |
this.in = in; | |
} | |
public void run() { | |
int receive = 0; | |
try { | |
while (-1 != (receive = in.read(buf))) { | |
System.out.print(new String(buf, 0, receive)); | |
} | |
} catch (IOException ignored) { | |
} | |
} | |
} | |
} |
ThreadLocal
:该类到时候会详细讲解,此处简单介绍。它是线程变量,存储的是一个键值对,以ThreadLocal
对象为键,存储指定的泛型值。每个线程都有一个ThreadLocalMap
,存放的就是各种ThreadLocal->value
对。
public class ThreadLocalTest { | |
private static ThreadLocal<String> threadLocal = new ThreadLocal<String>(); | |
public static void main(String[] args){ | |
// 创建第一个线程 | |
Thread threadA = new Thread(()->{ | |
threadLocal.set("ThreadA:" + Thread.currentThread().getName()); | |
System.out.println("线程A本地变量中的值为:" + threadLocal.get()); | |
},"ThreadA"); | |
// 创建第二个线程 | |
Thread threadB = new Thread(()->{ | |
threadLocal.set("ThreadB:" + Thread.currentThread().getName()); | |
System.out.println("线程B本地变量中的值为:" + threadLocal.get()); | |
},"ThreadB"); | |
// 启动线程 A 和线程 B | |
threadA.start(); | |
threadB.start(); | |
} | |
} | |
// 每次的打印结果可能不一致 | |
// 线程 B 本地变量中的值为:ThreadB:ThreadA | |
// 线程 A 本地变量中的值为:ThreadA:ThreadB |
如果你此时对
ThreadLocal
还不熟悉(很正常),之后会有一篇文章专门讲解这个,当然,看源码也是必不可缺的。
# 参考
https://pdai.tech/md/java/thread/java-thread-x-thread-basic.html
《Java 并发编程的艺术》第四章