Java 并发

理论基础

多线程的出现是要解决什么问题的?

众所周知,CPU、内存、V0 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:
CPU 增加了缓存,以均衡与内存的速度差异; // 导致 可见性 问题
操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/0 设备的速度差异; // 导致 原子性 问题
编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。 // 导致 有序性 问题

线程不安全是指什么?

如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。

举例说明并发出现线程不安全的本质什么? 保障 可见性,原子性和有序性。

可见性【CPU缓存引起】:一个线程对共享变量的修改,另外一个线程能够立刻看到。
原子性【分时复用引起】:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
有序性【重排序引起】:即程序执行的顺序按照代码的先后顺序执行。

Java是怎么解决并发问题的? 3个关键字,JMM 和 8个 Happens-Before

  • Java 内存模型是个很复杂的规范,推荐:https://pdai.tech/md/java/jvm/java-jvm-jmm.html
  • 理解的第一个维度:核心知识点
    JMM本质上可以理解为,Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。
    具体来说,这些方法包括(1)volatile、synchronized 和 final 三个关键字;(2)Happens-Before
  • 规则理解的第二个维度:可见性,有序性,原子性
    • 原子性:在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行要么不执行。(Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronizedLock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。)
    • 可见性:Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。(另外,通过synchronizedLock也能够保证可见性,能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。)
    • 有序性:通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。当然JMM是通过Happens-Before 规则来保证有序性的。
  • 关键字: volatile、synchronized 和 final
  • Happens-Before 规则:JVM 还规定了先行发生原则,让一个操作无需控制就能先于另一个操作完成。
    • 1.单一线程原则Single Thread rule:在一个线程内,在程序前面的操作先行发生于后面的操作。
    • 2.管程锁定规则Monitor Lock Rule:一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
    • 3.??

线程安全是不是非真即假?…不是

一个类在可以被多个线程安全调用时就是线程安全的。
线程安全不是一个非真即假的命题,可以将共享数据按照安全程度的强弱顺序分成以下五类: 不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

  1. 不可变、
  2. 绝对线程安全、
  3. 相对线程安全、
  4. 线程兼容
  5. 线程对立。
    ,,

线程安全有哪些实现思路?

  1. 互斥同步
    synchronized 和 ReentrantLock。
  2. 非阻塞同步
  3. 无同步方案
    ,,

如何理解并发和并行的区别?

并发:单核CPU宏观上可以处理多线程,是通过CPU调度实现的交替执行
并行:在多核CPU系统中利用每个处理器处理一个可以并发执行的程序,这样多个程序便可以同时执行
上下文切换:线程上下文是指某一时间点 CPU寄存器和程序计数器的内容,CPU通过时间片分配算法来循环执行任务(线程)。多线程往往会比单线程更快更能够提高并发,但同时更多的线程意味着线程创建销毁开销加大、上下文非常频繁,程序反而不能支持更高的TPS。
时间片:时间片是CPU分配给各个任务(线程)的时间。多任务系统往往需要同时执行多道作业。作业数往往大于机器的CPU数,然而一颗CPU同时只能执行一项任务,利用了时间片轮转的方式进行调度。
建议:合理设置线程数目既可以最大化利用CPU,又可以减少线程切换的开销。高并发,低耗时的情况,建议少线程;低并发,高耗时的情况,建议多线程;高并发,高耗时的情况,要分析任务类型、增加排队、加大线程数


线程基础

