Skip to content

GUC并发学习 ​

概念 ​

同步VS异步 ​

同步和异步通常用来形容一次方法调用。同步方法调用一开始,调用者必须等待被调用的方法结束后,调用者后面的代码才能执行。而异步调用,指的是,调用者不用管被调用方法是否完成,都会继续执行后面的代码,当被调用的方法完成后会通知调用者。比如,在超时购物,如果一件物品没了,你得等仓库人员跟你调货,直到仓库人员跟你把货物送过来,你才能继续去收银台付款,这就类似同步调用。而异步调用了,就像网购,你在网上付款下单后,什么事就不用管了,该干嘛就干嘛去了,当货物到达后你收到通知去取就好。

并发与并行 ​

并发和并行是十分容易混淆的概念。并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。实际上,如果系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能通过切换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出现在拥有多个CPU的系统中。

阻塞和非阻塞 ​

阻塞和非阻塞通常用来形容多线程间的相互影响,比如一个线程占有了临界区资源,那么其他线程需要这个资源就必须进行等待该资源的释放,会导致等待的线程挂起,这种情况就是阻塞,而非阻塞就恰好相反,它强调没有一个线程可以阻塞其他线程,所有的线程都会尝试地往前运行。

临界区 ​

临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每个线程使用时,一旦临界区资源被一个线程占有,那么其他线程必须等待。

创建线程 ​

实现Runnable接口 ​

  • 特点:解耦任务与线程,支持 Lambda 表达式,代码简洁。
java
Thread runnableThread = new Thread(()-> System.out.println("Runnable线程启动"));
        runnableThread.start();

继承Thread重写run()方法 ​

  • 特点:简单直观,但 Java 不支持多继承,限制了类的扩展性。
java
 Thread extendThread = new Thread() {
            @Override
            public void run() {
                super.run();
                System.out.println("继承线程启动");
            }
        };
        extendThread.start();

使用线程池 ​

  • 特点:线程可复用,避免频繁创建销毁的开销,性能最好。
java
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<String> submit = executorService.submit(() -> {
    System.out.println("线程池线程启动");
    return "线程池线程返回";
});
try {
    String callBack = submit.get();
    System.out.println(callBack);
} catch (InterruptedException e) {
    throw new RuntimeException(e);
} catch (ExecutionException e) {
    throw new RuntimeException(e);
}

线程的状态 ​

image-20260122110828580

状态名称英文核心含义
新建NEW线程对象已创建,但尚未调用 start() 方法启动。
可运行RUNNABLE线程正在运行或正在等待 CPU 调度(就绪)。
阻塞BLOCKED线程正在等待获取一个监视器锁(进入 synchronized 块/方法)。
无限等待WAITING线程无限期等待另一个线程执行特定操作(如 notify)。
计时等待TIMED_WAITING线程在指定时间后会自动唤醒(如 sleep(3000))。
终止TERMINATED线程执行完毕或因异常退出,生命周期结束。

当线程进入到synchronized方法或者synchronized代码块时,线程切换到的是BLOCKED状态,而使用java.util.concurrent.locks下lock进行加锁的时候线程切换的是WAITING或者TIMED_WAITING状态,因为lock会调用LockSupport的方法。

线程状态的基本操作 ​

interrupted ​

interrupted 是 Java 中 Thread 类的一个静态方法,它的作用是检测当前正在执行的线程是否已经被中断,并且它会“顺手”把中断状态给清除。在结束线程时通过中断标志位或者标志位的方式可以有机会去清理资源,相对于武断而直接的结束线程,这种方式要优雅和安全。

方法行为描述
interrupted()静态方法 (作用于当前线程)。 会清除中断状态 (读完即擦除),通常在线程内部检查自己是否该停止
isInterrupted()实例方法 (作用于调用它的线程对象)。只读不改 (状态保持原样),通常在外部检查某个线程的状态

运行中(RUNNABLE):interrupt() 只是设置标志位为 true。线程是否停止,全看它自己有没有检查这个标志位。

阻塞中(BLOCKED/TIMED_WAITING):interrupt() 会打断阻塞,强制唤醒线程并抛出 InterruptedException,同时自动清除标志位。

join ​

一个线程A中调用另一个线程B的 join() 方法时,调用者A会立刻停下来等待,直到被调用者B彻底执行完毕,调用者才会继续往下走。

