java多线程编程核心技术(第二版)

初识多线程

  • Thread与Runnable实现多线程区别

    • Runnable接口相较于Thread,更适合用于想创建的线程类已有一个父类的情况,java不支持多继承(p13)
    • 使用Runnable接口时,需要通过Thread类来实际启动线程。(幸好Thread有8种构造方法)
    • 二者启动一个线程的执行过程不同:JVM会直接调用Thread的run方法,而Runnable实现的需要在Thread中的init中被调用。Runnable在执行过程上比继承Thread要稍微复杂(p16)
  • 实例变量的共享问题

    • 若不想令实例变量进行共享,可以使用synchronized关键字修饰run方法(p20)
    • 两个线程向同一个对象方法传递参数时,方法的参数值不会被覆盖,因为方法的参数值是绑定到当前执行线程上的(p22-23)
  • run调用方式不同决定了其被调用的线程不同

    • 自动执行和主动执行,调用run的线程是不一样的(p26-27)
  • 线程优先级

    • 会被继承(p65)
    • 高优先级线程大部分先执行完,但不代表高优先级的线程全部先执行完(p67-68)
  • 守护线程

    • JVM会为一些系统级的线程设置守护线程
    • 将自己创建的线程设置为守护进程,需要在调用Thread.start()方法之前调用Thread.setDaemon(true)方法(p73)
  • 一个类实现Runnable接口与不实现的区别

    • 实现
      • 普通类:没有实现Runnable接口的类就是一个普通的Java类。
      • 多线程能力:此类本身不具备多线程的能力,除非显式的使用线程类(如Thread)或其他并发工具类实现多线程逻辑
      • 代码组织:类中方法和逻辑可以根据需要自由组织,没有特别限制
    • 不实现
      • 多线程能力:该类可通过创建Thread对象并传递该类的实例给Thread构造器来启动一个新的线程
      • run方法:实现Runnable接口的类必须提供run方法的实现。该方法会在新线程中被调用
      • 线程入口点:run方法充当了线程的入口点,即线程开始执行时,run方法的内容被执行
      • 代码组织:实现Runnable接口的类通常会把线程相关的逻辑放在run方法内,以此使代码更加清晰和便于维护
      • 灵活性:该类允许将线程的逻辑与线程管理分离,使得可以重用相同的线程逻辑而不必每次都创建新的线程对象