进程、线程、纤程

  1. Java 进程:在操作系统中,进程是基本的资源分配单位,操作系统通过进程来管理计算机的资源(指令加载到CPU、数据到内存、磁盘、网络,),每个进程有一个唯一的进程标识符(PID)用来区分。
    当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)
    Java中,一个进程通常是指一个独立运行的 Java 虚拟机(JVM)实例;每个 Java 进程都有自己的内存空间、程序计数器、寄存器等资源,启动 Java 程序就启动了一个 Java 进程。
  2. Java 线程:线程是操作系统中的基本执行单元(能够直接执行的最小代码块),是CPU调度和分派的基本单位。一个进程可包含多个线程,每个线程独立执行不同的任务,共享进程的资源。一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行。同一时刻一个CPU核心只能运行一线程,8核CPU同时可以执行8个线程代码。
    • Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。
    • 异步调用
      以调用方角度来讲,如果需要等待结果返回,才能继续运行就是同步;不需等待结果返回就能继续运行,是异步
      1. 设计:多线程可以让方法执行变为异步的(即不要巴巴干等着),比如说读取磁盘文件时,假设读取操作花费了 5 秒钟,如果没有线程调度机制,这 5 秒 cpu 什么都做不了,其它代码都得暂停…
      2. 结论:比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程ui 程序中,开线程进行其他操作,避免阻塞 ui 线程
    • 效率提升
      1. 单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用cpu ,不至于一个线程总占用 cpu,别的线程没法干活
      2. 多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。
      3. IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化
    • 进程与线程对比:
      进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
      进程拥有共享的资源,如内存空间等,供其内部的线程共享
      进程间通信较为复杂,同一台计算机的进程通信称为 IPC(Inter-process communication);不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
      线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
      线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
  3. 纤程:Java19才支持虚拟线程(纤程【协程】)
    • 纤程可以在一个线程内部创建多个纤程这些纤程之间可以共享同一个线程的资源
    • 纤程是在同一个进程内部运行的,不需操作系统的介入,可以在用户空间内实现协作式多任务处理(jvm级别的)。因此纤程的创建和销毁开销很小,可更高效地利用系统资源。
    • 先有进程,然后进程可以创建线程,线程是依附在进程里面的。
      线程里面可以包含多个协程;进程之间不共亭全局变量。线程之间共享全局变量,但是要注意资源竞争的问题
  4. 常用方法
    • start() 与 run()
      • 类型:run方法是同步方法(按代码顺序执行),而start方法是异步方法(创建新线程,进入就绪状态,等待OS调用)
      • 作用:run方法的作用是存放(执行)任务代码,不产生新线程;而start方法创建,启动一个新线程,并调用其 run方法
      • 执行次数:run方法可以被执行无数次,而start方法只能被执行一次,原因在于线程不能被重复启动
    • setName():给当前线程取名字
    • getName():获取当前线程的名字。线程存在默认名称:子线程是 Thread-索引,主线程是 main
    • sleep():让当前线程休眠多少毫秒再继续执行,Thread.sleep(0):让操作系统立刻重新进行一次cpu竞争。
      • 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞);睡眠结束后的线程未必会立刻得到执行
      • 可以使用 interrupt() 打断正在睡眠的线程,这时 sleep() 会抛出 InterruptedException.
      • 建议用 TimeUnit的 sleep() 代替 Thread 的 sleep 来获得更好的可读性,其底层还是 sleep 方法.
      • 在循环访问锁的过程中,可以加入sleep让线程阻塞时间,防止大量占用cpu资源其它线程。
    • yield():提示线程调度器尽力让出当前线程对CPU的使用,让当前线程从 Running?? 进入 Runnable 就绪状态
    • setPriority():更改该线程的优先级,1~10,(CPU忙时)优先级越大被CPU调度的几率越高。
    • getPriority():返回该线程的优先级
    • isInterrupted():判断是否打断,不会清楚打断标记
    • interrupt():中断这个线程,异常处理机制。实际上,并不会直接中断线程的执行,只是设置线程的中断标记,线程可以在适当的时候检查这个标记并自行决定中断执行。
      • 打断 sleep,wait,join 的线程,会清空打断状态
      • 打断正常运行的线程,不会清空打断状态
    • join():主线程将等待这个线程结束再顺序执行代码(异步变同步了),可以设置等待millis毫秒。
    • setDaemon(true):设置守护线程;可以通过 Thread.isDaemon()来判断
      • 默认情况下创建的线程都是用户线程(普通线程),需要等待所有的线程执行完毕后,进程才会结束
      • 当所有的用户线程退出后,守护线程会立马结束
      • 应用:垃圾回收器线程属于守护线程,tomcat用来接受处理外部的请求的线程就是守护线程

