Java 线程
1. 前言
此文为学习笔记,不是很详细,还望理解,有错也希望各位及时指出。详细可以参考《Java 并发编程的艺术》。
2. 什么是 Java 多线程
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
多线程:指的是这个程序(一个进程)运行时产生了不止一个线程
并行与并发:
- 并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
- 并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。
这里定义和线程相关的另一个术语 - 进程:
一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。
3. 线程的生命
3.1 生命周期及状态
线程是一个动态执行的过程,它也有一个从产生到死亡的过程。
-
新建状态(NEW):
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
-
运行状态(RUNNABLE):
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。
操作系统隐藏 Java虚拟机(JVM)中的 READY 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:HowToDoInJava:Java Thread Life Cycle and Thread States),所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。
-
阻塞状态(BLOCKED):
表示线程阻塞于锁。
-
等待状态(WAITING):
表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程趋一些特定动作(通知或中断)。
-
超时等待状态(TIME_WAITING):
该状态不同于WATING,它是可以在指定的时间自行返回的。
-
终止状态(TERMINATED):
表示当前线程已经执行完毕。
3.2 创建线程
Java 提供了三种创建线程的方法:
- 通过实现 Runnable 接口;
- 通过继承 Thread 类本身;
- 通过 Callable 和 Future 创建线程。
这里先只写一下前两种,第三种涉及与线程池使用,暂时不写
public class MyThread {
public static void main(String[] args) {
new Thread(new RunnableImplThread(), "RunnableImplThread").start();
new Thread(new ThreadExtend(),"ThreadExtend").start();
}
static class RunnableImplThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
static class ThreadExtend extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
}
/*
运行结果:
RunnableImplThread
ThreadExtend
*/
三种创建方法想深入了解的话可以参考:
3.3 加入线程Thread.join()
Thread API 包含了等待另一个线程完成的方法:join()
方法。当调用 Thread.join()
时,调用线程将阻塞,直到目标线程完成为止。
Thread.join()
通常由使用线程的程序使用,以将大问题划分成许多小问题,每个小问题分配一个线程。等待线程终止,Happens-Before 规则中也有规定。
3.4 线程中断
中断一个进程是通过调用该线程的 interrupt() 方法来完成的,真正的实现也是一个 native 方法 interrupt0(),该方法设置线程的中断标志位为有效,然后线程的某些方法检测到中断就会抛出 InterruptException 并清除中断标志位。Thread 的 isInterrupt 方法只有在设置中断但未响应时才会返回 true
随着 run() 方法的执行完毕,线程就会正常终止。如果想要提前终止线程可以使用比较优雅的方式:
中断或者 boolean 变量检测:
class Runner implements Runnable {
private static volatile boolean on = true;
@Override
public void run() {
while (on && !Thread.currentThread().isInterrupted()) {
// TODO
}
}
public void cancel() {
on = false;
}
}
3.5 结束线程
线程会以以下三种方式之一结束:
- 线程到达其
run()
方法的末尾。 - 线程抛出一个未捕获到的
Exception
或Error
。 - 另一个线程调用一个弃用的
stop()
方法。弃用是指这些方法仍然存在,但是您不应该在新代码中使用它们,并且应该尽量从现有代码中除去它们。
当 Java 程序中的所有线程都完成时,程序就退出了。
3.6 休眠和等待
3.6.1 休眠
Thread API 包含了一个 sleep()
方法,它将使当前线程进入等待状态,直到过了一段指定时间,或者直到另一个线程对当前线程的 Thread
对象调用了 Thread.interrupt()
,从而中断了线程。当过了指定时间后,线程又将变成可运行的,并且回到调度程序的可运行线程队列中。
如果线程是由对 Thread.interrupt()
的调用而中断的,那么休眠的线程会抛出 InterruptedException
,这样线程就知道它是由中断唤醒的,就不必查看计时器是否过期。
Thread.yield()
方法就象 Thread.sleep()
一样,但它并不引起休眠,而只是暂停当前线程片刻,这样其它线程就可以运行了。在大多数实现中,当较高优先级的线程调用 Thread.yield()
时,较低优先级的线程就不会运行。
3.6.2 等待/通知机制
等待/通知机制的相关方法是任意 Java 对象都具备的,这些方法都定义在 java.lang.Object
中.
方法名称 | 描述 |
---|---|
notify() | 随机唤醒等待队列中等待同一共享资源的 “一个线程”,并使该线程退出等待队列,进入可运行状态,也就是notify()方法仅通知“一个线程” |
notifyAll() | 使所有正在等待队列中等待同一共享资源的 “全部线程” 退出等待队列,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,这取决于JVM虚拟机的实现 |
wait() | 使调用该方法的线程释放共享资源锁,然后从运行状态退出,进入等待队列,直到被再次唤醒 |
wait(long) | 超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回 |
wait(long,int) | 对于超时时间更细力度的控制,可以达到纳秒 |
public class WaitNotify {
static class MyList {
private static List<String> list = new ArrayList<>();
public static void add() {
list.add("something");
}
public static int size() {
return list.size();
}
}
static class ThreadA implements Runnable {
private final Object lock;
public ThreadA(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
if (MyList.size() != 5) {
System.out.println("wait() " + System.currentTimeMillis());
lock.wait();
System.out.println("wait() end " + System.currentTimeMillis());
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class ThreadB implements Runnable {
private final Object lock;
public ThreadB(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
for (int i = 0; i < 10; i++) {
MyList.add();
if (MyList.size() == 5) {
lock.notify();
System.out.println("已发出通知!");
}
System.out.println("添加了" + (i + 1) + "个元素!");
Thread.sleep(1000);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
try {
Object lock = new Object();
new Thread(new ThreadA(lock)).start();
Thread.sleep(50);
new Thread(new ThreadB(lock)).start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果
wait() 1575620403794
添加了1个元素!
添加了2个元素!
添加了3个元素!
添加了4个元素!
已发出通知!
添加了5个元素!
添加了6个元素!
添加了7个元素!
添加了8个元素!
添加了9个元素!
添加了10个元素!
wait() end 1575620413846
注意:从运行结果,我们可以看出,notify() 或 notifyAll() 执行后并不会立即释放锁,需要调用的 notify() 或 nitify() 的线程线程释放锁后,等待线程才会从 wait() 返回。线程状态从 WATING 变成 BLOCKED。
3.6.3 sleep()和wait()的区别
- wait()是Object的方法,而sleep()是Thread的方法。
- sleep()方法不会释放锁,可以定义时间,时间过后会自动唤醒。wait()方法会释放锁。
- sleep()不会释放资源,wait()进入线程等待池等待,出让系统资源,其它线程可以占用CPU。一般 wait()不会加时间限制,这是因为如果 wait()线程运行的资源不够,要等待其它线程调用 notify()或 notifyAll()唤醒等待池中所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒,如果时间不到,只能用 interrupt()强行打断。
- wait()、notify()、notifyAll()只能在同步控制方法或同步控制块中使用,而 sleep()可以任何地方使用。
- sleep()必须捕获异常,wait()、notify()、notifyAll()则不用。
3.7 ThreadLocal
ThreadLocal 提供线程内部的局部变量,在本线程内随时随地可取,隔离其他线程
JDK 8 之前是每个 ThreadLocal 类都创建一个 Map,然后用 threadID 作为 Map 的 key,要存储的局部变量作为 Map 的 value,这样就能达到各个线程的值隔离的效果
JDK8 的设计是:每个 Thread 维护一个 ThreadLocalMap 哈希表,这个哈希表的 key 是 ThreadLocal实例本身,value 才是真正要存储的值 Object
这样有两个好处:
- 这样设计之后每个
Map
存储的Entry
数量就会变小,因为之前的存储数量由Thread
的数量决定,现在是由ThreadLocal
的数量决定 - 当
Thread
销毁之后,对应的ThreadLocalMap
也会随之销毁,能减少内存的使用
4. 小结&参考资料
小结
多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。
多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。
Java 多线程对于 Java 开发程序猿来说真的非常非常重要。得深刻地理解其中的知识点,并熟练的使用。