多线程
2020-03-05 11:37:02 4 举报
AI智能生成
Java多线程知识点大全
作者其他创作
大纲/内容
java的多线程并发问题最终都会反映在JVM内存模型上,解决原子性、可见性、有序性3大问题
主内存主要包括本地方法区和堆。每个线程都有一个工作内存,工作内存中主要包括两个部分,一个是属于该线程私有的栈和对主存部分变量拷贝的寄存器(包括程序计数器PC和cup工作的高速缓存区)
Java内存模型规定所有变量都存储在主内存中,每条线程的工作内存使用到变量到主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量
所有的变量都存储在主内存中(虚拟机内存的一部分),对于所有线程都是共享的
线程之间无法直接访问对方的工作内存中的变量,线程间变量的传递均需要通过主内存来完成
lock、unlock、read、load、use、assign、store、write
JVM定义了线程对主存的操作原语
工作内存&主内存
串行语义(单线程)具有天生的可见性
根据可见性的原则,主内存的值可以被其他线程可见
锁释放之前,会将对变量的修改刷新到主内存,保证一个操作内一组原子操作的可见性
将当前内核中线程工作内存中该共享变量刷新到主存
通知其他内核里缓存的该共享变量内存地址无效
volatile保证了修饰的共享变量,当CPU发现这个指令时
可见性
一般情况下,编译器和CPU为了提升程序执行的效率,会按照一定的规则允许进行指令优化
JVM的\"happens-before\"原则保证编译器进行优化时指令重排不会破坏原有的语义结构
它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个规则,我们可以通过几条规则一揽子地解决并发环境下两个操作之间是否可能存在冲突的所有问题。
happens-before先天原则
代码编译后成二进制代码被加载到内存后按原语义有一套初始串行化指令
一个指令需要多个汇编步骤(多级流水线),每个步骤使用的硬件可能不一样
指令重排是减少流水线中断的一种技术,显著提高性能
指令重排可以保证串行语义一致,但是无法保证多线程之间的语义也一致,多线程之间是通过原语保证有序性
CPU指令重排
有序性
一个操作必须在一个线程执行,不可拆开,不可被中断,要么不执行要么执行完
Java中只有对基本类型变量的赋值和读取是原子操作,如i = 1的赋值操作。但是像j = i或者i++这样的操作都不是原子操作,因为他们都进行了多次原子操作,两个原子操作加起来就不是原子操作
32位操作系统对long、double类型的操作
非原子性操作
原语是指由若干条机器指令构成的,并用以完成特定功能的一段程序,这段程序在执行期间不可分割
CPU提供了在指令执行期间对总线加锁的手段,原子指令由硬件提供,供软件来实现,但是开销太大
CPU缓存一致性是比对总线加锁更高效的,MESI协议。java的CAS原理就是调用CPU的缓存一致性协议
原子指令(Java原语)
非原子操作在并发访问时是线程非安全,需要使用并发处理策略实现同步操作,保证一个操作内一组原子操作的原子性
原子性
锁可以同时保证原子性、可见性、有序性3种特性,但是需要付出的性能代价最大
volatile只能保证可见性和有序性,无法保证原子性,但是配合CAS就可以高效的完成功能
如何保证3大特性
JVM内存模型
重写run方法
继承Thread类
任务有返回结果Future
构造函数实现Callable接口
FutureTask实现了Future接口和Runnable接口,即能作为Thread构造函数入参,也有返回结果
缺点是Future不支持回调,需要借助第三方包扩展回调处理能力,如:netty、guava
FutureTask装饰类
任务无返回结果,构造函数实现Runnable接口
Runnable接口
lambda表达式
new Thread(task)
CompletableFuture是增强的Future,支持回调,模式类似于ES6中的Promise
如果不指定线程池,则默认使用默认线程池ForkJoinPool
特点
返回CompletableFuture,具有初始结果
completedFuture()
返回CompletableFuture,任务无结果
runAsync()
返回CompletableFuture,任务有结果
supplyAsync()
初始化一组CompletableFuture
allOf()
初始方法
thenApply(Function)
thenAccept(Consumer)
thenRun(Runnable)
单个任务执行
runAfterBoth(Runnable)
thenAcceptBoth(BiConsumer)
thenCombine(BiFunction)
合并任务
applyToEither(Function)
runAfterEither(Runnable)
两个任务谁快计算谁
处理异常
中间方法
线程阻塞执行时间设置
手动结束线程
complete()
join()
归约方法
CompletableFuture类
配合completedFuture()可以返回一个CompletableFuture
返回void表示无返回结果
@Async注解
线程创建
线程与线程组之间的关系类似于文件与文件夹之间的关系,一个线程组可以包含多个线程以及其他线程组
一个线程组包含其他线程组的时候,该线程组被称为这些线程组的父线程组
给线程组起一个非常好听的名字非常重要
ThreadGroup线程组
为线程关联 UncaughtExceptionHandler
为线程设置一个含义更加具体的有助于问题定位的名称、组信息
确保线程的优先级为正常级别
线程创建的时候打印相关日志信息
ThreadFactory.newThread方法中封装线程创建的逻辑和配置管理
线程工厂
线程池维护线程的最少数量
corePoolSize
任务大于corePoolSize则优先放入该队列
workQueue
任务大于workQueue队列的容量,新建线程处理(线程池维护线程的最大数量)
maximumPoolSize
线程池中线程所允许的空闲时间,大于这个时间队列线程将会被kill
keepAliveTime
线程池维护线程所允许的空闲时间的单位
unit
任务数大于maximumPoolSize,无法进行新建线程处理时,对这些任务的拒绝策略
AbortPolicy(默认),直接抛出异常,阻止系统正常工作
CallerRunsPolicy
DiscardOldestPolicy,丢弃最老的请求
DiscardPolicy,默默丢弃无法处理的任务
handler拒绝策略
Runtime.getRuntime().availableProcessors()
取得可用的CPU数量
CPU密集型线程:Ncpu+1
这是因为I/O密集型线程在等待 I/O操作返回结果时是不占用处理器资源的
优先考虑将线程数设置为1,仅在一个线程不够用的情况下将线程数向2×Ncpu靠近
I/O密集型线程:2×Ncpu
合理的线程池大小
ThreadPoolExecutor
CompletionService
ScheduledExecutorService
ExecutorService 接口
无返回值
execute(Runnable)
有返回值
submit(Runnable)
Executor接口方法
Executor 接口
newFixedThreadPool
newSingleThreadExecutor
newCachedThreadPool
newScheduledThreadPool
Executors工具类方法
Exectors工具类
ThreadPoolTaskExecutor
TaskExecutor接口
ThreadPoolTaskScheduler
@Scheduled
TaskScheduler接口
spring的线程池技术
线程池
Fork/Join框架
Actor计算模型
线程管理
抽象队列同步器AQS实现了对同步状态的管理、阻塞线程进行排队、等待通知等一些底层的实现处理,同时拥有 同步队列 与 等待队列
原子状态使用CAS操作来存储当前锁的状态,判断锁是否被别的线程持有
同步器状态的原子性管理
所有没有请求到锁的线程都要进入等待队列等待
等待队列的管理
是阻塞原语park和unpark用来挂起和恢复线程
线程阻塞与解除阻塞
AQS(AbstractQueuedSynchronizer抽象类)
线程阻塞不占用CPU时间片,也不参与CPU上下文切换
线程阻塞会占用线程池的任务缓冲队列,新的任务进不来则被迫处于阻塞状态
主动阻塞常用于延时执行
阻塞
线程会进入该对象AQS等待队列中,等待队列中的线程不会主动去竞争该对象的锁
wait()
随机唤醒对象的等待队列中的一个线程,进入同步队列
notify()
唤醒对象的等待队列中的所有线程,进入同步队列
notifyAll()
Object对象
Condition对象
LockSupport阻塞工具
主动释放锁,让出临界资源
返回的是对当前正在执行线程对象的引用
CurrentThread
当前线程
不释临界资源
sleep
线程睡眠
并不是真是中断线程,而是打开中断标志位为true
interrupt
执行后自动将中断标志位清除为false
interrupted
线程中断
该方法是将调用方线程插入当前线程,当前线程阻塞直到join方法结束,从而控制线程的执行顺序
join
线程插入
暂停放弃CPU资源,放弃CPU的时间不确定
yield
线程让步
Thread方法
主动阻塞(等待)
调用加锁的临界资源的方法,但是锁被其它线程占用,当前线程只能被动阻塞
同步方法
加锁的方法
信号量方法
调用阻塞方法
被动阻塞
线程阻塞和唤醒
原子性、可见性、有序性的前提保障
锁的申请和释放所产生的开销
锁可能导致线程频繁的上下文切换的开销
锁的开销
一个锁是否一次只能被一个线程持有,如:读写锁中读锁就是共享锁,写锁是排他锁(互斥锁)
排他性&共享性
加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得
Fair
加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待
Nonfair
公平&不公平
锁的特点
内部锁 synchronized 以前性能比Lock差一点,jvm优化后,性能差不多
相对功能比较单一
不是很复杂的功能前提下,建议使用内部锁 synchronized
配合Synchronized搭档使用
会让当前线程立即释放Object监视器,进入object对象的等待队列
随机唤醒等待队列中的线程,被唤醒的线程不是立即执行代码,而是先重新获取object监视器
常用方法
Object阻塞方法
内部锁(Synchronized关键字)
Lock接口提供了一些内部锁所不具备的特性,但并不是内部锁的替代品
ReentrantLock是一种标准的互斥锁,最多只有一个线程能拥有它。这样的实现是线程安全的,但是对读和写两种情况都进行了同步限制,那么在频繁读取时,会对性能造成不必要的浪费
获得锁,如果锁已经被占用,则等待
lock
尝试获得锁,如果成功返回true,失败返回false,该方法不等待,立即返回
tryLock
在指定时间内尝试获得锁
释放锁
unLock
Lock方法
配合Lock搭档使用
要求线程释放锁,进入Condition对象的等待队列
await()
从当前Condition对象的等待队列上随机唤醒一个线程,并且主动释放锁
singal()
Condition对象阻塞
显式锁
读写锁允许多个线程可以同时读取(只读)共享变量,一次只允许一个线程对共享变量进行更新(包括读取后再更新)
一个线程更新共享变量的时候,其他任何线程都无法访问该变量。
改进型的互斥锁,也被称为“共享/排他锁”
读锁是可以同时被多个线程持有的,即读锁是共享的。任何一个线程持有一个读锁的时候,其他任何线程都无法获得相应锁的写锁。
写锁是排他的,即一个线程持有写锁的时候其他线程无法获得相应锁的写锁或读锁。
ReadWriteLock和显式锁Lock一样也是一个接口,提供了readLock和writeLock两种锁的操作机制,一个是只读的锁,一个是写锁
获得读锁
readLock()
获得写锁
writeLock()
ReadWriteLock接口
只读操作比写(更新)操作要频繁得多。和volatile关键字功能类似,volatile适用于一个写,多个读
读线程持有锁的时间比较长
场景中使用
读写锁
指定信号量(临界区)的准入数(线程数),如:临界区有10个资源则准入线程数最大只能是10,这样准入的线程数就不会发生阻塞
计数信号量是对锁的扩展,无论是内部锁还是重入锁,一次都只允许一个线程访问一个资源,而信号量却可以指定多个线程,同时访问某一个资源
加锁是为了安全而考虑,而限制线程并发数是为了性能考虑。Semaphore可以在构造方法中指定共享锁的数量,Semaphore在大于指定的共享锁数量时会阻塞
获得一个准入许可,如果无法获得,则等待
acquire
尝试获得一个准入许可,如果成功返回true,失败返回false,该方法不等待,立即返回
tryAcquire
在指定时间内尝试获取准入许可
tryAcquire(font color=\"#c41230\
释放一个许可
release
Semaphore方法
流量控制,特别是对资源池的场景,比如数据库连接
限制中的令牌桶算法
Semaphore信号量
锁的分类
所有的方法都是静态方法
阻塞原语park和unpark的使用不会出现死锁的情况
可以让线程在任意位置阻塞
线程栈信息提示更加清晰,便于查到问题
LockSupport
sleep()
延时
TimeUnit.DAYS、TimeUnit.HOURS、TimeUnit.MINUTES、TimeUnit.SECONDS、TimeUnit.MILLISECONDS
常用颗粒度
toMillis、toSeconds、toMinutes、toHours、toDays
颗粒度转换
TimeUnit
线程阻塞工具类
CountDownLatch 倒计数控制顺序(对join的扩展),异步转同步
CycliBarrier 循环栅栏(对join的扩展)
并发控制实用工具类
正则表达式的 Pattern 类,没有直接锁定方法,而是锁定方法中的一部分
减小锁持有时间
ConcurrentHashMap
减小锁粒度
ArrayBlockingQueue类不同方法的锁
锁分离
不是将锁的粒度变大,而是合并锁请求
锁粗化
锁消除是发生在编译器级别的一种锁优化方式,对不需要锁的位置进行自动锁消除
锁消除
一个线程在其持有一个锁的时候能再次(或者多次)申请该锁
非常有利于提高锁性能,让轻量级锁成为可能
可重入性
在几乎无竞争的条件下。当这个线程再次请求锁时,无须再做任何同步操作
锁偏向
在轻度竞争的条件下,偏向失败就会进行轻量级加锁
轻量级锁
轻量级锁加锁失败后,线程会忙等待,让当前线程做几个空循环(自旋),直到它拿到锁
自旋锁
在竞争非常激烈的条件下,轻量级加锁且自旋都失败,锁请求就会膨胀为重量级锁(synchronized、Lock)
互斥锁加锁失败后,线程会释放CPU被迫进入阻塞状态,操作系统内核会将线程置为wait状态,等到锁被释放后,内核会在合适的时机唤醒线程
互斥锁加锁失败时,会用「线程切换」来应对,当加锁失败的线程再次加锁成功后的这一过程,会有两次线程上下文切换的成本,性能损耗比较大。
重量级锁(互斥锁)
锁竞争激烈程度
JVM对锁的优化
锁优化标准
最常见的是数据库两个session对交叉数据的事务更新,权重较小的session将做出牺牲
死锁
常见锁问题
锁
随着硬件和操作系统指令集的发展和优化,产生了非阻塞同步,被称为乐观锁。简单地说,就是先进行操作,操作完成之后再判断操作是否成功,是否有并发问题,如果有则进行失败补偿,如果没有就算操作成功,这样就从根本上避免了同步锁的弊端
目前,在 Java中应用最广泛的非阻塞同步就是 CAS,在 IA64、 X86指令集中通过 cmpxchg指令完成 CAS功能,在 sparc - TSO中由case指令完成,在 ARM和 PowerPC架构下,需要使用一对 Idrex / strex指令完成。
回滚重试
无障碍
普通变量与volatile变量的区别是,volatile的特殊规则保证了写操作能立即同步到主内存,读操作立即从主内存刷新
volatile最适合使用的是一个线程写,其他线程读的场合
volatile配合CAS才能发挥其强大的无锁能力
无法保证原子性,volatile变量只有在完整的一个操作后才能主动写入内存如:i++,这个非原子操作由3个原子操作组成,在操作过程中还未将i写入内存
局限性
volatile
全称 Compare And Swap,比较并交换。需要volatile变量配合才能发挥作用
Unsafe类是进行底层操作的方法集合,可以直接操作内存,进行一些非常规操作,所以说是\"不安全\"的操作。提供了硬件级别的原子操作,大多数API都是通过native函数实现,利用JNI来完成CPU指令的操作
compareAndSwapObject()
compareAndSwapInt()
compareAndSwapLong()
常用操作
Unsafe类
compareAndSet()
Atomic原子变量实现
A线程写volatile变量,随后B线程用CAS更新这个volatile变量
A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量
Concurrent包实现
JDK8推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观 锁的重试次数)
LongAdder
应用
CAS
无锁
无饥饿
RCU(Read-Copy-Update)
无等待
非阻塞
线程上下文设计,用于实现线程内部的数据共享叫线程共享(对于同一个线程内部数据一致),即相同的一段代码多个线程来执行 ,每个线程使用的数据只与当前线程有关。
每个Thread维护着一个ThreadLocalMap的引用,ThreadLocalMap是ThreadLocal的内部类
从set方法我们可以看到,首先获取到了当前线程t,然后调用getMap获取ThreadLocalMap,如果map存在,则将当前线程对象t作为key,要存储的对象作为value存到map里面去。如果该Map不存在,则初始化一个。
set(T value)
get()
remove()
替代request的attribute属性
使用场景
ThreadLocal
容器操作方法上设置阻塞(阻塞方法+锁优化)
容器上设置非阻塞(CAS)
基本原理
CopyOnWriteArrayList
CopyOnWriteArraySet
CopyOnWrite
队列中元素按 FIFO 原则进行排序。没有任何锁操作,完全采用 CAS操作,来保证元素的一致性
ConcurrentLinkedQueue非阻塞队列
生产者— 消费者模式,非常适合用于数据共享的通道
操作队列元素的方法都是阻塞方法
基于数组实现的有界队列
ArrayBlockingQueue
基于链表实现的无界队列,默认是 Integer.MAX_VALUE
LinkedBlockingQueue
无界队列,有优先级
PriorityBlockingQueue
同步队列,该队列容量是0,严格说并不是一种容器,而是一个数据交换信道
每个put必须等待一个take,反之亦然。类似于生活中一手交钱一手交货,非常适合于传递性设计
SynchronousQueue
定时出队列,ThreadPoolTaskScheduler的底层实现
DelayedQueue
BlockingQueue阻塞队列
并发容器类
临界区并发处理策略
多线程
0 条评论
下一页