并发笔记二 Java并行程序基础(一)

2021/11/8

# 关于线程

# 进程

**进程(Process)**是计算机中的程序关于某数据集合上的一次运行活动。

  • 进程是系统资源分配的最小单位(线程是CPU调度的最小单位)。
  • 进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

# 关于进程与线程的区别的理解

转载一个知乎上看到的 biaodianfu的回答 (opens new window)

做个简单的比喻:进程=火车,线程=车厢

  • 线程在进程下行进(单纯的车厢无法运行)
  • 一个进程可以包含多个线程(一辆火车可以有多个车厢)
  • 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
  • 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
  • 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
  • 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
  • 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
  • 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-"互斥锁"
  • 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”

# Java 线程生命周期

Java 线程生命周期

# 线程的基本操作

# 新建线程

start() 方法:会新建一个线程并让这个线程执行 run() 方法。

在当前线程直接执行 run() 方法不会创建新的线程,只会当做普通方法调用,串行执行。

# Thread类和Runnable接口

实现多线程可以通过继承 Thread 类,重写 run() 方法或是**实现 Runnable 接口,重写 run() **来实现。

实际上,默认的 Thread.run() 就是调用内部的 Runnable 接口。因此推荐直接使用 Runnable 接口。

通常可以继承 Runnable 接口,并将对应实例传入 Thread 构造方法中,这样可以避免重写 run() 方法。

# 终止线程

Thread 类中有一个 stop() 方法,但是被标记为了 Deprecated 。原因在于线程被 stop() 终止时,会立即释放这个线程所持有的锁,而这些锁是为了维持对象一致性的。

# 线程中断

有3个方法与线程中断有关:

// 中断线程
public void Thread.interrupt()
// 判断是否被中断
public boolean Thread.isInterrupted()
// 判断是否被中断,并清除当前中断状态
public static boolean Thread.interrupted()
1
2
3
4
5
6

# interrupt() 方法

中断目标线程,给目标线程发一个中断信号,线程被打上中断标记。

如果 sleep() 方法被中断,会清除中断标志位,如果此时调用 isInterrupted() 会返回 false。因此可以在捕获异常的时候手动调用interrupt():

private static void test() throws InterruptedException {
    Thread thread = new Thread(() -> {
        while (true) {
            // 响应中断
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("线程被中断,程序退出。");
                return;
            }

            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                System.out.println("线程休眠被中断,程序退出。");
                Thread.currentThread().interrupt();
            }
        }
    });
    thread.start();
    Thread.sleep(2000);
    thread.interrupt();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# isInterrupted() 方法

判断目标线程是否被中断,不会清除中断标记。

在 JDK17 中(不清楚是否是17新特性,应该不是),Thread 类中拥有一个 private volatile boolean interrupted 中断标志位,因此该方法是直接返回的标志位:

// JDK 17
public boolean isInterrupted() {
    return interrupted;
}
1
2
3
4

而 JDK8 中,该方法则是一个native方法:

// JDK 8
private native boolean isInterrupted(boolean ClearInterrupted);
1
2

# interrupted() 方法

测试当前线程是否已中断。此方法会清除线程的中断状态。

这里 JDK17 中也有不同的实现:

// JDK 17
public static boolean interrupted() {
    Thread t = currentThread();
    boolean interrupted = t.interrupted;
    // We may have been interrupted the moment after we read the field,
    // so only clear the field if we saw that it was set and will return
    // true; otherwise we could lose an interrupt.
    if (interrupted) {
      t.interrupted = false;
      clearInterruptEvent();
    }
    return interrupted;
}

// JDK 8
public static boolean interrupted() {
    // 调用上面的 native 方法
    return currentThread().isInterrupted(true);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 等待(wait)和通知(notify)

Object 类中提供了 wait()notify() 两个方法。

如果一个线程带哦通了 object.wait() 方法,那么它会进入object对象的等待队列。这个等待队列可能有多个线程(都在等待)。当 object.notify() 方法被调用的时候,会从等待队列中随机选择一个线程(并不公平,不按照先来后到,完全随机),并将其唤醒。

Object.wait() 方法并不能随便调用。它必须包含在对应的 synchronzied 语句中,

# wait() 方法和 notify() 方法工作流程

# wait() 和 sleep() 的区别

  • wait() 会释放目标对象的锁。

  • sleep() 不会释放资源。

# 挂起(suspend)和继续执行(resume)线程

  • suspend() 方法在导致线程暂停的同时,不会释放任何资源。如果 resume() 方法操作意外在 suspend() 方法之前就执行,很容易产生死锁。
  • 对于被挂起的线程,线程状态显示为 Runnable。(严重影响我们对系统当前状态的判断)

可以参见[参考代码](#suspend() 造成的死锁问题),演示 suspend() 造成的死锁问题。

# 等待线程结束(join)和谦让(yeild)

# join() 方法

JDK中对于 join() 的描述:"Waits for this thread to die. "。join主要提供了3个方法(重载):

// 无限等待,一直阻塞当前线程,直到目标线程执行完毕
public final void join() throws InterruptedException

// 设定最大等待时间
public final synchronized void join(final long millis)
throws InterruptedException

public final synchronized void join(long millis, int nanos)
throws InterruptedException
1
2
3
4
5
6
7
8
9

join() 核心逻辑代码:

// isAlive() 是一个 native 方法,用于判断线程是否还活着
while (isAlive()) {
    // 调用在当前线程对象实例上的wait()方法
    wait(0);
}
1
2
3
4
5

当线程结束的时候会调用 notifyAll() 方法通知所有等待的线程继续执行。

所以推荐不要在线程实例上使用 wait, notify, 或者 notifyAll 方法

# yield() 方法

JDK 注释:yield() 是一种启发式尝试,旨在改善线程之间的相对进度,否则会过度使用CPU。

通俗的解释:当前线程会让出CPU,但是让出之后会重新进行CPU资源争夺,能否再抢到是不一定的。适当的时候调用该方法,可以给其他重要线程更多的工作机会。

# 参考代码

# suspend() 造成的死锁问题

public class BadSuspend {
    public static Object u = new Object();
    static ChangeObjectThread t1 = new ChangeObjectThread("t1");
    static ChangeObjectThread t2 = new ChangeObjectThread("t2");

    public static class ChangeObjectThread extends Thread {
        public ChangeObjectThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            synchronized (u) {
                System.out.println("in " + getName());
                currentThread().suspend();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        t1.start();
        Thread.sleep(100);
        t2.start();
        t1.resume();
//        这里如果sleep一下 就没有问题了
//        Thread.sleep(100);
        t2.resume();
        t1.join();
        t2.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
Last Updated: 2021/12/7 下午2:23:53