avatar

并发编程线程基础

什么是线程


在讲线程之前,我们先来了解一下什么是进程。

  • 进程是代码在数据集合上的一次运行活动 是系统进行资源分配和调度的基本单位
  • 线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。

线程私有

  • 程序计数器:当执行的是native方法时,记录undefined地址;当执行Java代码的时候,记录下一条指令地址。分支、跳转、循环、异常处理、线程恢复等基础功能都需要依赖程序计数器来完成
  • 虚拟机栈:用于存储局部变量表、操作数栈、常量池引用等
  • 本地方法栈:用于存储native方法的局部变量表、操作数栈、常量池引用等

线程共享

  • 堆:用于存放对象实例。Java堆是垃圾收集器管理的主要区域,因此也被称为GC堆
  • 方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等
    • 运行时常量池:方法区的一部分,用于存放编译器生成的各种字面量和字符引用,这部分内容将在类加载后进入方法区的运行时常量池存放

线程的创建与运行


线程的创建方式

  • 实现Runnable接口的run方法

  • 继承Thread类并重写run方法

  • 使用FutureTask方式

线程的运行

调用Thread对象的start方法。调用start方法后,线程进入可运行状态,可运行状态分为就绪状态运行状态,即获取了CPU时间片后才从就绪状态转为运行状态。

线程通知与等待


wait函数

当一个线程调用一个共享变量的wait方法时,那么该调用线程会被阻塞挂起,直到以下事件之一发生:

  • 其他线程调用了该共享对象的notify或notifyAll方法,当该步执行后,则该线程释放该共享变量的锁
  • 其他线程调用了该线程的interrupt方法,该线程抛出异常返回

另外需要注意的是,如果调用 wait 方法的线程没有事先获取该对象的监视器锁,则调用 wait 方法时该线程会抛出 IllegalMonitorStateException 异常。

如何获取一个共享变量的监视器锁

  • 执行synchronized代码块时,使用该共享变量作为参数
  • 调用该共享变量的synchronized方法

虚假唤醒

即一个线程可以从挂起状态变为可以运行状态( 就是被唤醒),即使该线程没有被其他线程调用 notify、notifyAll方法进行通知,或者被中断,或者等待超时。

因此我们需要在一个while循环中不断判断被唤醒的条件是否满足,若不满足则一直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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class ProducerAndConsumer {

private String name;
private int size = 0;
static final int MAX_SIZE = 10;

public synchronized void produce(String name) {
while(true){
while (size == MAX_SIZE) {
try {
this.wait();
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
this.size++;
this.name = name;
System.out.println("produce: " + name);
this.notifyAll();
}
}

public synchronized void consume() {
while(true){
while (size <= 0) {
try {
this.wait();
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
this.size--;
System.out.println("consume: " + name);
this.notifyAll();
}
}

public static void main(String[] args) {
LeetCode leetCode = new LeetCode();

Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
leetCode.produce("MAC");
}
});

Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
leetCode.produce("Huawei");
}
});

Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
leetCode.consume();
}
});

Thread t4 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
leetCode.consume();
}
});

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

wait(long timeout)函数

该方法相 wait 方法多了一个超时参数,它的不同之处在于,如果一个线程调用共享对象的该方法挂起后,没有在指定的 timeout ms 内被其他线程调用该共享变量的notify 或者 notifyAll 方法唤醒,那么该函数还是会因为超时而返回。

notify函数

一个线程调用共享对象的 notify 方法后,会唤醒一个在该共享变量上调用 wait 系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。

此外,被唤醒的线程不能马上从 wait 方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁只有该线程竞争到了共享变量的监视器锁后才可继续执行。

notifyAll函数

不同于在共享变量上调用 notify 函数会唤醒被阻塞到该共享变量上一个线程,notifyAll 方法则会唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程。

等待线程执行终止的join方法


某个线程需要等待多个线程执行完毕才可以执行,这个时候可以使用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
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("t1");
}
});

Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("t2");
}
});

t1.start();
t2.start();
System.out.println("wait t1 t2 t3 complete");
t1.join();
t2.join();
System.out.println("main thread start");
}

让线程睡眠的sleep方法


Thread 类中有一个静态的 sleep 方法,当一个执行中的线程调用了 Thread 的 sleep 法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与 CPU 的调度,但是该线程所拥有的监视器资源,比如锁还是持有不让出的。