方法行为描述
join()无限等待。死等该线程结束,不等到天荒地老不罢休。
join(long millis)限时等待。最多等 millis 毫秒。如果超时了线程还没结束,就不等了,继续执行。
join(long millis, int nanos)限时等待(纳秒级)。精度更高,但很少用到。

sleep ​

暂停执行,让出 CPU。需要注意的是如果当前线程获得了锁,sleep方法并不会失去锁

方法行为描述
wait()死等。一直等到别人叫醒我(notify),否则永不醒来。
wait(long timeout)限时等待。最多等 timeout 毫秒。时间一到自动醒来。
wait(long timeout, int nanos)限时等待(纳秒级)。精度更高,很少用。

sleep() 和wait()

  1. sleep()方法是Thread的静态方法,而wait是Object实例方法
  2. wait()方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁。而sleep()方法没有这个限制可以在任何地方种使用。另外,wait()方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而sleep()方法只是会让出CPU并不会释放掉对象锁;
  3. sleep()方法在休眠时间达到后如果再次获得CPU时间片就会继续执行,而wait()方法必须等待Object.notift/Object.notifyAll通知后,才会离开等待池,并且再次获得CPU时间片才会继续执行。

yield ​

一旦执行,它会是当前线程让出CPU,但是,需要注意的是,让出的CPU并不是代表当前线程不再运行了,如果在下一次竞争中,又获得了CPU时间片当前线程依然会继续运行。

守护线程Daemon ​

它的核心使命是为用户线程(User Thread,即前台线程) 提供服务(如垃圾回收、监控等)。一旦所有的用户线程(主角)都执行完毕,JVM 会立即退出,此时守护线程会被强制终止,无论它手头的工作是否做完。

通过给线程设置 setDaemon(true)可以将线程设置为守护线程。非常适合执行“可丢弃”的后台任务。

守护线程在退出的时候并不会执行finnaly块中的代码,所以将释放资源等操作不要放在finnaly块中执行,这种操作是不安全的

java
 Thread daemon = new Thread(() -> {
            System.out.println("守护线程运行中");
     });
        daemon.setDaemon(true); // 设置为守护线程
        daemon.start();

Java 内存模型(JMM) ​

并发编程中主要需要解决两个问题:1. 线程之间如何通信;2.线程之间如何完成同步

内存结构 ​

java内存模型是共享内存的并发模型,线程之间主要通过读-写共享变量来完成隐式通信。

内存结构模型 ​

  • 主内存 (Main Memory):存储所有共享变量(如实例字段、静态字段),对应物理内存。
  • 工作内存 (Working Memory):每个线程都有自己的工作内存,保存了该线程使用到的变量的副本(对应 CPU 寄存器、缓存等)。
  • 交互机制:线程不能直接操作主内存,必须先将变量拷贝到工作内存,修改后再回写主内存。

共享变量 ​

在java程序中所有实例域,静态域和数组元素都是放在堆内存中(所有线程均可访问到,是可以共享的),而局部变量,方法定义参数和异常处理器参数不会在线程间共享。共享数据会出现线程安全的问题,而非共享数据不会出现线程安全的问题。

JMM抽象结构模型 ​

JMM内存模型的抽象结构示意图

如图为JMM抽象示意图,线程A和线程B之间要完成通信的话,要经历如下两步:

  1. 线程A从主内存中将共享变量读入线程A的工作内存后并进行操作,之后将数据重新写回到主内存中;
  2. 线程B从主存中读取最新的共享变量

出现“脏读时“,可以通过同步机制(控制不同线程间操作发生的相对顺序)来解决或者通过volatile关键字,使得每次volatile变量都能够强制刷新到主存

重排序 ​

为了提高性能,编译器和处理器常常会对指令进行重排序。一般重排序可以分为如下三种:

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。

happens-before规则 ​

如果操作 A happens-before 操作 B,那么 A 的结果对 B 可见。主要包含以下规则:

  • 程序顺序规则:在一个线程内,按照代码顺序,前面的操作先行发生于后面的操作。
  • 监视器锁规则:对一个锁的 unlock 操作先行发生于后续对这个锁的 lock 操作(保证了同步块内的可见性)。
  • volatile 变量规则:对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
  • 线程启动规则:Thread.start() 的调用先行发生于线程中的任何操作。
  • 线程终止规则:线程中的所有操作都先行发生于其他线程检测到该线程终止(如 join() 返回)。
  • 传递性规则:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
最近更新