多线程中解决同步问题【synchronized】

  • synchronized同步方法(同步化)

    • 对 会被多个线程同步执行的方法 且 该方法中用到了实例变量,那么要对该方法添加synchronized关键词 (p77-79)
    • 字节码指令中的原理:执行线程先持有同步锁,然后执行方法,最后在方法完成时释放锁(p80)
  • 将synchronized方法与对象(Object)作为锁【synchronized 对象监视器为Object时的使用方法】

    • 多个对象多个锁,彼此间无锁争抢关系(p83-84)
    • java中只有 将对象作为锁 的说法,没有 锁方法 这种说法。因此使用synchronized的方法,并不是锁方法,而是锁当前类的对象(p89)
    • 锁 只针对于 对象中带有 synchronized 关键词的方法,若是非synchronized类型方法,未持锁线程可以异步调用(p89)
    • 多个线程执行同一个业务对象中的 不同 同步方法时,是按顺序同步的方式调用的(89-91)
  • synchronized 锁重入

    • 在使用synchronized时,当一个线程得到一个对象锁后,此时再请求此对象锁是可以得到该对象锁的(即,在一个synchronized方法/块 内部调用本类的其他 synchronized 方法/块时,可以永远得到锁)(常用于内部方法调用、递归调用等情形)(p91-93)
    • 支持父子类继承(子类可通过 锁重入 调用父类的同步方法)(p93-94)
    • 当一个线程执行的代码出现异常时,其所持有的锁会自动释放(p94-96)
    • 当重写方法不使用synchronized关键字,就是非同步方法;当加上synchronized,便是同步方法(p96-98)
  • synchronized 对象监视器为 Class 时的使用方法

    • 表示对整个类的所有实例的同步操作,仅针对这个类中任何synchronized方法(p99)
  • synchronized同步代码块 ⭐

    • 目的:解决synchronized声明方法时,持有对象执行时间过长导致的另一等待线程 等待时间过长的问题(p99-102)
    • 不同线程访问同一对象(object)时,不属于synchronized块中的内容就是异步执行,在synchronized块中的就是同步执行(p105-107)
    • synchronized代码块间的同步性
      • 当一个线程访问object的一个 synchronized(this) 同步代码块时,其他线程对同一个object中所有其他的synthronized(this)同步代码块的访问将被阻塞。(即synchronized使用的对象监视器是同一个,锁是同一个)(p108-109)
    • synchronized(this) 代码块是锁定当前对象的(p110-113);使用synchronized(非this对象) 时,该代码块中的程序和synchronized同步方法 是 异步执行 的,因为有两把锁,不与其他锁this同步方法争抢this锁,可提高运行效率(p113-116)
      • this这个位置一般不使用 String 相关字符串,因为String 的 常量池特性 可能会导致同步相关问题(p138-141)
    • 多个锁就是异步执行(p117-118)
    • 不同步导致的逻辑错误(p121-123)
      • 描述:有两个线程A和B调用同一个对象的非同步化方法 method1()。在这个方法内部,又调用了另一个类的同步化方法 synchronizedMethod()。尽管 synchronizedMethod() 是同步化的,但在实际运行中观察到了非同步化的结果,即出现了数据竞争或不一致性问题。
      • 原因:method1() 本身是非同步化的,这意味着线程 A 和 B 可以同时进入并执行 method1()。 + 即使 synchronizedMethod() 是同步化的,能够确保同一时刻只有一个线程能执行这个方法,但由于 method1() 的非同步性质,仍然可能出现线程 A 和 B 同时执行 method1() 中其他部分代码的情况,这可能导致数据竞争问题。
      • 解决方案:局部使用synchronized同步代码块
    • 锁对象 改变 导致同步变为异步执行(p153-156)
      • 这里的改变不是指 锁对象的属性 改变,是指‘对象’改变,如:字符串对象 一旦改变 其值,就相当于创建了新的字符串对象,此时锁对象的内存空间已经发生了变化
    • 优势:
      • 更精细的颗粒度锁定:只需对需要同步的代码段加锁,不必对整个方法进行加锁,减少锁的持有时间,提高程序的并发性能。
      • 使锁对象更加灵活:代码块中,可以指定任意对象作为锁的对象,而不仅仅局限于当前对象(this)或类(class)对象。
      • 减少锁的竞争:若一个类中有多个方法需要同步,但 这些方法在执行时 不总是需要 同时锁定,使用代码块可有效地避免不必要的锁竞争。
      • 异常处理:能够更好的处理异常,即使在代码块中抛出了异常,锁也会在finally块中被自动释放,以此避免死锁的发生
      • 避免不必要的同步:实现在特定条件下的同步,避免在不需要同步的情况下加锁。
    • 何时不用
      • 当方法体内的所有操作都应当被视为一个原子操作时,使用synchronized方法相较于synchronized代码块就更有优势。
  • 静态同步synchronized方法(p130-134)

    • 这样写是对 *.java 文件对应的Class类对象进行持锁,Class类的对象是单例的
    • 对比于将synchronized关键字加到非static方法上,加到非static是将方法所在类的对象作为锁;而将synchronized加到static方法上是将Class类对象作为锁
    • 因此,使用 syn修饰的static方法 可以对 类的所有对象实例 起作用(p135-136)
  • 同步synchronized方法无限等待问题与解决方案(p141-143)

    • 原因:因为使用synchronized关键词标注同一类中的两个方法,因此导致这两个方法的锁都是这个类的对象,因此当两个线程同时使用这个类的同一个对象时,就会因为要争抢同一个锁(该对象),而导致其中一个同步方法等待·
    • 解决:使用synchronized代码块,标注原来使用synchronized方法的部分内容,使得这两个方法所需的锁不再一致
  • synchronized 与 静态内置类(p149-152)

    • 内置类中的同步方法若使用不同的锁,则输出结果是异步的
    • 同步代码块synchronized(lock)对lock上锁后,其他线程只能以同步方式调用lock中的同步方法【对于实例方法,锁是基于该方法所属的对象的实例的,即每个对象对应一个锁;对于静态方法而言,锁是基于该方法所属类的类对象的,即无论多少个对象,锁唯一】
  • synchronized 同步写法案例(p158-159)