指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与 CPU 的调度,获取到 CPU 资源后就可以继续运行了。如果在睡眠期间其他线程调用了该线程的 interrupt 方法中断了该线程,则该线程会在调用 sleep 方法的地方抛出 InterruptedException 异常而返回。

让出CPU执行权的yield方法


Thread 有一 静态 yield 方法,当一个线程调用 yield 方法时,实际就是在暗示线程调度器当前线程请求让出自己 CPU 使用,但是线程调度器可以无条件忽略这个暗示。

当一 线程调用 yield 方法时, 当前线程会让出 CPU 使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出 CPU 的那个线程来获取 CPU 执行权。

理解线程上下文切换


当前线程使用完 CPU 时间片后,就会处于就绪状态并让出 CPU 资源让其他线程使用,这就是一次上下文切换。

线程死锁


什么是死锁

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会 直相互等待而无法继续运行下去。

死锁产生的条件:

  • 互斥条件:指线程对己经获取到的资源进行排它性使用 即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资线的线程释放该资源。
  • 请求并持有条件:指一个线程己经持有了至少一个资源,但又提出了新的资源请求,而新资源己被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不会释放自己已经拥有的资源。
  • 不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,除非自己主动释放资源。
  • 环路等待条件:指在发生死锁时,必然存在一个线程一资源的环形链,如上图。

如何避免死锁

想要避免死锁,只需要破坏其中一个条件即可。方法有以下:

  • 资源申请顺序一致:破坏了请求并持有条件和环路等待条件
  • 一次性申请所有资源:破坏了请求并保持条件
  • 线程申请某资源时若申请不到,则主动释放其已经拥有的资源:破快了不可剥夺条件

守护线程与用户线程


守护线程:由JVM自动创建,服务于用户线程(比如垃圾回收线程等)

用户线程:由用户创建,比如main函数所在的线程

区别:当所有用户线程结束时,JVM会正常退出,而与守护线程结束与否无关

ThreadLocal


ThreadLocal介绍

多钱程访问同一个共享资源时特别容易出现并发问题,特别是在多个线程需要对共享资源进行写入时,为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步。

为了解决这个问题,我们可以使用ThreadLocal类。当你创建了一个ThreadLocal变量时,每一个访问该变量的线程都会将其拷贝一个变量到自己的本地内存里。

ThreadLocal的实现原理

由上图我们可以看到,Thread类中有两个变量叫threadLocals和inheritableThreadLocals,他们都是ThreadLocalMap类型。在默认情况下,这两个变量都为null,只有当线程第一次调用ThreadLocal的set或get方法时才会创建它们。

而其实ThreadLocal就是一个工具,调用ThreadLocal实例的set方法,就是把副本保存到线程里的threadLocasl变量里,调用get方法就从线程的threadLocals里取出该副本,调用remove方法就可以在threadLocals里删除该副本。

1
2
3
4
5
6
7
8
9
10
11
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

下面讲解ThreadLocal的get、set和remove方法

get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的threadLocals变量
ThreadLocalMap map = getMap(t);
if (map != null) {
//获取value值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

set

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value); //设置value值
else
createMap(t, value);
}

remove

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* {@code initialValue} method in the current thread.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

lnheritableThreadLocal类

如果想让子线程可以访问在父线程中设置的本地变量,则可以使用lnheritableThreadLocal类。

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
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
/**
* Computes the child's initial value for this inheritable thread-local
* variable as a function of the parent's value at the time the child
* thread is created. This method is called from within the parent
* thread before the child is started.
* <p>
* This method merely returns its input argument, and should be overridden
* if a different behavior is desired.
*
* @param parentValue the parent thread's value
* @return the child thread's initial value
*/
protected T childValue(T parentValue) {
return parentValue;
}

/**
* Get the map associated with a ThreadLocal.
*
* @param t the current thread
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}

/**
* Create the map associated with a ThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the table.
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}

简单来说,就是使用inheritableThreadLocals变量代替了threadLocals变量,而当父线程创建子线程时,Thread构造函数会把父线程中的inheritableThreadLocals变量里面的本地变量复制一份保存到子线程中的inheritableThreadLocals变量里面。

Author: WJZheng
Link: https://wellenzheng.github.io/2020/04/16/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E7%BA%BF%E7%A8%8B%E5%9F%BA%E7%A1%80/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.

Comment