线程有哪几种状态? 分别说明从一种状态到另一种状态转变有哪些方式?

  • Java 中线程状态是用 6个enum 表示
    • NEW:初始状态,线程被构建,但是还没有调用start()
    • RUNNABLE:可能正在运行(运行状态),也可能正在等待 CPU 时间片(可运行状态)(,还可能处在OS层面的阻塞状态,如正在进行IO操作)。Java线程将操作系统中的就绪和运行两种状态统称为“运行中”。
    • BLOCKED:阻塞状态,表示线程阻塞于锁。等待获取一个排它锁,如果其线程释放了锁就会结束此状态。
    • WAITING:无限期等待,等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。如,等待一个死循环 t.join(),当前线程将无限期等待。
    • TIME_WAITING:限期等待,无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。
      调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。
      ??睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。
    • TERMINATED:终止状态。可以是线程结束任务之后自己结束,或者产生了异常而结束。

通常线程有哪几种使用方式?

有三种使用线程的方法:实现 Runnable 接口,实现 Callable 接口,继承 Thread 类。
实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。
实现接口 VS 继承 Thread:实现接口会更好一些。因为 Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;类可能只要求可执行就行,继承整个 Thread 类开销过大。

  1. 通过继承Thread类,重写run方法。
    Thread 类也实现了 Runable 接口。当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。
    1
    2
    3
    4
    public class MyThread extend Thread {
    @Override
    public void run() { ...}
    }
    1
    2
    3
    4
    5
    6
    public static void main(String[] args) {
    // 启动过程为: thread.start()->中间过程->thread.run() 即重写的方法
    MyThread mt = new MyThread();
    // 开启异步线程:异步,创建一个新线程 mt
    mt.start();
    }
  2. 通过实现Runnable接口
    需要实现 run() 方法。通过 Thread 调用 start() 方法来启动线程。
    通过 Runnable接口 把线程和任务分开了;用 Runnable 更容易与线程池等高级 API 配合;用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
    1
    2
    3
    4
    public class MyRunnable implements Runnable {
    @Override
    public void run() { ...}
    }
    1
    2
    3
    4
    5
    6
    7
    public static void main(String[] args) {
    // 通过实现Runnable接口。实例化Thread,传递一个Runable任务
    // 启动过程为: thread.start()->中间过程>thread.run() 默认逻辑->runable.run() 实际业务
    Thread thread = new Thread(new MyRunnable());
    thread.start();
    }
    // 主线程main、线程thread 异步执行
  3. 实现 Callable 接口
    与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。
    1
    2
    3
    public class MyCallable implements Callable<Integer> {
    public Integer call() { ...}
    }
    1
    2
    3
    4
    5
    6
    7
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    MyCallable mc = new MyCallable();
    FutureTask<Integer> ft = new FutureTask<>(mc);
    Thread thread = new Thread(ft);
    thread.start();
    System.out.println(ft.get());
    }

基础线程机制有哪些?

  • Executor:管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。
    这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。主要有三种 Executor:
    • CachedThreadPool: 一个任务创建一个线程;
    • FixedThreadPool: 所有任务只能使用固定大小的线程;
    • SingleThreadExecutor: 相当于大小为 1 的 FixedThreadPool。
  • 1
    2
    3
    4
    5
    6
    7
    public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < 5; i++) {
    executorService.execute(new MyRunnable());
    }
    executorService.shutdown();
    }
  • Daemon:守护线程,是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。
    当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。main() 属于非守护线程。使用 setDaemon() 方法将一个线程设置为守护线程。
    1
    2
    3
    4
    public static void main(String[] args) {
    Thread thread = new Thread(new MyRunnable());
    thread.setDaemon(true);
    }

线程的中断方式有哪些?

一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。

  • InterruptedException:通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
  • interrupted():如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。
    但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。
  • Executor 的中断操作:调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。
    ???
  • 过时方法:stop(), suspend()休眠, resume()??

线程的互斥同步方式有哪些? 如何比较和选择?

Java 提供两种锁机制来控制多个线程对共享资源的互斥访问:JVM 实现的 synchronized,JDK 实现的 ReentrantLock
???
https://pdai.tech/md/java/thread/java-thread-x-thread-basic.html#%E7%BA%BF%E7%A8%8B%E4%BA%92%E6%96%A5%E5%90%8C%E6%AD%A5