volatile关键字

  • volatile关键字(同步机制)(p159-165)

    • 可见性:确保实例变量在多个线程之间的可见性,其适用于任何类型的变量。一个线程修改了volatile类型的变量,对于其他所有类型线程是可见的。(即该变量的读写操作都变特殊了)
    • private仅仅代表该变量只能在声明它的类中使用,而volatile则是保证所有能够访问它的线程都是可见的
  • 不同线程为何需要同步机制

    • (java内存模型允许线程缓存变量的副本,而不是直接从主内存中读取)而java内存模型jmm有以下几点:
      • 主内存:所有线程共享的内存区域,存储的对象的实例和静态对象
      • 工作内存:每个线程都有自己的工作内存栈,其中包含了该线程使用的变量副本
      • 变量读写过程:当线程读取一个变量时,它会从主内存中复制一份到自己的工作内存中; 当线程修改一个变量时,他会修改自己工作内存中的变量副本;若没有显示的同步操作,线程不会将修改后的副本写回到主内存中,也不会从主内存中刷新自己的副本
    • 因此需要同步机制 保证对多个线程共用变量的同步操作
  • synchronized代码块具有增加可见性的作用(p165-168)

    • 当一个线程修改了一个共享变量,并且释放了锁时,其他线程再次获得锁并读取这个变量时,将会看到最新的值。(这是因为 synchronized 保证了在锁释放之前,修改过的共享变量会被写回到主内存中,并且在锁获取时,线程会从主内存中读取最新的值。)
    • 此过程只涉及被修改过的变量,而不是所有数据
  • synchronized和volatile在可见性上的区别

    • 两者可见性的个自原理:前者伴随着锁的释放与获取来写入和读取;后者则是标注对象发生修改时,会立刻写回主存,并且其它线程读到的也都是最新的
    • 原子性:前者可保证,后者不可保证
    • 互斥访问:只有前者支持
    • 防止指令重排序:前者靠锁机制实现,后者靠volatile的读写操作不会被编译器或处理器重新排序实现
    • 性能:前者慢,因为需要锁操作
    • 使用场景:前者适用于需要复杂同步逻辑(互斥访问)和保证复合操作原子性的场景;后者适用于需要保证变量可见,且不需要复合原子操作的场景
  • volatile的非原子性

    • 原子性:意味着一组操作或一个操作要么完全执行,要么完全不执行(具有不可分割性和完整性)
    • 由于原子性的操作往往是复合操作,其通常涉及多个步骤,这些步骤在没有锁的情况下可以被不同的线程交叉执行,从而导致不一致的结果
    • 因此volatile只能保证单个读写操作的原子性
  • 使用atomic类和synchronized保证复合操作的原子性的区别(p172-176)

    • 前者是无锁原子操作,该无锁操作是基于底层的硬件支持的;同时该底层硬件也避免了脏读的发生(硬件支持、内存屏障、循环CAS);后者是通过锁来实现
    • 前者性能更好,因为使用的底层硬件的原子操作以及在高并发环境下显著减少锁的竞争
    • 适用场景:前者适用于高性能场景,尤其是多个线程频繁访问同一变量;后者适用于复杂逻辑同步的情况
  • volatile和synchronized的禁止重排序

    • volatile禁止重排序:关键字volatile修饰的内容,该内容之前或之后的代码不可以跨越该内容(p176-184)
    • synchronized禁止重排序:以该关键字为界,前后代码不可跨越执行(p184-186)

