Thread类解读

简介

Thread类是Java并发编程的基础,是对多线程编程的实现,底层大量使用了native调用C/C++实现的API,是jdk中也非常基础也非常重要的类。

类图


volatile关键字

作用

Java 语言中的 volatile 变量可以被看作是一种 “程度较轻的 synchronized”;与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  • 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效)
  • 2)禁止进行指令重排序。
    volatile关键字禁止指令重排序有两层意思:
    • 1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
    • 2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

volatile保证了可见性,不保证原子性

比如下列代码就时而成功,时而失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ThreadTest {
private volatile int inc = 0;

private void increase() {
inc++;
}

@Test
public void t(){
for(int i=0;i<10;i++){
new Thread(() -> {
for(int j=0;j<1000;j++)
increase();
}).start();
}
assert inc==10000;
}
}

解释:当线程读取inc的值后修改之前,cpu调度到其它线程,但是线程1没有进行修改,所以其他线程根本就不会看到修改的值。

使用volatile关键字需要具备的条件

  • 1.对变量的写操作不依赖当前值
  • 2.该变量没有包含在具有其他变量的不变式中

成员变量

类型 变量名 说明
String name 线程名
int priority 优先级
long eetop
boolean single_step 线程是否单步
boolean daemon 是否为守护线程
boolean stillborn 虚拟机状态
Runnable target 目标run方法对象
ThreadGroup group 线程组
ClassLoder contextClassLoader 类加载器
AccessControlContext inheritedAccessControlConext
ThreadLocalMap threadlocals
ThreadLocalMap inheritableThreadLocals
long stackSize 该线程栈大小
long nativeParkEventPointer
long tid 线程id
int threadStatus 线程状态
Object parkBlocker
Interruptible blocker
Object blockerLock
UncaughtExceptionHandler uncaughtExceptionHandler

静态成员变量

类型 变量名 说明
int threadInitNumber s
long threadSeqNumber
int MIN_PRIORITY 最小优先权
int NORM_PRIORITY 默认优先权
Int MAX_PRIORITY 最大优先权
StackTraceElement[] EMPTY_STACK_TRACE
RuntimePermission SUBCLASS_IMPLEMENTATION_PERMISSION
class Caches
UncaughtExceptionHandler defaultExceptionhandler
long threadLocalRandomSeed
int threadLocalRandomProbe
int threadLocalRandomSecondarySeed

线程的生命周期

在Java虚拟机 中,线程从最初的创建到最终的消亡,要经历若干个状态:创建(new)就绪(runnable/start)运行(running)阻塞(blocked)等待(waiting)时间等待(time waiting) 和 **消亡(dead/terminated)**。
在给定的时间点上,一个线程只能处于一种状态,各状态的含义如下图所示:

解释:

  • 创建:程创建之后,不会立即进入就绪状态,因为线程的运行需要一些条件(比如程序计数器、Java栈、本地方法栈等)。
  • 就绪:只有线程运行需要的所有条件满足了,才进入就绪状态。不代表立刻就能获取CPU执行时间,也许此时CPU正在执行其他的事情,因此它要等待。
  • 运行:当得到CPU执行时间之后,线程便真正进入运行状态。行状态过程中,可能有多个原因导致当前线程不继续运行下去,比如用户主动让线程睡眠(睡眠一定的时间之后再重新执行)、用户主动让线程等待,或者被同步块阻塞,此时就对应着多个状态:time waiting(睡眠或等待一定的时间)、waiting(等待被唤醒)、blocked(阻塞)。
  • 消亡:当由于突然中断或者子任务执行完毕,线程就会被消亡。

创建线程的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class ThreadTest {

public static void main(String[] args) {

//使用继承Thread类的方式创建线程
new Thread(){
@Override
public void run() {
System.out.println("Thread");
}
}.start();

//使用实现Runnable接口的方式创建线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Runnable");
}
});
thread.start();

//JVM 创建的主线程 main
System.out.println("main");
}
}
/* Output: (代码的运行结果与代码的执行顺序或调用顺序无关)
Thread
main
Runnable
*///

线程相关比较重要的方法

start()

创建好自己的线程类之后,就可以创建线程对象了,然后通过start()方法去启动线程。
通过start()方法启动一个线程之后,若线程获得了CPU执行时间,便进入run()方法体去执行具体的任务。如果用户直接调用run()方法,即相当于在主线程中执行run()方法,跟普通的方法调用没有任何区别,此时并不会创建一个新的线程来执行定义的任务。
实际上,start()方法的作用是通知 “线程规划器” 该线程已经准备就绪,以便让系统安排一个时间来调用其 run()方法,也就是使线程得到运行。

run()

自定义的执行方法实体,在线程执行该线程时,会执行该方法体中的内容

一般来说,有两种方式可以达到重写run()方法的效果:

  • 直接重写:直接继承Thread类并重写run()方法;
  • 间接重写:通过Thread构造函数传入Runnable对象 (注意,实际上重写的是 Runnable对象 的run() 方法)。

sleep()