线程之间有哪些协作方式?

当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调。

  • join():在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。
    对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先于 b 线程的输出。
  • wait(),notify(),notifyAll()
    调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。它们都属于 Object 的一部分,而不属于 Thread。只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateExeception。使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。
  • await(),signal(),signalAll()
    java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。

Java锁

Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。

  1. 乐观锁 VS 悲观锁
    乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。Java和数据库中都有此概念对应的实际应用。
    • 对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。
    • 乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果数据没有被更新,当前线程将自己修改的数据成功写入;如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。
      乐观锁在Java中是通过使用无锁编程来实现,最常用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
    • 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
      乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
  2. 独享锁(排他锁) VS 共享锁
    • 独享锁也叫排他锁(写锁),指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。
    • 共享锁(读锁)是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。

CAS

不加锁的情况下保持数据读写一致。
CAS在java中的底层实现: lockcmpxchg

对象的内存布局??

对象头、类型指针、实例数据、对齐


共享模型之管程

一个程序运行多个线程本身是没有问题的,问题出在多个线程访问共享资源;多个线程读共享资源其实也没有问题,在多个线程对共享资源读写操作时发生指令交错,就会出现问题。
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
为了避免临界区的竞态条件发生,有多种手段可以达到目的。阻塞式的解决方案:synchronized,Lock;非阻塞式的解决方案:原子变量。

synchronized 解决方案

  • synchronized,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。
    synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,拥有锁的线程可以安全的执行临界区内的代码,不会被线程切换所打断。临界区中的代码只能被串行运行。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    static int counter = 0;
    // 同一时刻 t1,t2 中只有其一能持有对象锁 room 以执行临界区中的代码
    static final Object room = new Object();
    Thread t1 = new Thread(() -> {
    for (int i = 0; i < 5000; i++) {
    // 当线程 t1 执行到 synchronized(room) 时持有了对象锁,执行 count++ 代码
    // 这中间即使 t1 的 cpu 时间片不幸用完,其仍将持有对象锁
    synchronized (room) {
    counter++; // 获取了对象锁后,自增的四个原子操作便作为一个整体,不可打断
    }
    }
    }, "t1");

    Thread t2 = new Thread(() -> {
    for (int i = 0; i < 5000; i++) {
    // 如果 t2 运行到 synchronized(room) 时 t1 仍持有对象锁,t2 将发生上下文切换,阻塞
    // 当 t1 执行完 synchronized{} 块内的代码,这时才释放对象锁,唤醒 t2 线程并让 t2 持有锁
    synchronized (room) {
    counter--;
    }
    }
    }, "t2");
  • 面向对象改进
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class Room {
    // 需要保护的共享变量作为类的成员变量
    int value = 0;
    public void increment() {
    // 自身实例化的对象作为锁
    synchronized (this) {
    value++;
    }
    }
    public void decrement() {
    synchronized (this) {
    value--;
    }
    }
    public int get() {
    synchronized (this) {
    return value;
    }
    }
    }
  • 方法上的 synchronized
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Test{
    public synchronized void test() {
    // 在这个方法内,只有一个线程能够访问 Resource
    Resource++;
    }
    }
    // 等价于代码块级别的:
    class Test{
    public void test() {
    // 该实例化对象作为锁
    synchronized(this) {
    Resource++;
    }
    }
    }
  • 静态方法上的 synchronized
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Test{
    public synchronized static void test() {
    }
    }
    // 等价于
    class Test{
    public static void test() {
    // 该类作为锁
    synchronized(Test.class) {

    }
    }
    }
  • 所谓的“线程八锁”
    ,,,