synchronized和volatile的总结:

  • synchronized
    • 可见性:通过锁实现
    • 原子性:因为同步,实现了原子性,保证被同步的代码在同一时间段只有一个线程在执行
    • 禁止代码排序
  • volatile
    • 可见性:通过立马写入写出实现
    • 原子性:不支持
    • 禁止代码重排序
  • 两者使用场景
    • volatile:当想实现一个变量的值被更改时,让其他线程立马获取到最新的值
    • synchronized:当多个线程对同一个对象中的同一个实例变量进行操作时,为了保证线程安全问题

线程间通信

  • wait/notify机制(p192)(p194-198)

    • 必须是拥有相同锁的线程
    • wait()
      • 作用:使当前执行wait方法的线程等待,在wait所在的代码行处暂停执行,并立即释放锁【这里对比sleep(),sleep不会释放锁】,直到接到通知或被中断为止。【使线程暂停执行】
      • 前提:调用wait前必须获得对象级别锁【只能在同步方法或同步代码块中使用】
      • 若是使用 wait(long) 形式,则可以超过设置的等待时间后自动唤醒(p213-217)
        • 条件:需要重新持有锁
        • 因此唤醒后,若没有锁,则会一直等待,直到持有锁
    • notify
      • 作用:使暂停执行的线程继续执行
      • 前提:要在同步方法或同步块中调用,调用前必须获得锁
      • 执行notify方法的线程将程序都执行完,并且退出synchronized同步区域后,才会释放锁【不立即释放锁】
  • 线程状态的切换(p201-202)

    • runnable、running、blocked
    • running -> runnable:被高优先级线程抢占了cpu
    • runable -> blocked:1、遇到阻塞IO 2、等待notify 3、调用suspend挂起 4、调用sleep,主动放弃所占处理器资源 5、想拿的被其他线程占有
  • notify() 【唤醒顺序也可取决于JVM的具体实现】

    • 每次唤醒只通知一个线程,且按照wait执行顺序进行通知(p208-211)
  • notifyAll() 【唤醒顺序可通过JVM具体实现】

    • 全部唤醒,一般是以倒序的顺序
  • while解决wait条件发生变化的情况(p220-224)

    • 不知道有什么用
  • 生产者/消费者模式(wait/notify模式的使用)

    • 多生产与多消费:操作值(假死)(p227-232)
      • 操作值:指使用一个共享的缓冲区来存储单个值或少量数据项。消费者和生产者通过这个缓冲区进行数据交换
      • 现象:即此时线程全部进入了waiting状态,导致项目呈现停滞状态(p227-231)
      • 产生原因:极大可能是连续唤醒同类导致全部假死
      • 解决方案:粗暴的使用notifyAll全部唤醒
    • 为解决高并发场景下,消费者与生产者频繁阻塞的问题,提出 操作栈 的概念
    • 使用一生产者多消费者时问题如下(p234-238)(操作栈)
      • 使用if来作为条件判断时,很有可能出现由虚假唤醒导致的程序异常
        • 虚假唤醒:指一个线程在调用wait()方法后,虽然没有受到其他线程调用notify()或notifyAll()方法的通知,但仍然被唤醒了。【线程可能在没有任何外部条件改变的情况下被唤醒】
      • 因此使用while来代替if,称为“条件检查循环”;
      • 但是使用while时,一p多c的场景下很有可能发生假死问题
      • 因此最粗暴的方法,使用notifyAll()代替notify()的使用可以解决这些问题
    • 多生产多消费:操作栈(栈大小为1)
      • 使用while + notifyAll的使用,几乎没有问题
  • 通过管道进行线程间通信

    • 字节流(p250-253)
      • 使用PipedInputStream和PipedOutputStream
      • 使用.connect建立连接;read()在超时或可用连接关闭时才不执行,否则会一直处于阻塞等待状态
    • 字符流(p253-256)
    • 字节流与字符流的区别
      • 1、数据类型不一样;2、缓冲机制:字节流没有内置的字符缓冲机制,因此需要在读写数据时自己管理缓冲区。而字符流内设置了缓冲机制;3、编码:字节流不涉及字符编码,直接处理原始字节数据;字符流设计编码转换,在读写时会进行编码和解码;4、字节流用于处理大量原始数据,如图像、音频和视频文件。字符流用于处理文本数据。
  • join()方法的使用(p259-263)【在springboot项目中CompletableFuture 和 Future已经可以替代join】

    • 解决问题:很多情况下,主线程创建并启动子线程,若子线程要进行大量的耗时操作,主线程往往早于子线程之前结束,而join的出现就是让主线程可以等待子线程销毁后再结束。
    • 初步认识:join()就是使所属的线程对象x正常执行run()方法中的任务,而使当前线程z进行无限期的阻塞,等待线程x销毁后再继续执行线程z后面的代码,具有串联执行的效果。
    • join与synchronized的区别:
      • join()方法是在内部使用wait()方法进行等待
      • synchronized是使用锁作为同步
    • join()方法与interrupt()方法预报,必会出现异常,不管二者的先后顺序
  • join(long)的使用(p263-)

    • 用法:x.join(long)方法中的参数用于设定等待的时间,不管x线程是否执行完毕,时间到了并且 重新获得了锁 ,则当前进程会继续向后运行。若没有获得锁,则会一直尝试,直至获得锁为止。

    • 与sleep(long)的区别:

      • join(long)在内部是使用wait(long)来进行实现的,因此具有释放锁的特点;而sleep方法无特殊情况不会释放锁
    • join(long)与synchronized合用同一锁对象时,可能出现的 join方法后面的代码提前运行(迷糊)【执行的顺序可能每次都不同】

      • 一般都是因为join先抢到锁,后面的synchronized再抢到锁,因为join在内部是使用wait方法进行等待,因此再等待期间锁会被其他线程使用,当其他线程执行完并且join中的时间到期后,他会优先(or与其他线程争抢?)获得锁的使用权