作用是在指定的毫秒数内让当前正在执行的线程(即 currentThread() 方法所返回的线程)睡眠,并交出 CPU 让其去执行其他的任务。
调用sleep方法相当于让线程进入阻塞状态。

注意:

  • 如果调用了sleep方法,必须捕获InterruptedException异常或者将该异常向上层抛出;
  • sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。

wait()

wait/notify的通俗解释:

  • 1.线程A首先获取到obj的锁,然后执行了obj.wait(),这个方 法就会是线程A暂时让出对obj锁的持有,并把线程A转换waiting状态,同时加入锁对象的等待队列。
  • 2.线程B获取到了obj的锁,然后执行了obj.notify(),这个方法通知了锁对象的等待队列,使正在等待队列中的线程A改为阻塞状态,使A进入对obj锁的竞争。当然在执行notify后并不会使线程A马上获取到锁,因为线程B目前还在持有obj的锁。
  • 3.线程A获取到obj锁,继续从wait()之后的代码运行。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class ThreadWaitAndNotifyDemo {

static final Object obj = new Object(); //对象锁
private static boolean flag = false;

public static void main(String[] args) throws Exception {
Thread consume = new Thread(new Consume(), "Consume");
Thread produce = new Thread(new Produce(), "Produce");
consume.start();
Thread.sleep(1000);
produce.start();
}

// 生产者线程
static class Produce implements Runnable {

@Override
public void run() {
synchronized (obj) {
System.out.println("进入生产者线程");
System.out.println("生产");
try {
TimeUnit.MILLISECONDS.sleep(2000); //模拟生产过程
flag = true;
obj.notify(); //通知消费者
TimeUnit.MILLISECONDS.sleep(1000); //模拟其他耗时操作
System.out.println("退出生产者线程");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

//消费者线程
static class Consume implements Runnable {

@Override
public void run() {
synchronized (obj) {
System.out.println("进入消费者线程");
System.out.println("wait flag 1:" + flag);
while (!flag) { //判断条件是否满足,若不满足则等待
try {
System.out.println("还没生产,进入等待");
obj.wait();
System.out.println("结束等待");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("wait flag 2:" + flag);
System.out.println("消费");
System.out.println("退出消费者线程");
}
}
}
}

yield()

调用 yield()方法会让当前线程交出CPU资源,自身状态转变为就绪状态,让CPU去执行其他的线程,并将自身线程。但是,yield()不能控制具体的交出CPU的时间。

源码注释中比较重要的几点

  • Yield是一个静态的原生(native)方法。
  • Yield告诉当前正在执行的线程把运行机会交给线程池中拥有相同优先级的线程。
  • Yield不能保证使得当前正在运行的线程迅速转换到可运行的状态。
  • 仅能使一个线程从运行状态转到可运行状态,而不是等待或阻塞状态.

注意:

  • yield()方法只能让 拥有相同优先级的线程 有获取 CPU 执行时间的机会。
  • 调用yield()方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新得到 CPU 的执行。
  • 它同样不会释放锁。

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class YieldTest{
@Test
public void t1(){
new Thread(() -> {
long beginTime = System.currentTimeMillis();
int count = 0;
for (int i = 0; i < 50000; i++) {
Thread.yield(); // 将该语句注释后,执行会变快
count = count + (i + 1);
}
long endTime = System.currentTimeMillis();
System.out.println("用时:" + (endTime - beginTime) + "毫秒!");
});
}
}

join()

由于 join方法 会调用 wait方法 让宿主线程进入阻塞状态,并且会释放线程占有的锁,并交出CPU执行权限。

该方法是Thread类中的一个方法,该方法的定义是等待该线程执行直到终止。其实就说join方法将挂起调用线程的执行,直到被调用的对象完成它的执行。

例如:如何让三个线程按顺序执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class JoinTest extends TestCase {
/**
* @author wcc
* @date 2021/8/21 20:46
* 现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?
*/
@Test
public void test() {

//初始化线程一
Thread t1 = new Thread(() -> System.out.println("t1 is running..."));

//初始化线程二
Thread t2 = new Thread(() -> {
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("t2 is running...");
}
});

//初始化线程三
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("t3 is running...");
}
}
});

t1.start();
t2.start();
t3.start();
}
}

底层代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;

if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}

if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}

可以看见底层仍是通过调用thread对象的wait方法,而wait方法的解释可以参考前面。但这里有一个问题,wait后没有nofity动作。此时调用线程是阻塞态,那么是谁在什么时候释放了调用线程线程呢?

这时要了解一下Thread.exit()方法,在方法执行中有执行notifyAll方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 这个方法由系统调用,当该线程完全退出前给它一个机会去释放空间。
*/
private void exit() {
if (group != null) {
group.threadTerminated(this);
group = null;
}
/* Aggressively null out all reference fields: see bug 4006245 */
target = null;
/* Speed the release of some of these resources */
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}

void threadTerminated(Thread t) {
synchronized (this) {
remove(t);

if (nthreads == 0) {
notifyAll();
}
if (daemon && (nthreads == 0) &&
(nUnstartedThreads == 0) && (ngroups == 0))
{
destroy();
}
}
}

