第一部分 多线程基础
本系列博客,主要是面向Java8的源码。
本系列博客主要参考汪文君老师 《Java高并发编程详解》一书
转载请注明出处,多谢~。
1. 线程的start方法剖析
/**
* Causes this thread to begin execution; the Java Virtual Machine
* calls the <code>run</code> method of this thread.
*/
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();//核心部分就是这个start0本地方法,也就是JNI方法,run方法就是被此方法调用
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
2. Runnable接口的引入
//Runnable接口非常简单,只定义了一个无参数无返回值的run方法
public interface Runnable {
public abstract void run();
}
可以通过继承Thread然后重写run方法实现自己的业务逻辑,也可以实现Runnable接口实现
//构造Thread时传递Runnable
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
}
});
//Lambda简化
Thread thread = new Thread(() -> {
});
- 准确地讲,创建线程只有一种方式那就是构造Thread类,而实现线程的执行单元则有两种方式
- Thread负责线程本身相关的职责和控制,而Runnable则负责逻辑执行单元的部分
3. 线程的父子关系
Thread的所有构造函数,最终都会调用一个方法 init()
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
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(); //获取当前线程作为父线程
SecurityManager security = System.getSecurityManager();
......
}
- 一个线程的创建肯定是由另一个线程完成的
- 被创建的线程的父线程就是创建它的线程
4. Thread与ThreadGroup
在Thread的构造函数中,可以显示的指定线程的Group,也就是ThreadGroup
//接着读Thread init方法的源码,你就会发现
SecurityManager security = System.getSecurityManager();
if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
g = parent.getThreadGroup();
}
}
- main线程所在的ThreadGroup成为main
- 构造一个线程时如果没有显示的指定ThreadGroup,那么它将会和父线程同属于一个ThreadGroup
5. 守护线程
守护线程是一类比较特殊的线程,一般用于处理一些后台的工作,比如JDK的垃圾回收线程
JVM程序在什么情况下会退出?
The Java Virtual Machine exits when the only threads running are all daemon threads.
在正常退出的情况下,若JVM中没有一个非守护线程,则JVM的进程会退出
public class Demo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (true) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.setDaemon(true); //将thread设置为守护线程
thread.start(); //启动thread线程
Thread.sleep(2000L);
System.out.println("Main thread exit");
}
}
运行结果:main线程结束生命周期后,JVM也随之退出运行
6. Thread API
6.1 线程sleep
//sleep是一个静态方法,会让当前线程进入指定毫秒数的休眠
//注意:休眠不会放弃monitor锁的所有权
Thread.sleep()
//使用TimeUnit代替Thread.sleep
TimeUnit.HOURS.sleep(3)//休眠3小时
TimeUnit.MINUTES.sleep(24)//休眠24分钟
TimeUnit.SECONDS.sleep(17)//休眠17秒
TimeUnit.MILLISECONDS.sleep(12)//休眠12毫秒
6.2 线程yield
//yield方法属于一种启发式的方法
//其会提醒调度器我愿意放弃当前的CPU资源,如果CPU资源不紧张,则会忽略 RUNNING->RUNNABLE
public static native void yield();
- sleep会导致当前线程暂停指定的时间,没有CPU时间片的消耗
- yield只是对CPU调度器的一个提示,如果CPU调度器没有忽略这个提示,它会导致线程上下文的切换
- sleep会使线程短暂block,会在给定的时间内释放CPU资源
- 一个线程sleep另一个线程调用interrupt会捕获到中断信号,而yield则不会
6.3 设置线程优先级
public final void setPriority(int newPriority)
为线程设置优先级
public final int getPriority()
获取线程的优先级
源码分析:
public final static int MAX_PRIORITY = 10;
public final static int MIN_PRIORITY = 1;
public final void setPriority(int newPriority) {
ThreadGroup g;
checkAccess();
if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) { //线程的优先级不能小于1也不能大于10
throw new IllegalArgumentException();
}
if((g = getThreadGroup()) != null) {
//如果线程的优先级大于线程所在group的优先级,指定的优先级就会失效,取而代之的是group的最大优先级
if (newPriority > g.getMaxPriority()) {
newPriority = g.getMaxPriority();
}
setPriority0(priority = newPriority);
}
}
6.4 获取线程ID
public long getId()
线程的ID在整个JVM进程中都会是唯一的,并且都是从0开始逐次递增
自己创建的线程绝非第0号线程
6.5 获取当前线程
public static native Thread currentThread();
用于返回当前执行线程的引用
6.6 线程interrupt
public void interrupt()
public static boolean interrupted()
判定当前线程是否被中断
public boolean isInterrupted()
判定当前线程是否被中断
以下方法的调用会使得当前线程进入阻塞状态,调用interrupt方法可以打断阻塞
- Object的
wait()
方法- Object的
wait(long)
方法- Object的
wait(long,int)
方法- Thread的
sleep(long)
方法- Thread的
sleep(long,int)
方法- Thread的
join()
方法- Thread的
join(long)
方法- Thread的
join(long,int)
方法- InterruptibleChannel的io操作
- Selector的wakeup方法
- 其他方法
上述的方法都会使得当前线程进入阻塞状态
另外一个线程调用被阻塞线程的interrupt方法可以打断阻塞
因此这些方法有时候被称为可中断的方法
注意:仅仅打断了线程的阻塞状态,不等于生命周期结束
interrupt到底做了什么?
①:在一个线程内部存在着名为interrupt flag的标识
②:如果一个线程被interrupt,那么它的flag将被设置
③:线程执行可中断方法被阻塞时,调用interrupt方法将其中断,反而会导致flag被清除
④:如果一个线程是死亡状态,iterrupt会直接被忽略
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (true) {
//空的死循环,不会捕捉中断信号
}
});
thread.start(); //启动thread线程
Thread.sleep(2000L);
System.out.println(thread.isInterrupted());//false
thread.interrupt();//
Thread.sleep(2000L);
System.out.println(thread.isInterrupted());//true
}
}
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (true) {
try {
Thread.sleep(1000L);//可中断方法,会捕捉到中断信号,并且擦去interrupt标识
} catch (InterruptedException e) {
System.out.println("I am be interrupted");
}
}
});
thread.start();
Thread.sleep(2000L);
System.out.println(thread.isInterrupted());//false
thread.interrupt();
Thread.sleep(2000L);
System.out.println(thread.isInterrupted());//false
}
}
interrupted()
和isInterrupted()
区别
interrupted是一个静态方法,虽然其也用于判断当前线程是否被中断,但是调用该方法会直接擦除线程的interrupt标识
如果当前线程被打断,第一次调用Interrupt方法会返回true,并且擦除interrupt标识,第二次之后的调用都会返回false,除非再次被打断
public class ThreadInterrupted {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (true) {
System.out.println(Thread.interrupted());
}
});
thread.setDaemon(true);
thread.start();
TimeUnit.MILLISECONDS.sleep(2);
thread.interrupt();
}
}
/**
* 输出
* ...
* false
* false
* true
* false
* false
* ...
*/
interrupt注意事项:
//interrupted源码
public static boolean interrupted() {
return currentThread().isInterrupted(true); //擦除interrupt标识
}
//isInterrupted源码
public boolean isInterrupted() {
return isInterrupted(false); //不擦除interrupt标识
}
//都调用了isInterrupted这个本地方法
private native boolean isInterrupted(boolean ClearInterrupted);//ClearInterrupted主要来控制是否擦除interrupt标识
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
// 1,判断当前线程是否被中断
System.out.println("Main Thread is interrupted? " + Thread.interrupted());
//2,中断当前线程
Thread.currentThread().interrupt();
//3,判断当前线程是否已经被中断
System.out.println("Main Thread is interrupted? " + Thread.currentThread().isInterrupted());
try {
//4,执行可中断方法
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
System.out.println("Interrupted Exception");
}
}
}
//输出
// Main Thread is interrupted? false
// Main Thread is interrupted? true
// Interrupted Exception
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
// 1,判断当前线程是否被中断
System.out.println("Main Thread is interrupted? " + Thread.currentThread().isInterrupted());
//2,中断当前线程
Thread.currentThread().interrupt();
//3,判断当前线程是否已经被中断
System.out.println("Main Thread is interrupted? " + Thread.interrupted());
try {
//4,执行可中断方法
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
System.out.println("Interrupted Exception");
}
}
}
//输出
//Main Thread is interrupted? false
//Main Thread is interrupted? true
可以看出Demo2的可中断方法没有抛出异常,意味着没有被中断,而Demo1被中断了
原因:
interrupted()
立即擦除了Interrupt标识isInterrupted()
捕获到了InterruptedException
之后才会擦除Interrupt标识
6.7 线程join
可中断方法
join某个线程A,会使当前线程B进入等待,知道A生命周期结束,或者到达给定时间,B此时是BLOCKED的
public class ThreadJoin {
public static void main(String[] args) throws InterruptedException {
//1.定义两个线程
List<Thread> threads = IntStream.range(1, 3).mapToObj(ThreadJoin::create).collect(Collectors.toList());
//2.启动两个线程
threads.forEach(Thread::start);
//3.执行这两个线程的join方法
for (Thread thread : threads) {
thread.join();
}
//4.main线程循环输出
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "#" + i);
shortSleep();
}
}
//构造一个简单线程,每个线程只是简单的循环输出
private static Thread create(int seq) {
return new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "#" + i);
shortSleep();
}
}, String.valueOf(seq));
}
private static void shortSleep() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行上面的代码:
//输出 线程一和线程二交替输出直到生命周期结束,main线程才开始运行
...
2#8
1#8
1#9
2#9
main#0
main#1
main#2
...
//注释掉3 线程一和线程二和main线程交替执行
总结:join方法会使当前线程永远的等待下去,直到期间被另外的线程中断,或者join的线程执行结束,或者join的另外两个重载方法指定的毫秒数时间到达后,当前线程才会退出阻塞
6.8 如何关闭一个线程
6.8.1 正常关闭
线程结束生命周期正常结束
捕获中断信号关闭线程
public class Way1 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread() { @Override public void run() { while (!isInterrupted()) { //working } } }; t.start(); TimeUnit.MINUTES.sleep(1); t.interrupt(); } } public class Way2 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { for (; ; ) { //working try { TimeUnit.MILLISECONDS.sleep(1); } catch (InterruptedException e) { break; } } }); t.start(); TimeUnit.MINUTES.sleep(1); t.interrupt(); } }
使用volatile开关控制
public class FlagThreadExit { static class MyTask extends Thread { private volatile boolean closed = false; @Override public void run() { System.out.println("I will start work"); while (!closed && !isInterrupted()) { //working... } System.out.println("I will stop work"); } public void close() { closed = true; this.interrupt(); } } public static void main(String[] args) throws InterruptedException { MyTask t = new MyTask(); t.start(); TimeUnit.MINUTES.sleep(1); System.out.println("System will be end"); t.close(); } }
上面的例子定义了一个closed开关变量,并且是用volatile修饰,同样可以关闭线程
6.8.2 异常退出
在一个线程的执行单元中,是不允许抛出checked异常的,不论Thread的run方法还是Runnable的run方法,如果线程运行过程中需要捕获checked异常并且判断是否还有运行下去的必要,那么可以将checked异常封装成unchecked异常(RuntimeException)抛出进而结束线程的生命周期
6.8.3 进程假死
所谓假死,就是进程虽然存在,但是没有日志输出,程序不进行任何的作业,看起来就像死了一样,但事实上并没有死 一般是由于某个线程阻塞了或者出现死锁的情况
7. 线程安全与数据同步
7.1 什么是 synchronized ?
sychronized 关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么对该对象的所有读和写都将通过同步的方式来进行。
- synchronized 关键字提供了一种锁的机制,能够确保共享变量的互斥访问,从而防止数据不一致问题的出现
- synchronized 关键字包括
monitor enter
和monitor exit
两个 JVM 指令,它能够保证在任何时候任何线程执行到monitor enter
成功之前都必须从主内存中获取数据,而不是从缓存中,在monitor exit
运行成功之后,共享变量被更新后的值必须刷入主内存。 - synchronized 的指令严格遵守java happens-before规则,一个
monitor exit
指令之前必定要有一个monitor enter
JVM指令分析
package com.xp;
import java.util.concurrent.TimeUnit;
public class Mutex {
private final static Object MUTEX = new Object();
public void accessResource() {
synchronized (MUTEX) {
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final Mutex mutex = new Mutex();
for (int i = 0; i < 5; i++) {
new Thread(mutex::accessResource).start();
}
}
}
使用JDK命令javap 对Mutex class进行反汇编,输出了大量的JVM指令,在这些指令中,你将发现monitor enter 和monitor exit是成对出现的(有些时候会出现一个monitor enter 多个monitor exit,但是每一个monitor exit 之前必有对应的monitor enter,这是肯定的)
(1) Monitorenter
每个对象都与一个monitor 相关联,一个monitor的lock的锁只能被一个线程在同一时间获得,在一个线程尝试获得与对象关联monitor的所有权时会发生如下的几件事情。
- 如果monitor的计数器为0,则意味着该monitor的lock还没有被获得,某个线程获得之后将立即对该计数器加一,从此该线程就是这个monitor的所有者了。
- 如果一个已经拥有该monitor所有权的线程重入,则会导致monitor计数器再次累加。
- 如果monitor 已经被其他线程所拥有,则其他线程尝试获取该monitor的所有权时,会被陷入阻塞状态直到monitor计数器变为0,才能再次尝试获取对monitor的所有权。
(2) Monitorexit
释放对monitor的所有权,想要释放对某个对象关联的monitor的所有权的前提是,你曾经获得了所有权。释放monitor所有权的过程比较简单,就是将monitor的计数器减一,如果计数器的结果为0,那就意味着该线程不再拥有对该monitor的所有权,通俗地讲就是解锁。与此同时被该 monitor block的线程将再次尝试获得对该monitor的所有权。
与monitor关联的对象不能为空
synchronized作用域要尽可能小,尽可能只作用于共享资源的读写作用域
不同的monitor企图锁相同的方法是不现实的
多个锁交叉容易导致死锁产生
7.2 程序死锁的原因
- 交叉锁可导致程序出现死锁
- 内存不足
- 一问一答式数据交换
- 数据库锁
- 文件锁
- 死循环引起的死锁
8. 线程间通信
8.1 wait和notify方法详解
wait和notify并不是Thread特有的方法,而是Object中的方法,也就是说在JDK中的每一个类都拥有着两个方法
wait三个重载方法:
public final void wait() throws InterruptedException;
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException;
- wait方法的这三个重载方法都将调用
wait(long timeout)
这个方法,前文使用的wait()方法等价于 wait(0),0代表着永不超时 - Object 的
wait(long timeout)
方法会导致当前线程进入阻塞,直到有其他线程调用了 Object的notify()
或者notifyAll()
方法才能将其唤醒,或者阻塞时间到达了timeout时间而自动唤醒。 - wait 方法必须拥有该对象的monitor,也就是wait方法必须在同步方法中使用。
- 当前线程执行了该对象的wait方法之后,将会放弃对该monitor的所有权并且进入与该对象关联的wait set中,也就是说一旦线程执行了某个object的wait方法之后,它就会释放对该对象monitor的所有权,其他线程也会有机会继续争抢该monitor的所有权。
notify方法:
public final native void notify();
- 唤醒单个正在执行该对象wait方法的线程。
- 如果有某个线程由于执行该对象的wait方法而进入阻塞则会被唤醒,如果没有则会忽略。
- 被唤醒的线程需要重新获取对该对象所关联monitor的lock才能继续执行。
- wait方法是可中断方法,这也就意味着,当前线程一旦调用了wait方法进入阻塞状态,其他线程是可以使用interrupt方法将其打断的;可中断方法被打断后会收到中断异常 InterruptedException,同时interrupt标识也会被擦除。
- 线程执行了某个对象的wait方法以后,会加人与之对应的wait set中,每一个对象的monitor 都有一个与之关联的wait set。
- 当线程进入wait set 之后,notify方法可以将其唤醒,也就是从 wait set中弹出,同时中断wait中的线程也会将其唤醒。
- 必须在同步方法中使用wait和notify方法,因为执行wait 和notify的前提条件是必须持有同步方法的monitor的所有权,也就是必须在同步方法中使用
- 同步代码的monitor必须与执行wait notify方法的对象一致
8.2 wait和sleep的异同
从表面上看,wait 和sleep方法都可以使当前线程进入阻塞状态,但是两者之间存在着本质的区别,下面我们将总结两者的区别和相似之处。
- wait 和sleep方法都可以使线程进入阻塞状态。
- wait 和 sleep方法均是可中断方法,被中断后都会收到中断异常。
- wait 是Object的方法,而sleep是Thread特有的方法。
- wait方法的执行必须在同步方法中进行,而sleep则不需要。
- 线程在同步方法中执行 sleep方法时,并不会释放monitor的锁,而 wait方法则会释放monitor的锁。
- sleep方法短暂休眠之后会主动退出阻塞,而wait方法(没有指定wait时间)则需要被其他线程中断后才能退出阻塞。
8.3 多线程间通信
notifyAll 方法 多线程间通信需要用到Object的notifyAll方法,该方法与notify 比较类似,都可以唤醒由于调用了wait方法而阻塞的线程,但是notify方法每次只能唤醒其中的一个线程,而notifyAll方法则可以同时唤醒全部的阻塞线程,同样被唤醒的线程仍需要继续争抢 monitor
线程休息室wait set
在虚拟机规范中存在一个wait set(wait set 又被称为线程休息室)的概念,至于该wait set 是怎样的数据结构,JDK官方并没有给出明确的定义,不同厂家的JDK有着不同的实现方式,甚至相同的JDK厂家不同的版本也存在着差异,但是不管怎样,线程调用了某个对象的wait方法之后都会被加入与该对象monitor 关联的wait set中,并且释放monitor的所有权。
若干个线程调用了wait方法之后被加入与monitor关联的wait set中
当另外一个线程调用该monitor的notify方法之后,其中一个线程会从wait set中弹出,至于是随机弹出还是以先进先出的方式弹出,虚拟机规范同样也没有给出强制的要求
而执行notifyAll则不需要考虑哪个线程会被弹出,因为wait set中的所有wait线程都将被弹出
9. ThreadGroup详细讲解
9.1 ThreadGroup与Thread
在Java程序中,默认情况下,新的线程都会被加入到main 线程所在的group中,main线程的group 名字同线程名。如同线程存在父子关系一样,ThreadGroup 同样也存在父子关系。 无论如何,线程都会被加入某个ThrcadGroup之中。
9.2 创建ThreadGroup
public ThreadGroup(String name) //为ThreadGroup赋予名字,父ThreadGroup是创建它的线程的ThreadGroup
public ThreadGroup(ThreadGroup parent, String name) //赋予名字的同时,显式的指定父ThreadGroup
9.3 复制Thread数组和ThreadGroup数组
复制Thread数组
public int enumerate(Thread list[])
public int enumerate(Thread list[], boolean recurse)
上述两个方法,会将ThreadGroup中的active线程全部复制到Thread数组中,其中recurse参数如果为true,则该方法会将所有子group中的active线程都递归到Thread数组中,enumerate(Thread[] list)实际上等价于enumerate(Thread[] true),上面两个方法都调用了ThreadGroup的私有方法