ThreadLocal的使用

​ 使用ThreadLocal是为了在每个线程中存储一个独立的变量副本,用途如下:

  1. 线程隔离:ThreadLocal 允许每个线程拥有自己独立的变量副本,这意味着每个线程都可以独立地改变自己的副本,而不会影响其他线程的副本

  2. 简化代码:使用 ThreadLocal 可以避免频繁地传递线程相关的参数,从而使代码更加简洁和易于理解

  3. 线程安全:由于每个线程都有自己的变量副本,因此不存在多个线程共享同一份数据所带来的线程安全问题

  4. 性能优化:由于避免了同步开销,使用 ThreadLocal 可以提高程序的性能

  5. 资源管理:ThreadLocal 可以用来存储线程特有的资源,例如数据库连接、线程上下文等

    使用ThreadLocal的具体场景如下:

  6. 数据库连接管理:在多线程应用程序中,每个线程可以使用 ThreadLocal 来存储自己的数据库连接。这样可以避免线程间共享连接所带来的复杂性和潜在的同步问题

  7. 事务管理:ThreadLocal 可以用来存储事务状态,以便在事务逻辑中保持一致性

  8. 日志记录:ThreadLocal 可以用来存储线程特定的日志上下文,例如用户ID或其他与线程相关的元数据

  9. 状态跟踪:ThreadLocal 可以用来跟踪线程的状态,例如当前正在处理的请求或事务的状态

  10. 缓存:ThreadLocal 可以用来存储线程级别的缓存,例如计算结果或临时数据

  • 原理(p273-274)
    ThreadLocal 的主要作用是将数据放入当前线程对象中的Map中,此Map是Thread类的实例变量。类ThreadLocal不进行管理、不存储任何数据,他只是数据和Map之间的桥梁,用于将数据放入Map中。流程:数据 -》 Threadloca -》 currentThread() -》 Map
    • 每个线程在Map中会有自己的数据,见图3-57
  • ThreadLocal的存取测试以及隔离性测试(p275-281)
  • 解决get()方法返回null的问题(p282-284)

