《Java高并发核心编程.卷2,多线程、锁、JMM、JUC、高并发设计模式》读书笔记
2022-05-10 18:38:28 50 举报
AI智能生成
《Java高并发核心编程.卷2,多线程、锁、JMM、JUC、高并发设计模式》
作者其他创作
大纲/内容
第1章 多线程原理与实战
1.2 无处不在的进程和线程
1.2.1 进程的基本原理
1.2.2 线程的基本原理
1.2.3 进程与线程的区别
1.3 创建线程的4种方法
1.3.1 Thread类详解
1.3.2 创建一个空线程
1.3.3 线程创建方法一:继承Thread类创建线程类
1.3.4 线程创建方法二:实现Runnable接口创建线程目标类
1.3.5 优雅创建Runnable线程目标类的两种方式
1.3.6 通过实现Runnable接口的方式创建线程目标类的优缺点
1.3.7 线程创建方法三:使用Callable和FutureTask创建线程
1.3.8 线程创建方法四:通过线程池创建线程
1.4 线程的核心原理
1.4.1 线程的调度与时间片
1.4.2 线程的优先级
1.4.3 线程的生命周期
1.4.4 一个线程状态的简单演示案例
1.4.5 使用Jstack工具查看线程状态
1.5 线程的基本操作
1.5.1 线程名称的设置和获取
1.5.2 线程的sleep操作
1.5.3 线程的interrupt操作
1.5.4 线程的join操作
1.5.5 线程的yield操作
1.5.6 线程的daemon操作
1.5.7线程状态总结
1.6线程池原理与实战
1.6.1 JUC的线程池架构
1.Executor
Executor是Java异步目标任务的“执行者”接口,其目标是执行目标任务。“执行者”Executor提供了execute()接口来执行已提交的Runnable执行目标实例。Executor作为执行者的角色,其目的是提供一种将“任务提交者”与“任务执行者”分离开来的机制。它只包含一个函数式方法:
2.ExecutorService
ExecutorService继承于Executor。它是Java异步目标任务的“执行者服务接”口,对外提供异步任务的接收服务。ExecutorService提供了“接收异步任务并转交给执行者”的方法,如submit系列方法、invoke系列方法等,具体如下:
3.AbstractExecutorService
AbstractExecutorService是一个抽象类,它实现了ExecutorService接口。AbstractExecutorService存在的目的是为ExecutorService中的接口提供默认实现。
4.ThreadPoolExecutor
ThreadPoolExecutor就是大名鼎鼎的“线程池”实现类,它继承于AbstractExecutorService抽象类。ThreadPoolExecutor是JUC线程池的核心实现类。线程的创建和终止需要很大的开销,线程池中预先提供了指定数量的可重用线程,所以使用线程池会节省系统资源,并且每个线程池都维护了一些基础的数据统计,方便线程的管理和监控。
5.ScheduledExecutorService
ScheduledExecutorService是一个接口,它继承于ExecutorService。它是一个可以完成“延时”和“周期性”任务的调度线程池接口,其功能和Timer/TimerTask类似。
6.ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor继承于ThreadPoolExecutor,它提供了ScheduledExecutorService线程池接口中“延时执行”和“周期执行”等抽象调度方法的具体实现。ScheduledThreadPoolExecutor类似于Timer,但是在高并发程序中,ScheduledThreadPoolExecutor的性能要优于Timer。
7.Executors
Executors是一个静态工厂类,它通过静态工厂方法返回ExecutorService、ScheduledExecutorService等线程池示例对象,这些静态工厂方法可以理解为一些快捷的创建线程池的方法。
1.6.2 Executors的4种快捷创建线程池的方法
1.newSingleThreadExecutor创建“单线程化线程池”
1. 特点
(1)单线程化的线程池中的任务是按照提交的次序顺序执行的。
(2)池中的唯一线程的存活时间是无限的。
(3)当池中的唯一线程正繁忙时,新提交的任务实例会进入内部的阻塞队列中,并且其阻塞队列是无界的。
2. 适用场景
任务按照提交次序,一个任务一个任务地逐个执行的场景
2.newFixedThreadPool创建“固定数量的线程池”
1. 特点
(1)如果线程数没有达到“固定数量”,每次提交一个任务线程池内就创建一个新线程,直到线程达到线程池固定的数量。
(2)线程池的大小一旦达到“固定数量”就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
(3)在接收异步任务的执行目标实例时,如果池中的所有线程均在繁忙状态,新任务会进入阻塞队列中(无界的阻塞队列)。
2. 适用场景
需要任务长期执行的场景。“固定数量的线程池”的线程数能够比较稳定地保证一个数,能够避免频繁回收线程和创建线程,故适用于处理CPU密集型的任务,在CPU被工作线程长时间占用的情况下,能确保尽可能少地分配线程
3. 弊端
内部使用无界队列来存放排队任务,当大量任务超过线程池最大容量需要处理时,队列无限增大,使服务器资源迅速耗尽。
3.newCachedThreadPool创建“可缓存线程池”
1. 特点
1)在接收新的异步任务target执行目标实例时,如果池内所有线程繁忙,此线程池就会添加新线程来处理任务。
(2)此线程池不会对线程池大小进行限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
(3)如果部分线程空闲,也就是存量线程的数量超过了处理任务数量,就会回收空闲(60秒不执行任务)线程
2. 适用场景
需要快速处理突发性强、耗时较短的任务场景,如Netty的NIO处理场景、REST API接口的瞬时削峰场景。“可缓存线程池”的线程数量不固定,只要有空闲线程就会被回收;接收到的新异步任务执行目标,查看是否有线程处于空闲状态,如果没有就直接创建新的线程。
3. 弊端
线程池没有最大线程数量限制,如果大量的异步任务执行目标实例同时提交,可能会因创建线程过多而导致资源耗尽。
4.newScheduledThreadPool创建“可调度线程池”
1.6.3线程池的标准创建方式
1.核心和最大线程数量
(1)当在线程池接收到新任务,并且当前工作线程数少于corePoolSize时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求,直到线程数达到corePoolSize。
(2)如果当前工作线程数多于corePoolSize数量,但小于maximumPoolSize数量,那么仅当任务排队队列已满时才会创建新线程。通过设置corePoolSize和maximumPoolSize相同,可以创建一个固定大小的线程池。
(3)当maximumPoolSize被设置为无界值(如Integer.MAX_VALUE)时,线程池可以接收任意数量的并发任务。
(4)corePoolSize和maximumPoolSize不仅能在线程池构造时设置,也可以调用setCorePoolSize()和setMaximumPoolSize()两个方法进行动态更改。
2.BlockingQueue
BlockingQueue(阻塞队列)的实例用于暂时接收到的异步任务,如果线程池的核心线程都在忙,那么所接收到的目标任务缓存在阻塞队列中。
3.keepAliveTime
线程构造器的keepAliveTime(空闲线程存活时间)参数用于设置池内线程最大Idle(空闲)时长(或者说保活时长),如果超过这个时间,默认情况下Idle、非Core线程会被回收。
默认情况下,Idle超时策略仅适用于存在超过corePoolSize线程的情况。但若调用了allowCoreThreadTimeOut(boolean)方法,并且传入了参数true,则keepAliveTime参数所设置的Idle超时策略也将被应用于核心线程。
1.6.4向线程池提交任务的两种方式
方式一:调用execute()方法
方式二:调用submit()方法
submit()和execute()两类方法的区别
(1)二者所接收的参数不一样
Execute()方法只能接收Runnable类型的参数,而submit()方法可以接收Callable、Runnable两种类型的参数。Callable类型的任务是可以返回执行结果的,而Runnable类型的任务不可以返回执行结果。Callable是JDK 1.5加入的执行目标接口,作为Runnable的一种补充,允许有返回值,允许抛出异常。Runnable和Callable的主要区别为:Callable允许有返回值,Runnable不允许有返回值;Runnable不允许抛出异常,Callable允许抛出异常。
(2)submit()提交任务后会有返回值,而execute()没有
execute()方法主要用于启动任务的执行,而任务的执行结果和可能的异常调用者并不关心。submit()方法也用于启动任务的执行,但是启动之后会返回Future对象,代表一个异步执行实例,可以通过该异步执行实例去获取结果。
(3)submit()方便Exception处理
execute()方法在启动任务执行后,任务执行过程中可能发生的异常调用者并不关心。而通过submit()方法返回的Future对象(异步执行实例),可以进行异步执行过程中的异常捕获。
1.通过submit()返回的Future对象获取结果
2.通过submit()返回的Future对象捕获异常
1.6.5线程池的任务调度流程
(1)如果当前工作线程数量小于核心线程数量,执行器总是优先创建一个任务线程,而不是从线程队列中获取一个空闲线程。
(2)如果线程池中总的任务数量大于核心线程池数量,新接收的任务将被加入阻塞队列中,一直到阻塞队列已满。在核心线程池数量已经用完、阻塞队列没有满的场景下,线程池不会为新任务创建一个新线程。
(3)当完成一个任务的执行时,执行器总是优先从阻塞队列中获取下一个任务,并开始执行,一直到阻塞队列为空,其中所有的缓存任务被取光。
(4)在核心线程池数量已经用完、阻塞队列也已经满了的场景下,如果线程池接收到新的任务,将会为新任务创建一个线程(非核心线程),并且立即开始执行新任务。
(5)在核心线程都用完、阻塞队列已满的情况下,一直会创建新线程去执行新任务,直到池内的线程总数超出maximumPoolSize。如果线程池的线程总数超过maximumPoolSize,线程池就会拒绝接收任务,当新任务过来时,会为新任务执行拒绝策略。
1.6.6 ThreadFactory (线程工厂)
ThreadFactory是Java线程工厂接口,使用Executors创建新的线程池时,也可以基于ThreadFactory(线程工厂)创建,在创建新线程池时可以指定将要使用的ThreadFactory实例。只不过,如果没有指定的话,就会使用Executors.defaultThreadFactory默认实例。使用默认的线程工厂实例所创建的线程全部位于同一个ThreadGroup(线程组)中,具有相同的NORM_PRIORITY(优先级为5),而且都是非守护进程状态。
Executors为线程池工厂类,用于快捷创建线程池(Thread Pool);ThreadFactory为线程工厂类,用于创建线程(Thread)。
1.6.7任务阻塞队列
(1)ArrayBlockingQueue:是一个数组实现的有界阻塞队列(有界队列),队列中的元素按FIFO排序。ArrayBlockingQueue在创建时必须设置大小,接收的任务超出corePoolSize数量时,任务被缓存到该阻塞队列中,任务缓存的数量只能为创建时设置的大小,若该阻塞队列已满,则会为新的任务创建线程,直到线程池中的线程总数大于maximumPoolSize。
(2)LinkedBlockingQueue:是一个基于链表实现的阻塞队列,按FIFO排序任务,可以设置容量(有界队列),不设置容量则默认使用Integer.Max_VALUE作为容量(无界队列)。该队列的吞吐量高于ArrayBlockingQueue。如果不设置LinkedBlockingQueue的容量(无界队列),当接收的任务数量超出corePoolSize时,则新任务可以被无限制地缓存到该阻塞队列中,直到资源耗尽。有两个快捷创建线程池的工厂方法Executors.newSingleThreadExecutor和Executors.newFixedThreadPool使用了这个队列,并且都没有设置容量(无界队列)。
(3)PriorityBlockingQueue:是具有优先级的无界队列。
(4)DelayQueue:这是一个无界阻塞延迟队列,底层基于PriorityBlockingQueue实现,队列中每个元素都有过期时间,当从队列获取元素(元素出队)时,只有已经过期的元素才会出队,队列头部的元素是过期最快的元素。快捷工厂方法Executors.newScheduledThreadPool所创建的线程池使用此队列。
(5)SynchronousQueue:(同步队列)是一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程的调用移除操作,否则插入操作一直处于阻塞状态,其吞吐量通常高于LinkedBlockingQueue。快捷工厂方法Executors.newCachedThreadPool所创建的线程池使用此队列。与前面的队列相比,这个队列比较特殊,它不会保存提交的任务,而是直接新建一个线程来执行新来的任务。
1.6.8调度器的钩子方法
(1)beforeExecute:
异步任务执行之前的钩子方法线程池工作线程在异步执行目标实例(如Runnable实例)前调用此钩子方法。此方法仍然由执行任务的工作线程调用。默认实现不执行任何操作,但可以在子类中对其进行自定义。此方法由执行目标实例的工作线程调用,可用于重新初始化ThreadLocal线程本地变量实例、更新日志记录、开始计时统计、更新上下文变量等。
(2)afterExecute:
异步任务执行之后的钩子方法线程池工作线程在异步执行目标实例后调用此钩子方法。此方法仍然由执行任务的工作线程调用。此钩子方法的默认实现不执行任何操作,可以在调度器子类中对其进行自定义。此方法由执行目标实例的工作线程调用,可用于清除ThreadLocal线程本地变量、更新日志记录、收集统计信息、更新上下文变量等。
(3)terminated:
线程池终止时的钩子方法terminated钩子方法在Executor终止时调用,默认实现不执行任何操作。
说明
beforeExecute和afterExecute两个方法在每个任务执行前后被调用,如果钩子(回调方法)引发异常,内部工作线程可能失败并突然终止。
1.6.9线程池的拒绝策略
任务被拒绝有两种情况
(1)线程池已经被关闭。
(2)工作队列已满且maximumPoolSize已满。
RejectedExecutionHandler是拒绝策略的接口
(1)AbortPolicy:拒绝策略。
使用该策略时,如果线程池队列满了,新任务就会被拒绝,并且抛出RejectedExecutionException异常。该策略是线程池默认的拒绝策略。
(2)DiscardPolicy:抛弃策略。
该策略是AbortPolicy的Silent(安静)版本,如果线程池队列满了,新任务就会直接被丢掉,并且不会有任何异常抛出。
(3)DiscardOldestPolicy:抛弃最老任务策略。
抛弃最老任务策略,也就是说如果队列满了,就会将最早进入队列的任务抛弃,从队列中腾出空间,再尝试加入队列。因为队列是队尾进队头出,队头元素是最老的,所以每次都是移除队头元素后再尝试入队。
(4)CallerRunsPolicy:调用者执行策略。
调用者执行策略。在新任务被添加到线程池时,如果添加失败,那么提交任务线程会自己去执行该任务,不会使用线程池中的线程去执行新任务。
(5)自定义策略。
如果以上拒绝策略都不符合需求,那么可自定义一个拒绝策略,实现RejectedExecutionHandler接口的rejectedExecution方法即可。
1.6.10线程池的优雅关闭
1. 线程池的5种状态
(1)RUNNING:线程池创建之后的初始状态,这种状态下可以执行任务。
(2)SHUTDOWN:该状态下线程池不再接受新任务,但是会将工作队列中的任务执行完毕。
(3)STOP:该状态下线程池不再接受新任务,也不会处理工作队列中的剩余任务,并且将会中断所有工作线程。
(4)TIDYING:该状态下所有任务都已终止或者处理完成,将会执行terminated()钩子方法。
(5)TERMINATED:执行完terminated()钩子方法之后的状态。
2. 线程池的状态转换规则
(1)线程池创建之后状态为RUNNING。
(2)执行线程池的shutdown()实例方法,会使线程池状态从RUNNING转变为SHUTDOWN。
(3)执行线程池的shutdownNow()实例方法,会使线程池状态从RUNNING转变为STOP。
(4)当线程池处于SHUTDOWN状态时,执行其shutdownNow()方法会将其状态转变为STOP。
(5)等待线程池的所有工作线程停止,工作队列清空之后,线程池状态会从STOP转变为TIDYING。
(6)执行完terminated()钩子方法之后,线程池状态从TIDYING转变为TERMINATED。
3. 优雅地关闭线程池主要涉及的方法有3个
(1)shutdown:是JUC提供的一个有序关闭线程池的方法,此方法会等待当前工作队列中的剩余任务全部执行完成之后,才会执行关闭,但是此方法被调用之后线程池的状态转为SHUTDOWN,线程池不会再接收新的任务。
Shutdown()方法首先加锁,其次检查调用者是否用于执行线程池关闭的Java Security权限。接着shutdown()方法会将线程池状态变为SHUTDOWN,在这之后线程池不再接受提交的新任务。此时如果还继续往线程池提交任务,将会使用线程池拒绝策略响应,默认的拒绝策略将会使用ThreadPoolExecutor.AbortPolicy,接收新任务时会抛出RejectedExecutionException异常。
(2)shutdownNow:是JUC提供的一个立即关闭线程池的方法,此方法会打断正在执行的工作线程,并且会清空当前工作队列中的剩余任务,返回的是尚未执行的任务。
shutdownNow()方法将会把线程池状态设置为STOP,然后中断所有线程(包括工作线程以及空闲线程),最后清空工作队列,取出工作队列所有未完成的任务返回给调用者。与有序的shutdown()方法相比,shutdownNow()方法比较粗暴,直接中断工作线程。不过这里需要注意的是,中断线程并不代表线程立刻结束,只是通过工作线程的interrupt()实例方法设置了中断状态,这里需要用户程序主动配合线程进行中断操作。
(3)awaitTermination:等待线程池完成关闭。在调用线程池的shutdown()与shutdownNow()方法时,当前线程会立即返回,不会一直等待直到线程池完成关闭。如果需要等到线程池关闭完成,可以调用awaitTermination()方法。
调用了线程池的shutdown()与shutdownNow()方法之后,用户程序都不会主动等待线程池关闭完成,如果需要等待线程池关闭完成,需要调用awaitTermination()进行主动等待。
4.优雅地关闭线程池
(1)执行shutdown()方法,拒绝新任务的提交,并等待所有任务有序地执行完毕。
(2)执行awaitTermination(long timeout,TimeUnit unit)方法,指定超时时间,判断是否已经关闭所有任务,线程池关闭完成。
(3)如果awaitTermination()方法返回false,或者被中断,就调用shutDownNow()方法立即关闭线程池所有任务。
(4)补充执行awaitTermination(long timeout,TimeUnit unit)方法,判断线程池是否关闭完成。如果超时,就可以进入循环关闭,循环一定的次数(如1000次),不断关闭线程池,直到其关闭或者循环结束。
5.注册JVM钩子函数自动关闭线程池
1.6.11 Executors快捷创建线程池的潜在问题
(1)FixedThreadPool和SingleThreadPool这两个工厂方法所创建的线程池,工作队列(任务排队的队列)的长度都为Integer.MAX_VALUE,可能会堆积大量的任务,从而导致OOM(即耗尽内存资源)。
(2)CachedThreadPool和ScheduledThreadPool这两个工厂方法所创建的线程池允许创建的线程数量为Integer.MAX_VALUE,可能会导致创建大量的线程,从而导致OOM。
1.7确定线程池的线程数
1.7.1按照任务类型对线程池进行分类
(1)IO密集型任务
此类任务主要是执行IO操作。由于执行IO操作的时间较长,导致CPU的利用率不高,这类任务CPU常处于空闲状态。Netty的IO读写操作为此类任务的典型例子。
(2)CPU密集型任务
此类任务主要是执行计算任务。由于响应时间很快,CPU一直在运行,这种任务CPU的利用率很高。
(3)混合型任务
此类任务既要执行逻辑计算,又要进行IO操作(如RPC调用、数据库访问)。相对来说,由于执行IO操作的耗时较长(一次网络往返往往在数百毫秒级别),这类任务的CPU利用率也不是太高。Web服务器的HTTP请求处理操作为此类任务的典型例子。
1.7.2 为IO密集型任务确定线程数
1.7.3为CPU密集型任务确定线程数
1.7.4为混合型任务确定线程数
1.8 ThreadLoca原理与实战
1.8.1 ThreadLocal基本使用
1.8.2 ThreadL _ocal使用场景
1.8.3使用ThreadL ocal进行线程隔离
1.8.4使用ThreadL ocal进行跨函数数据传递
1.8.5 ThreadL ocal内部结构演进
1.8.6 ThreadL ocal源码分析
1.8.7 ThreadL ocalMap源码分析
1.8.8 ThreadL ocal综合使用案例
第2章 Java内置锁的核心原理
2.1线程安全问题
2.1.1 自增运算不是线程安全的
1.线程安全小实验
2.原因分析:自增运算符不是线程安全的
2.1.2临界区资源与临界区代码段
临界区资源表示一种可以被多个线程使用的公共资源或共享数据,但是每一次只能有一个线程使用它。一旦临界区资源被占用,想使用该资源的其他线程则必须等待。
竞态条件(Race Conditions)可能是由于在访问临界区代码段时没有互斥地访问而导致的特殊情况。如果多个线程在临界区代码段的并发执行结果可能因为代码的执行顺序不同而不同,我们就说这时在临界区出现了竞态条件问题。
2.2 synchronized关键字
2.2.1 synchronized同步方法
2.2.2 synchronized同步块
2.2.3静态的同步方法
Java有两种对象:Object实例对象和Class对象
这里将Object对象的监视锁叫作对象锁,将Class对象的监视锁叫作类锁。当synchronized关键字修饰static方法时,同步锁为类锁;当synchronized关键字修饰普通的成员方法(非静态方法)时,同步锁为类锁。由于类的对象实例可以有很多,但是每个类只有一个Class实例,因此使用类锁作为synchronized的同步锁时会造成同一个JVM内的所有线程只能互斥地进入临界区段。
通过synchronized关键字所抢占的同步锁什么时候释放呢?一种场景是synchronized块(代码块或者方法)正确执行完毕,监视锁自动释放;另一种场景是程序出现异常,非正常退出synchronized块,监视锁也会自动释放。所以,使用synchronized块时不必担心监视锁的释放问题
2.3生产者-消费者问题
2.3.1生产者-消费者模式
(1)生产者与生产者之间、消费者与消费者之间,对数据缓冲区的操作是并发进行的。
(2)数据缓冲区是有容量上限的。数据缓冲区满后,生产者不能再加入数据;数据缓冲区空时,消费者不能再取出数据。
(3)数据缓冲区是线程安全的。在并发操作数据缓冲区的过程中,不能出现数据不一致的情况;或者在多个线程并发更改共享数据后,不会造成出现脏数据的情况。
(4)生产者或者消费者线程在空闲时需要尽可能阻塞而不是执行无效的空操作,尽量节约CPU资源。
2.3.2一个线程不安全的实现版本
1.不是线程安全的数据缓冲区类
2.生产者、消费者的逻辑与动作解耦
3.通用的Producer类实现
4.通用的Consumer类实现
5.数据区缓冲区实例、生产动作、消费动作的定义
6.组装出一个生产者和消费者模式的简单实现版本
2.3.3一个线程安全的实现版本
2.4 Java对象结构与内置锁
2.4.1 Java对象结构
2.4.2 Mark Word的结构信息
Java内置锁的状态总共有4种,级别由低到高依次为:无锁、偏向锁、轻量级锁和重量级锁。其实在JDK 1.6之前,Java内置锁还是一个重量级锁,是一个效率比较低下的锁,在JDK 1.6之后,JVM为了提高锁的获取与释放效率,对synchronized的实现进行了优化,引入了偏向锁和轻量级锁,从此以后Java内置锁的状态就有了4种(无锁、偏向锁、轻量级锁和重量级锁),并且4种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级,也就是说只能进行锁升级(从低级别到高级别)
2.4.3使用JOLI具查看对象的布局
2.4.4大小端问题
(1)大端模式是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中。大端存放模式有点类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放。(2)小端模式是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,此模式和日常的数字计算在方向上是一致的。
2.4.5无锁、偏向锁、轻量级锁和重量级锁
2.5偏向锁的原理与实战
2.5.1偏向锁的核心原理
2.5.2偏向锁的演示案例
2.5.3偏向锁的膨胀和撤销
2.6轻量级锁的原理与实战
2.6.1轻量级锁的核心原理
1. 轻量锁存在的目的是尽可能不动用操作系统层面的互斥锁
2. 轻量级锁是一种自旋锁,通过自旋解决线程同步问题
3. 轻量级锁的执行过程
2.6.2轻量级锁的演示案例
1.轻量级锁的演示案例
2.演示案例的运行结果说明
2.6.3轻量级锁的分类
1.普通自旋锁
当有线程来竞争锁时,抢锁线程会在原地循环等待,而不是被阻塞,直到那个占有锁的线程释放锁之后,这个抢锁线程才可以获得锁
默认情况下,自旋的次数为10次,用户可以通过-XX:PreBlockSpin选项来进行更改
2.自适应自旋锁
就是等待线程空循环的自旋次数并非是固定的,而是会动态地根据实际情况来改变自旋等待的次数,自旋次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
原理
(1)如果抢锁线程在同一个锁对象上之前成功获得过锁,JVM就会认为这次自旋很有可能再次成功,因此允许自旋等待持续相对更长的时间。
(2)如果对于某个锁,抢锁线程很少成功获得过,那么JVM将可能减少自旋时间甚至省略自旋过程,以避免浪费处理器资源。
JDK 1.6的轻量级锁使用的是普通自旋锁,且需要使用-XX:+UseSpinning选项手工开启。JDK 1.7后,轻量级锁使用自适应自旋锁,JVM启动时自动开启,且自旋时间由JVM自动控制
2.6.4轻量级锁的膨胀
临界区代码执行耗时较长,在其执行期间,其他线程都在原地自旋等待,会空消耗CPU。所以,在争用激烈的场景下,轻量级锁会膨胀为基于操作系统内核互斥锁实现的重量级锁
2.7重量级锁的原理与实战
2.7.1重量级锁的核心原理
1. JVM中每个对象都会有一个监视器,监视器和对象一起创建、销毁。
2. 监视器的特点
(1)同步。监视器所保护的临界区代码是互斥地执行的。一个监视器是一个运行许可,任一线程进入临界区代码都需要获得这个许可,离开时把许可归还。
(2)协作。监视器提供Signal机制,允许正持有许可的线程暂时放弃许可进入阻塞等待状态,等待其他线程发送Signal去唤醒;其他拥有许可的线程可以发送Signal,唤醒正在阻塞等待的线程,让它可以重新获得许可并启动执行。
3. ObjectMonitor的内部抢锁过程
1.Cxq
2.EntryList
3.OnDeck Thread与Owner Thread
4.WaitSet
2.7.2重量级锁的开销
1. Linux系统的体系架构分为用户态(或者用户空间)和内核态(或者内核空间)
2. Linux系统的内核是一组特殊的软件程序,负责控制计算机的硬件资源
3. 用户态与内核态有各自专用的内存空间、专用的寄存器等,进程从用户态切换至内核态需要传递许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
4. 进程从用户态到内核态切换主要包括以下三种方式
(1)硬件中断。硬件中断也称为外设中断,当外设完成用户的请求时会向CPU发送中断信号。
(2)系统调用。其实系统调用本身就是中断,只不过是软件中断,跟硬件中断不同。
(3)异常。如果当前进程运行在用户态,这个时候发生了异常事件(例如缺页异常),就会触发切换。
5. 可以通过内核态所提供的访问接口实现,这些接口就叫系统调用
6. JVM重量级锁使用了Linux内核态下的互斥锁,这是重量级锁开销很大的原因
2.7.3重量级锁的演示案例
1.重量级锁的演示案例
2.重量级锁演示案例的运行结果说明
2.8偏向锁、轻量级锁与重量级锁的对比
1. synchronized的执行过程
(1)线程抢锁时,JVM首先检测内置锁对象Mark Word中的biased_lock(偏向锁标识)是否设置成1,lock(锁标志位)是否为01,如果都满足,确认内置锁对象为可偏向状态。
(2)在内置锁对象确认为可偏向状态之后,JVM检查Mark Word中的线程ID是否为抢锁线程ID,如果是,就表示抢锁线程处于偏向锁状态,抢锁线程快速获得锁,开始执行临界区代码。
(3)如果Mark Word中的线程ID并未指向抢锁线程,就通过CAS操作竞争锁。如果竞争成功,就将Mark Word中的线程ID设置为抢锁线程,偏向标志位设置为1,锁标志位设置为01,然后执行临界区代码,此时内置锁对象处于偏向锁状态。
(4)如果CAS操作竞争失败,就说明发生了竞争,撤销偏向锁,进而升级为轻量级锁。
(5)JVM使用CAS将锁对象的Mark Word替换为抢锁线程的锁记录指针,如果成功,抢锁线程就获得锁。如果替换失败,就表示其他线程竞争锁,JVM尝试使用CAS自旋替换抢锁线程的锁记录指针,如果自旋成功(抢锁成功),那么锁对象依然处于轻量级锁状态。
(6)如果JVM的CAS替换锁记录指针自旋失败,轻量级锁就膨胀为重量级锁,后面等待锁的线程也要进入阻塞状态。
2. 三种内置锁的对比
2.9线程间通信
2.9.1线程间通信的定义
1. 当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以避免无效的资源争夺
2. 线程间通信的方式
等待-通知
共享内存
管道流
2.9.2低效的线程轮询
消费者每一轮消费,无论数据区是否为空,都需要进行数据区的询问和判断
2.9.3 wait方法和notify方法的原理
1.对象的wait()方法
2.wait()方法的核心原理
(1)当线程调用了locko(某个同步锁对象)的wait()方法后,JVM会将当前线程加入locko监视器的WaitSet(等待集),等待被其他线程唤醒。
(2)当前线程会释放locko对象监视器的Owner权利,让其他线程可以抢夺locko对象的监视器。
(3)让当前线程等待,其状态变成WAITING。
3.对象的notify()方法
4.notify()方法的核心原理
(1)当线程调用了locko(某个同步锁对象)的notify()方法后,JVM会唤醒locko监视器WaitSet中的第一条等待线程。
(2)当线程调用了locko的notifyAll()方法后,JVM会唤醒locko监视器WaitSet中的所有等待线程。
(3)等待线程被唤醒后,会从监视器的WaitSet移动到EntryList,线程具备了排队抢夺监视器Owner权利的资格,其状态从WAITING变成BLOCKED。
(4)EntryList中的线程抢夺到监视器的Owner权利之后,线程的状态从BLOCKED变成Runnable,具备重新执行的资格。
2.9.4“等待通知’ 通信模式演示案例
一个线程A调用了同步对象的wait()方法进入等待状态,而另一线程B调用了同步对象的notify()或者notifyAll()方法通知等待线程,当线程A收到通知后,重新进入就绪状态,准备开始执行
线程间的通信需要借助同步对象(Object)的监视器来完成,Object对象的wait()、notify()方法就如开关信号,用于完成等待方和通知方之间的通信。
2.9.5生产者-消费者之间的线程间通信
使用“等待-通知”机制通信的生产者-消费者实现版本
(1)LOCK_OBJECT:用于临界区同步,临界区资源为数据缓冲区的dataList变量和amount变量。
(2)NOT_FULL:用于数据缓冲区的未满条件等待和通知。
(3)NOT_EMPTY:用于数据缓冲区的非空条件等待和通知。
2.9.6需要在synchronized同步块的内部使用wait和notify
notify()方法的原理:JVM从对象锁的监视器的WaitSet队列移动一个线程到其EntryList队列,这些操作都与对象锁的监视器有关。
调用wait()和notify()系列方法进行线程通信的要点
(1)调用某个同步对象locko的wait()和notify()类型方法前,必须要取得这个锁对象的监视锁
(2)调用wait()方法时使用while进行条件判断
第3章 CAS原理与JUC原子类
3.1 什么是CAS
3.1.1 Unsafe类中的CAS方法
3.1.2使用CAS进行无锁编程
3.1.3使用无锁编程实现轻量级安全自增
3.1.4字段偏移量的计算
3.2 JUC原子类
3.2.1 JUC中的Atomic原子操作包
3.2.2基础原子类AtomicInteger
3.2.3数组原子类AtomicIntegerArray
3.2.4 AtomicInteger线程安全原理,
3.3对象操作的原子性
3.3.1引用类型原子类
3.3.2属性更新原子类
3.4 ABA问题
3.4.1了解ABA问题
3.4.2 ABA问题解决方案
3.4.3使用AtomicStampedReference解决ABA问题
3.4.4使用AtomicMarkableReference解决ABA问题
3.5提升高并发场景下CAS操作的性能
3.5.1以空间换时间: LongAdder
3.5.2 LongAdder的原理
3.6 CAS在JDK中的广泛应用
3.6.1 CAS操作的弊端和规避措施
3.6.2 CAS操作在JDK中的应用
第4章 可见性与有序性的原理
4.1 CPU物理缓存结构
4.2并发编程的三大问题
4.2.1原子性问题
4.2.2可见性问题
4.2.3有序性问题
4.3硬件层的MESI协议原理
4.3.1总线锁和缓存锁
4.3.2 MSI协议
4.3.3 MES|协议及RFO请求
4.3.4 volatile的原理
4.4有序性与内存屏障
4.4.1重排序
4.4.2 As-if- -Serial规则
4.4.3硬件层面的内存屏障
4.5 JMM详解
4.5.1什么是Java内存模型
4.5.2 JMM与JVM物理内存的区别
4.5.3 JMM的8个操作
4.5.4 JMM如何解决有序性问题
4.5.5 volatile语义中的内存屏障
4.6 Happens- Before规则
4.6.1 Happens- -Before规则介绍
4.6.2规则1:顺序性规则
4.6.3规则2: volatile规则
4.6.4规则3:传递性规则
4.6.5规则4:监视锁规则.
4.6.6规则5: start()规则
4.6.7规则6: join()规则
4.7 volatile不具备原子性
4.7.1 volatile变量的自增实例
4.7.2 volatile变量的复合操作不具备原子性的原理
第5章 JUC显式锁的原理与实战
5.1显式锁
5.1.1显式锁L _ock接口
5.1.2可重入锁ReentrantL ock
5.1.3使用显式锁的模板代码
1.使用lock()方法抢锁的模板代码
2. 注意事项
(1)释放锁操作lock.unlock()必须在try-catch结构的finally块中执行,否则,如果临界区代码抛出异常,锁就有可能永远得不到释放。
(2)抢占锁操作lock.lock()必须在try语句块之外,而不是放在try语句块之内。为什么呢?原因之一是lock()方法没有申明抛出异常,所以可以不包含到try块中;原因之二是lock()方法并不一定能够抢占锁成功,如果没有抢占成功,当然也就不需要释放锁,而且在没有占有锁的情况下去释放锁,可能会导致运行时异常。
(3)在抢占锁操作lock.lock()和try语句之间不要插入任何代码,避免抛出异常而导致释放锁操作lock.unlock()执行不到,导致锁无法被释放。
2.调用tryLock()方法非阻塞抢锁的模板代码
5.1.4基于显式锁进行“等待通知”方式的线程间通信
5.1.5 L ockSupport
5.1.6显式锁的分类
5.2悲观锁和乐观锁
5.2.1悲观锁存在的问题
5.2.2通过CAS实现乐观锁
5.2.3不可重入的自旋锁
5.2.4可重入的自旋锁.
5.2.5 CAS可能导致“总线风暴’
5.2.6 CLH自旋锁
5.3公平锁与非公平锁
5.3.1非公平锁实战
5.3.2公平锁实战
5.4可中断锁与不可中断锁
5.4.1锁的可中断抢占
5.4.2死锁的监测与中断
5.5共享锁与独占锁
5.5.1独占锁
5.5.2共享锁Semaphore
5.5.3共享锁CountDownL .atch
5.6读写锁
5.6.1读写锁ReentrantReadWriteL ock
5.6.2锁的升级与降级
5.6.3 StampedL ock
第6章 AQS抽象同步器的核心原理
6.1锁与队列的关系
1.CLH锁的内部队列
2.分布式锁的内部队列
3.AQS的内部队列
6.2 AQS的核心成员
6.2.1状态标志位
6.2.2队列节点类
6.2.3 FIFO双向同步队列
6.2.4 JUC显式锁与AQS的关系
6.2.5 ReentrantL ock与AQS的组合关系
6.3 AQS中的模板模式
6.3.1模板模式
1.模板模式的定义
2.模板方法和钩子方法
6.3.2一个模板模式的参考实现
1.模板模式的参考实现代码
2.模板模式的优点
6.3.3 AQS的模板流程,
6.3.4 AQS中的钩子方法
1.tryAcquire独占式获取锁
2.tryRelease独占式释放锁
3.tryAcquireShared共享式获取
4.tryReleaseShared共享式释放
5.查询是否处于独占模式
6.4通过AQS实现一把简单的独占锁
6.4.1简单的独占锁的UML类图
6.4.2简单的独占锁的实现
6.4.3 SimpleMockL ock测试用例
6.5 AQS锁抢占的原理
6.5.1显式锁抢占的总体流程
6.5.2 AQS模板方法: acquire(arg)
6.5.3钩子实现: tryAcquire(arg)
6.5.4直接入队: addWaiter
6.5.5 自旋入队: enq
6.5.6自旋抢占: acquireQueued()
6.5.7挂起预判: shouldParkAfterFailedAcquire()
6.5.8线程挂起: parkAndCheckInterrupt()
6.6 AQS的两个关键点:节点的入队和出队
6.6.1节点的自旋入队
6.6.2节点的出队
6.7 AQS锁释放的原理
6.7.1 SimpleMockL _ock独占锁的释放流程
6.7.2 AQS模板方法: release()
6.7.3钩子实现: tryRelease()
6.7.4唤醒后继: unparkSuccessor()
6.8 ReentrantL ock的抢锁流程
6.8.1 ReentrantL ock非公平锁的抢占流程
6.8.2非公平锁的同步器子类
6.8.3非公平抢占的钩子方法: tryAcquire(arg)
6.8.4 ReentrantL ock公平锁的抢占流程
6.8.5公平锁的同步器子类
6.8.6公平抢占的钩子方法: tryAcquire(arg)
6.8.7是否有后继节点的判断
6.9 AQS条件队列
6.9.1 Condition基本原理
6.9.2 await()等待方法原理
6.9.3 signal()唤醒方法原理,
6.10 AQS的实际应用
第7章 JUC容器类
7.1线程安全的同步容器类
1.通过synchronizedSortedSet静态方法包装出一个同步容器
2.java.util.Collections所提供的同步包装方法
3.同步容器面临的问题
7.2 JUC高并发容器
1.什么是高并发容器
2.List
JUC包中的高并发List主要有CopyOnWriteArrayList,对应的基础容器为ArrayList。
3.Set
JUC包中的Set主要有CopyOnWriteArraySet和ConcurrentSkipListSet。
4.Map
JUC包中Map主要有ConcurrentHashMap和ConcurrentSkipListMap。
5.Queue
JUC包中的Queue的实现类包括三类:单向队列、双向队列和阻塞队列。
7.3 CopyOnWriteArrayList
7.3.1 CopyOnWriteArrayList的使用
7.3.2 CopyOnWriteArrayList的原理
CopyOnWrite(写时复制)就是在修改器对一块内存进行修改时,不直接在原有内存块上进行写操作,而是将内存复制一份,在新的内存中进行写操作,写完之后,再将原来的指针(或者引用)指向新的内存,原来的内存被回收
7.3.3 CopyOnWriteArrayL ist读取操作
7.3.4 CopyOnWriteArrayList写入操作
7.3.5 CopyOnWriteArrayList的迭代器实现
7.4 BlockingQueue
7.4.1 BlockingQueue的特点
7.4.2阻塞队列的常用方法
7.4.3常见的BlockingQueue
1.ArrayBlockingQueue
2.LinkedBlockingQueue
3.DelayQueue
4.PriorityBlockingQueue
5.SynchronousQueue
7.4.4 ArrayBlockingQueue的基本使用
7. 4.5 ArrayBlockingQueue构造器和成员
7.4.6非阻塞式添加元素: add()、 offer()方 法的原理
7.4.7阻塞式添加元素: put()方 法的原理
7.4.8非阻塞式删除元素: pol()方法的原理
7.4.9阻塞式删除元素: take()方 法的原理
7.4.10 peek()直接返回当前队列的头元素
7.5 ConcurrentHashMap
7.5.1 HashMap和HashTable的问题
基础容器HashMap是线程不安全的,在多线程环境下,使用HashMap进行put操作时,可能会引起死循环,导致CPU利用率飙升,甚至接近100%,所以在高并发情况下是不能使用HashMap的
(1)HashTable不允许key和value为null。(2)HashTable使用synchronized来保证线程安全,包含get()/put()在内的所有相关需要进行同步执行的方法都加上了synchronized关键字,对这个Hash表进行锁定
7.5.2 JDK 1.7版本ConcurrentHashMap的结构
7.5.3 JDK 1.7版本ConcurrentHashMap的核心原理
7.5.4 JDK 1.8版本ConcurrentHashMap的结构
7.5.5 JDK 1.8版本ConcurrentHashMap的核心原理
7.5.6 JDK 1.8版本ConcurrentHashMap的核心源码
第8章 高并发设计模式
8.1线程安全的单例模式
8.1.1从饿汉式单例到懒汉式单例
8.1.2使用内置锁保护懒汉式单例
8.1.3双重检查锁单例模式
8.1.4使用双重检查锁+volatile
8.1.5使用静态内部类实现懒汉式单例模式
8.2 Master-Worker模式
8.2.3 Nginx中Master- Worker模 式的实现
8.2.2 Netty中Master- -Worker模式的实现
8.2.1 Master-Worker模式的参考实现
8.3 ForkJoin模式
8.3.1 ForkJoin模式的原理
8.3.2 ForkJoin框架
8.3.3 ForkJoin框架使用实战
8.3.4 ForkJoin框架的核心API
8.3.5工作窃取算法
8.3.6 ForkJoin框 架的原理
8.4生产者-消费者模式
8.5 Future模式
第9章 高并发核心模式之异步回调模式
9.1从泡茶的案例讲起
9.2 join:异步阻塞之闷葫芦
9.2.1线程的合并流程
9.2.2调用join()实现异步泡茶喝
9.2.3 join()方法详解;
9.3 FutureTask:异步调用之重武器
9.3.1通过Future Task获取异步执行结果的步骤
(1)创建一个Callable接口的实现类,并实现它的call()方法,编写好异步执行的具体逻辑,并且可以有返回值。
(2)使用Callable实现类的实例构造一个FutureTask实例。
(3)使用FutureTask实例作为Thread构造器的target入参,构造新的Thread线程实例。
(4)调用Thread实例的start()方法启动新线程,启动新线程的run()方法并发执行。其内部的执行过程为:启动Thread实例的run()方法并发执行后,会执行FutureTask实例的run()方法,最终会并发执行Callable实现类的call()方法。
(5)调用FutureTask对象的get()方法阻塞性地获得并发线程的执行结果。
9.3.2使用FutureTask实现异步泡茶喝
9.4异步回调与主动调用
9.5 Guava的异步回调模式
9.5.1详解FutureCallback
9.5.2详解L istenableFuture
9.5.3 ListenableFuture异步任务
9.5.4使用Guava实现泡茶喝的实例
9.5.5 Guava异步回调和Java异步调用的区别
(1)FutureTask是主动调用的模式,“调用线程”主动获得异步结果,在获取异步结果时处于阻塞状态,并且会一直阻塞,直到拿到异步线程的结果。
(2)Guava是异步回调模式,“调用线程”不会主动获得异步结果,而是准备好回调函数,并设置好回调钩子,执行回调函数的并不是“调用线程”自身,回调函数的执行者是“被调用线程”,“调用线程”在执行完自己的业务逻辑后就已经结束了,当回调函数被执行时,“调用线程”可能已经结束很久了。
9.6 Netty的异步回调模式
9.6.1 GenericFuturel istener接口详解
9.6.2 Netty的Future接口详解
9.6.3 ChannelFuture的使用
在Netty网络编程中,网络连接通道的输入、输出处理都是异步进行的,都会返回一个ChannelFuture接口的实例。通过返回的异步任务实例可以为其增加异步回调的监听器。在异步任务真正完成后,回调执行。
9.6.4 Netty的出站和入站异步回调
9.7异步回调模式小结
第10章 CompletableFuture异步回调
10.1 CompletableFuture详解
10.1.1 CompletableFuture的UML类关系
10.1.2 CompletionStage接口
10.1.3使用runAsync和supplyAsync创建子任务
CompletionStage子任务的创建是通过CompletableFuture完成的。CompletableFuture类提供了非常强大的Future的扩展功能来帮助我们简化异步编程的复杂性,提供了函数式编程的能力来帮助我们通过回调的方式处理计算结果,也提供了转换和组合CompletionStage()的方法。
10.1.4设置子任务回调钩子
10.1.5调用handle()方法统- -处理异常和结果
10.1.6线程池的使用
10.2异步任务的串行执行
10.2.1 thenApply()方法
10.2.2 thenRun()方法
10.2.3 thenAccept()方法
10.2. 4 thenCompose()方法
10.2.5 4个任务串行方法的区别
10.3异步任务的合并执行
10.3.1 thenCombine()方法
10.3.2 runAfterBoth()方法
10.3.3 thenAcceptBoth()方法
10.3.4 allOf()等待所有的任务结束
10.4异步任务的选择执行
10.4.1 applyToEither()方法
10.4.2 runAfterEither()方法
10.4.3 acceptEither()方法
10.5 CompletableFuture的综合案例
10.5.1 1EFCompletableFuture实现泡茶喝实例
10.5.2 15 FCompletableFuture进行多个RPC调用
10.5.3使用RxJava模拟RPC异步回调
0 条评论
下一页