线程安全分析

  • 成员变量和静态变量是否线程安全?
    • 如果它们没有共享,则线程安全;
    • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况:
      • 如果只有读操作,则线程安全
      • 如果有读写操作,则这段代码是临界区,需要考虑线程安全
  • 局部变量是否线程安全?
    • 局部变量是线程安全的
      线程调用 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份(线程私有局部变量表),因此不存在共享
    • 但局部变量引用的对象则未必:如果该对象没有逃离方法的作用访问,它是线程安全的;如果该对象逃离方法的作用范围,需要考虑线程安全
  • 常见线程安全类
    String、Integer、StringBuffer、Random、Vector、Hashtable、java.util.concurrent 包下的类
    这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。它们的每个方法是原子的,但注意它们多个方法的组合不是原子的!!
    1
    2
    3
    4
    5
    // 下面代码是否线程安全?不安全
    Hashtable table = new Hashtable();
    if( table.get("key") == null) {
    table.put("key", value);
    }
  • 不可变类线程安全性
    String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
    String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?生成并且指向新的类。
  • 实例分析
    https://www.bilibili.com/video/BV16J411h7Rd?p=69&vd_source=ff210768dfaee27c0d74f9c8c50d7274
  • 例题
    https://www.bilibili.com/video/BV16J411h7Rd?p=69&vd_source=ff210768dfaee27c0d74f9c8c50d7274

Monitor

,,

wait/notify

,,

线程状态转换

,,

活跃性

,,

Lock

,,


非共享模型

,,


JUC 类汇总

JUC是 java.util.concurrent包的缩写,说白了就是并发场景进行多线程编程的工具类。
总的来说就是在并发场景下,怎么让程序尽量通过有限的硬件,高效的处理请求,并且保证程序“线程安全”。
开发高并发、高性能系统(1)加快响应用户的时间(2)使代码模块化、异步化、简单化(3)充分利用CPU的多核资源

Java 线程池 ??

  • JDK 线程池
    • ExecutorService
    • ScheduledExecutorService(定时任务)
  • Spring 线程池
    • ThreadPoolTaskExecutor
    • ThreadPoolTaskScheduler(定时任务)
  • 分布式定时任务
    • Spring Quartz:不同服务器上的quartz线程依赖于同一个数据库
  • 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
    // JDK普通线程池
    private ExecutorService executorService = Executors.newFixedThreadPool(5);
    // JDK可执行定时任务的线程池
    private ScheduledExecutorService scheduedExecutorSevice = Executors.newScheduledThreadPool(5);
    // Spring普通线程池
    @Autowired
    private ThreadPoolTaskExecutor taskExecutor;
    // Spring可执行定时任务的线程池
    @Autowired
    private ThreadPoolTaskScheduler taskScheduler;

    // 在 AlphaService 中给方法加上 @Async,使该方法在多线程环境下被异步的调用(被主线程并发并发,异步执行)
    // 在 AlphaService 中给方法加上 @Scheduled,只要有程序再跑就会自动调用
    @Autowired
    private AlphaService alphaService;

    // 1.JDK普通线程池
    public void testExecutorService() {
    Runnable task = new Runnable() {};
    for (int i = 0; i < 10; i++) {
    executorService.submit(task);
    }
    }
    // 2.JDK定时任务线程池
    public void testScheduledExecutorService() {
    Runnable task = new Runnable() {};
    scheduledExecutorService.scheduleAtFixedRate(task, 10000, 1000, TimeUnit.MILLISECONDS);
    }
    // 3.Spring普通线程池
    public void testThreadPoolTaskExecutor() {
    Runnable task = new Runnable() {};
    for (int i = 0; i < 10; i++) {
    taskExecutor.submit(task);
    }
    }
    // 4.Spring定时任务线程池
    public void testThreadPoolTaskScheduler() {
    Runnable task = new Runnable() {};
    Date startTime = new Date(System.currentTimeMillis() + 10000);
    taskScheduler.scheduleAtFixedRate(task, startTime, 1000);
    }
    // 5.Spring普通线程池(简化)
    public void testThreadPoolTaskExecutorSimple() {
    for (int i = 0; i < 10; i++) {
    // 并发,异步执行 AlphaService 中的 加了@Async 的 execute1()
    alphaService.execute1();
    }
    }
    // 6.Spring定时任务线程池(简化)
    public void testThreadPoolTaskSchedulerSimple() {
    // 自动调用 AlphaService 中的加上 @Scheduled 的方法
    }