InheritableThreadLocal的使用:

  • 类ThreadLocal不能实现值继承,因此使用InheritableThreadLocal实现值继承(p285-288)

  • 值继承特性在源代码中的执行流程(p288-291)【未】

  • 父线程中的值变化时,子线程中继承的值是否会变化,取决于继承的是不是 可变对象数据类型(p294-297)

  • 重写childValue()方法实现对继承的值的加工(p297-298)

Lock对象的使用【synchronized的进阶版】

  • ReentrantLock的简单同步使用(p299-304)

    • 默认条件下,ReentrantLock创建的对象是非公平锁
  • wait/notify模式

    • ReentrantLock类对象需要借助Condition对象实现,但是使用Condition对象的await()方法前,必须保证调用Condition对象的方法已经调用lock()方法获得了锁(p304-307)
    • 使用Condition对象的await()方法和signal()方法实现wait/notify机制(p307-309)
    • ReentrantLock类中Condition对象的await()方法的执行原理:释放锁,将当前线程放入等待队列中,并阻塞当前线程直到被唤醒或中断。当线程被唤醒时,它将重新参与锁的竞争并继续执行。【await() 方法最终会调用 Unsafe 类中的 park() 方法来实现线程的挂起和等待。】(p309-312)
    • 通知部分线程(p314-321)
      • 多生产多消费中如果只使用signal(),可能会造成死锁。因此建议使用signalAll()来唤醒所有线程
  • 公平锁(p321-324)

    • 使用new ReentrantLock进行lock声明时,写入参数fair
  • lock的一些方法的使用(p324-348)

    • 注意:lock()执行后,当前线程持有该锁;await()执行后,该线程会释放对锁的持有情况
    • 简易版 实现线程按顺序执行业务(p346-349)
  • ReentrantReadWriteLock类的使用(p349355)

    • 目的:缓解在进行多线程的读操作时,RenntrantLock类因完全互斥排他的效果,导致同一时间仅有一个线程在进行读操作
    • 使用此类时,读读共享、写写互斥、写读互斥、读写互斥

定时器Timer【在Springboot项目中已经被逐渐抛弃】

  • 在JDK库中,Timer类的主要作用是设置计划任务,即在指定时间开始执行某一个任务。TimerTask类的主要作用是封装任务,是一个抽象类。
  • schedule(TimerTask task, Date time)
    • 用于在指定日期执行一次某一任务
  • new Timer() 使用时会随之启动一个新的非守护线程(TimerThread)(p356-362)
    • 一般来说此非守护线程是一直在运行的,原因是其内部有一个死循环,两个条件分别是:
      • queue为空(该队列用来存储待执行的任务)
      • newTasksMayBeScheduled为true(该标志表示是否允许新的任务被添加到队列中)
    • 若队列为空,但是允许新的任务被添加时,线程不会立即退出,而是会等待新的任务被添加,或等待已安排的任务到期。只有满足下面这种情况时,TimerThread的mainLoop()方法才会退出:只有当queue为空且newTasksMayBeScheduled为false时,此方法才会退出,从而终止线程
    • 因此使用cancel()方法终止此计时器,丢弃当前已安排的所有任务并阻止新任务被添加到队列(queue置空),但不会影响当前任务,这会导致newTasksMayBeScheduled变为false,从而导致TimerThread线程的mainLoop()方法退出,线程终止
    • cancel方法也有时效的时候:当Timer类中的cancel没有争抢到queue锁时,TimerTask类中的任务就会正常执行
  • schedule 的多任务执行(p362-366)
    • 可能会发生延时执行
    • 原因:当该方法的计划时间早于当前时间,会立即执行(而执行多个TimerTask任务时,有的执行慢了导致后面的任务的运行时间被延后)