join的确是通过wait方法使调用线程变为等待状态,再在被调用线程运行结束时通过系统调用exit方法启动了notifyAll。

我们可以这么理解,把obj.join方法替换成obj.wait()方法,并且在obj线程运行结束后自动执行notifyAll()方法,这样就可以用wait的思路来理解join的运行过程了。

interrupt()

单独调用interrupt方法可以使得 处于阻塞状态的线程 抛出一个异常,也就是说,它可以用来中断一个正处于阻塞状态的线程;另外,通过 interrupted()方法 和 isInterrupted()方法 可以停止正在运行的线程。

问题:

interrupt()只能中断一个阻塞状态下的线程,是否有办法阻断一个运行中的线程?

死锁及相关

死锁就是多个并发进程因争夺系统资源而产生相互等待的现象。

如果一组进程中的每一个进程都在等待仅由该组进程中的其他进程才能引发的时间,那么该组进程就是死锁的(Deadlock)。

与此相类似的是线程饿死,指线程永远的卡在就绪状态,获取不到cpu执行时间。而死锁则是一直卡在阻塞态。

产生条件

  • 互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源。

  • 请求和保持条件:进程获得一定的资源后,又对其他资源发出请求,但是该资源可能被其他进程占有,此时请求阻塞,但该进程不会释放自己已经占有的资源。

  • 不可剥夺条件:进程已获得的资源,在未完成使用之前,不可被剥夺或不能被其他进程强行夺走,只能在使用后自己释放。

  • 循环等待条件:进程发生死锁后,必然存在一个进程-资源之间的环形链 ,环路中每个进程都在等待下一个进程所占有的资源。

例如下面的代码中的两个线程将一直卡在阻塞态,因双方都持有对方需要的对象锁,而有没有额外的措施对持有的锁进行重新分配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Slf4j
public class DeadLockTest {
public static void main(String[] args) {
Object o1=new Object();
Object o2=new Object();
new Thread(() -> {
synchronized (o1) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (o2) {
log.info("线程1:{} 执行中", Thread.currentThread().getName());
}
}
}).start();
new Thread(() -> {
synchronized (o2) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (o1) {
log.info("线程2:{} 执行中", Thread.currentThread().getName());
}
}
}).start();
}
}

死锁的预防

  • 破坏互斥条件:改造独占性资源为虚拟资源,大部分资源已无法改造。即允许进程同时访问某些资源。

    但是,有的资源是不允许被同时访问的,像打印机等等。所以,这种办法并无实用价值。

  • 破坏不可剥夺条件:当一进程占有一独占性资源后又申请一独占性资源而无法满足,则退出原占有的资源。

    当一个进程已占有了某些资源,它又申请新的资源,但不能立即被满足时,它必须释放所占有的全部资源,以后再重新申请。这就相当于该进程占有的资源被隐蔽地强占了。这种预防死锁的方法实现起来困难,会降低系统性能。

  • 破坏请求与保持条件:

    采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待,这样就不会占有且申请。

    可以实行资源预先分配策略。即进程在运行前,一次性地向系统申请它所需要的全部资源。

  • **破坏循环等待条件:**实现资源有序分配策略,对所有设备实现分类编号,所有进程只能采用按序号递增的形式申请资源。

    首先给系统中的资源编号,规定每个进程,必须按编号递增的顺序请求资源,同类资源一次申请完。也就是说,只要进程提出申请分配资源Ri,则该进程在以后的资源申请中,只能申请编号大于Ri的资源。

线程优先级

优先级代表cpu调度时获得cpu时间的几率大小,线程的优先级具有一定的规则性,也就是CPU尽量将执行资源让给优先级比较高的线程。特别地,高优先级的线程总是大部分先执行完,但并不一定所有的高优先级线程都能先执行完。
在 Java 中,线程的优先级具有继承性,比如 A 线程启动 B 线程, 那么 默认B 线程的优先级与 A 是一样的。

守护进程

在 Java 中,线程可以分为两种类型,即用户线程和守护线程。
任何一个守护线程都是整个JVM中所有非守护线程的保姆,只要当前JVM实例中存在任何一个非守护线程没有结束,守护线程就在工作;只有当最后一个非守护线程结束时,守护线程才随着JVM一同结束工作。

额外补充

首先Object类中有五个方法wait(long timeout, int nanos)、wait(long timeout)、wait()、notify()、notifyAll(),几种方法关系内部锁与线程的等待和唤醒有关,执行上面5种方法的线程必须拥有对象控制权monitor。线程可通过synchronized代码块或方法来获得对象的控制权。

monitor翻译是监视器的意思,对象monitor即对象控制权,也就是我们常说的对象锁。在Java中,对象的控制权在同一时刻只能被一个线程所拥有。

在JVM中会为每个使用(synchronized)的对象维护两个集合,即每个锁都有两个队列,同步队列和等待队列,有的人也叫锁池和等待池。

  • 同步队列:用于存储获取锁失败的线程,他们将一起竞争同一对象的锁

  • 等待队列:用于存储调用了wait之一方法的线程。当等待队列中的线程被唤醒,就会被移到同步队列中


相关参考资料