JUC学习

进程与线程

进程

  • 程序由指令和数据组成,数据要读写,就必须将指令加载至CPU,数据加载至内存,在指令运行过程中还需要用到磁盘,网络等设备.进程就是用来加载指令,管理内存,管理IO的
  • 进程可以视为程序的一个实例,大部分程序可以运行多个实例进程,有的程序也只能启动一个实例进程

线程

  • 一个进程之内可以分一道多个线程
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行
  • Java中,线程作为最小的调度单位,进程作为资源分配的最小单位

区别

  • 进程相互独立,而线程存在于进程内,是进程的一个子集
  • 进程拥有共享的资源,如内存空间,供其内部的线程共享
  • 进程间通信较为复杂
  • 线程间通信较为简单,因为他们共享进程内的内存,一个例子是多线程可以访问同一个共享变量
  • 线程更轻量,线程上下文切换成本一般要比进程上下文切换低

并行与并发

  • 并发是同一时间应对多件事情的能力
  • 并行是同一时间动手做多件事情的能力

案例:

  • 家庭主妇做饭,打扫卫生,给孩子喂奶,一个人轮流交替做,就是并发
  • 家庭主妇雇了保姆,他们一起干,既有并发,又有并行

Java线程

创建和运行线程

  • 方法一,直接使用Thread
1
2
3
4
5
6
7
8
// 创建线程对象
Thread t = new Thread() {
public void run() {
//要执行的任务
}
};
// 启动线程
t.start();
  • 方法二,使用Runable,配合Thread

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 创建任务对象
    Runnable task2 = new Runnable() {
    @Override
    public void run() {
    log.debug("hello");
    }
    };
    // 参数1 是任务对象; 参数2 是线程名字,推荐
    Thread t2 = new Thread(task2, "t2");
    t2.start()
    1
    2
    3
    4
    5
    // 创建任务对象
    Runnable task2 = () -> log.debug("hello");
    // 参数1 是任务对象; 参数2 是线程名字,推荐
    Thread t2 = new Thread(task2, "t2");
    t2.start();
    • 方法三,FutureTask 配合 Thread

      futureTask能够接收Callable类型的参数,用来处理有返回结果的情况

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 创建任务对象
    FutureTask<Integer> task3 = new FutureTask<>(() -> {
    log.debug("hello");
    return 100;
    });
    // 参数1 是任务对象; 参数2 是线程名字,推荐
    new Thread(task3, "t3").start();
    // 主线程阻塞,同步等待 task 执行完毕的结果
    Integer result = task3.get();
    log.debug("结果是:{}", result);

栈与栈帧

JVM由堆,栈,方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的方法

线程上下文切换

  • 线程的cpu时间片用完

  • 垃圾回收

  • 有更高优先级对的线程需要运行

  • 线程自己调用了sleep,yield,wait,join,park,synchronized,lock等方法

    当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念
    就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等

  • Context Switch 频繁发生会影响性能

常用方法

image-20230522204603958

Sleep与yield

Sleep

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  3. 睡眠结束后的线程未必会立刻得到执行
  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

yield

  1. . 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程具体的实现依赖于操作系统的任务调度器

两阶段终止模式

一个线程t1如何优雅的终止线程t2? 给t2一个料理后事的机会

错误思路

  • 使用stop()方法 停止线程

    stop方法会真正的杀死线程,如果线程锁住了共享资源,那么它被杀死后无法释放锁,其他线程将永远无法获取锁

  • 使用System.exit(int) 方法停止线程

    目的仅是停止一个线程,但会让整个程序都停止

两阶段终止模式

image-20230523094600569

1.利用isInterrupted

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
class TPTInterrupt {
private Thread thread;
public void start() {
thread = new Thread(() -> {
while (true) {
Thread current = Thread.currentThread();
if (current.isInterrupted()) {
log.debug("料理后事");
break;
}
try {
// 睡眠过程中打断 会清除打断标记 导致程序一直运行
Thread.sleep(1000);
log.debug("将结果保存");
} catch (InterruptedException e) {
// 重新设置打断标记
current.interrupt();
}
// 执行监控操作
}
}, "监控线程");
thread.start();
}

public void stop() {
thread.interrupt();
}
}