单例模式与多线程

  • 延迟加载/懒汉模式导致多线程中创建多个实例对象(p388-390)

  • 延迟加载导致的 线程不安全的单例模式 的解决方案(p390-399)

    • 使用synchronized关键字,标注多个线程所共用的那个方法(效率低)
    • synchronized代码块(效率也低)
    • 使用DCL(双检查锁)机制实现多项成环境中 延迟加载单例模式(推荐)
      • 双检查锁 并不是指具体的两个锁 ,而是指两次检查以确保只有一个实例被创建,可减少同步开销,提高性能。
      • 第一次检查:在进入同步块之前,检查是否已创建单例实例;若已创建,则直接返回已有的实例;反之进入同步块
      • 第二次检查:在同步块内再次检查单例实例是否已被创建;若已创建,则直接返回已有的实例;反之创建实例
      • 注意:使用DCL,一定要使用volatile保持关键实例变量的同步,以此保证内存的可见性和禁止指令重排序 + 保证构造函数是私有的,以防外部创建实例
    • (不使用延迟加载/懒汉模式 则不需要整这些东西,只用做好同步机制即可)
  • 序列化与反序列化的单例模式实现(p400-402)

    • 反序列化过程的本质是从字节流中创建一个新的对象实例,而不是从现有的单例实例中恢复,因此在反序列化时并不能保证对象的单例性。(默认情况下取出的对象是多例的)
    • 解决方法:重写readResolve方法,使反序列化时不创建新的MyObject对象(继承了Serializable的类),而是复用原有的MyObject对象。【重写位置位于MyObject类中】
  • 使用static代码块实现单例模式(p402-404)

    • 这种方式并不算延迟加载/懒汉模式
    • 但其是实现单例模式非常有效的一种方式;因为 static代码块在类加载阶段就已经执行;且类加载是由JVM管理的,因此静态代码块的执行是线程安全的
  • 使用enum枚举数据类型实现单例模式(p404-407)【简单扫一眼即可】

  • 结语:

    • 到目前为止,如果一个类被spring容器管理,则Spring会确保它的单例模式,即该类的实例是 自动线程安全(Spring容器内部处理多线程环境下的单例模式) 和 自动管理(Spring容器负责实例化、初始化、销毁等生命周期管理)的。
    • 但 如果一个类没有被Spring容器管理,则需要使用DCL机制实现线程安全的单例模式。
    • 序列化与反序列化的过程除外,就算用的是spring容器,也需要自己老老实实实现readResolve方法

拾遗增补

  • 通过几种状态的验证,了解这几种状态触发的条件(p408-415)
  • 关于线程组的内容,由于springboot 和 SpringFramework 中提供了自己的并发和线程管理机制,因此不在其中直接使用ThreadGroup。
    • ExecutorService(※※)
      • 使用ExecutorService接口来管理线程池
      • 使用ThreadPoolTaskExecutorThreadPoolTaskScheduler 来创建和配置线程池
    • ScheduledExecutorService
      • Spring 提供了 ThreadPoolTaskScheduler 来实现 ScheduledExecutorService,并允许你配置线程池和调度策略
    • @Async 注解
      • Spring 提供了 @Async 注解来支持异步方法调用
      • 当方法被标记为 @Async 时,Spring 会使用配置好的 ExecutorService 来执行这些方法
      • 你可以通过 @EnableAsync@AsyncConfigurer 来配置异步方法使用的线程池
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值