java并发
2022-03-28 18:54:40 3 举报
AI智能生成
java并发
作者其他创作
大纲/内容
实践
Disruptor框架
前言
基本概念
并发
分时间片,看起来像同时做但实际不是
并行
真的是同时在做
临界资源
指一次只能有一个线程访问的共用资源
临界区
指访问 临界资源的那段程序,需控制每次只能一个线程进入临界区保持互斥关系,否则产生并发问题
阻塞
多个线程进入 临界区,某一线程进入后其它线程需要等待挂起,则是阻塞
非阻塞
和阻塞相反,可让多个线程进入 临界区,则是非阻塞
死锁
线程之间相互占用对方的资源,互不释放
活锁
最终结果也是锁着,它是一个动态的,所以叫活锁(如两个线程都需要A和B资源,线程A拿B资源,线程B拿A资源,因为锁了两者同时释放,然后两者看到对方释放的资源就去拿,结果又锁了,如此反复)
饥饿
某些线程因为不能获取资源而一直无法执行
JMM
概念
线程并不是每次直接操作主内存,为了保证效率,每个线程都有自己一个本地/工作内存,类似一个高速cache,每次操作数据都是先经过本地内存。
本地内存
缓存行:线程从主内存读数据到缓存行
写缓冲区:线程数据更改先写入自己的写缓冲区,随后刷新到主内存
异常
因为有了本地内存,如果多个线程之前修改值都只是先保存到自己本地内存,没有马上更新到主内存,那么多个线程之间的值就没有及时同步,最后去更新主内存时可能就产生丢失更新和覆盖,这就是常见的可见性问题
并发 & 对象
对象头
Mark Word:包含锁标志位这些跟锁有关的信息
Class Meta Data:对象所属类的元数据(方法、字段)
Array Length:数据的时候才有,表示数组长度
监视器 monitor:每个对象都有个 monitor,在Java同步机制中使用
并行级别
阻塞
串行化操作,一个个排队处理
无障碍
比较再更新
无锁
无障碍锁的基础上,保证有一个可以成功
无等待
无锁的基础上,保证所有线程都能在有限步骤内完成
线程特性
原子性
操作不可中断,操作不会被其他线程干扰(比如i++就不原子,读、加、写三步,由于执行是时间片的切换,所以可能有异常问题)
可见性
一个线程改值,另一个线程能否立即知道这个修改,如果不知道可能出现线程问题
有序性
程序执行的顺序性按照代码的先后顺序(如 a = b + c,必须等 b 和 c 先读到内存,才能进行加操作),而多个线程就可能产生乱序或者指令重排(JVM性能优化)
重排序
简介
编译器为优化性能而改变程序中语句的先后顺序
经历
编译器优化的重排
指令级并行的重排
内存系统的重排
异常
比如单例中创建实例判断,A线程创建对象经历:①分配内存,②初始化对象③内存空间赋值给变量,但如果重排序后把②③变成③②,此时线程2在②步骤的时候就判断不空然后拿走值,其实是null值,就异常了
原则
数据依赖关系:单线程来看两个操作存在数据依赖关系(即操作同一个变量,其中一个为写,则存在数据依赖关系),则不可以重排序
as-if-serial语义:即不管怎么重排序,单线程时程序的执行结果不能变
屏蔽
java编译在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序
JMM根据代码关键字(如volatile、synchronized)和J.U.C 包下的的一些类来插入内存屏障
happen-before原则
内存屏障
CAS机制
就是 compare and swap/比较再更新,修改为更新值时,需保证预期值(原先读出来的值)一致才更新(A和B都能读数据并且写数据,但是在写之前需要校验值是否是之前读到的数据,确实是才修改否则不修改)
可重入性/递归锁
指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞(ReentrantLock和synchronized)
线程
渊源
起因
资源利用:某些工作遇到等待比如I/O,而如果每个线程都是串行,在等待I/O这段阻塞时间实际上是浪费资源,倒不如让出时间片给其它线程工作
大部分场景速度更快
缺点
设计更复杂:要考虑线程安全,线程通信,线程切换
上下文切换开销:保存 & 恢复线程执行状态,切换间增加开销
底层原理
执行原理
多个线程的运行是通过争取时间片(CPU执行权),CPU通过分配算法和规则在多个线程之前切换
不一定快
CPU切换线程是需要保持当前线程的执行状态(执行到哪步,变量和值),下次才能恢复,而这个保存-恢复是需要时间的,所以不一定多线程执行就会比串行快
减少切换
降低线程数
线程创建和销毁消耗资源,创建太多线程同样切换的几率就更高,所以多用池化技术(如线程池、连接池这些的思想,循环利用)
优化锁
可以对锁进行分段,减少频繁竞争导致的切换
保证原子性
锁的频繁获取-释放导致线程频繁切换,如果关键部分保证原子性,让一个线程做完所有事再切换其它线程,也就减少了来回切换(如CAS无锁)
线程状态
New
线程刚创建
Runnable
准备好,可以执行
Running
得到CPU时间片,执行中的
Dead
线程死亡
Blocked
阻塞状态
Wait Pool
等待状态
Lock Pool
等锁状态
创建方式
实现 Runnable接口
继承 Thread类
Callable + FutureTask
ExecutorService + Callable +Future
调度常用方法
stop()
直接终止线程,尽量少使用,可能导致部分在写的线程之间给停掉,部分数据没写入,用法 t1.stop()
interrupt()
中断线程,用法 t1.interrupt(),被中断的线程搭配 isInterrupted() 做处理来达到自己的效果,还有个 interrupted()它类似isInterrupted(),但 interrupted()调用之后会重置该中断状态而 isInterrupted()会一直处于中断,用法 t1.isInterrupted()、Thread.interrupted()
join()
B中调用了A的join(),就是先让A执行,然后再执行B,用法a.join()
yield()
谦让,暂停当前进程,和其他相同优先级的一起回到起点,可能它又会抢到资源,不能保证能马上暂停,用法 Thread.yield(),有锁它不会释放
sleep()
是不释放锁,wait()是释放锁,sleep()到处可以用但wait()、notify()、notifyAll()只能在同步控制方法或代码块中,用法 Thread.sleep()
wait()
作用于对象的wait,如让 a 等待,用法 a.wait(),必须在 synchronized里面也就是要拿到对象的锁
notify()/notifyAll()
作用于对象的notify,通知 a对象上随机线程醒来,用法 a.notify(),必须在 synchronized里面也就是要拿到跟 wait同个对象的锁,比如你notify后又不马上释放对象锁对方是不能马上醒来的。notifyAll() 是唤醒对象的所有等待线程
wait & notify(配合synchronized使用)
wait释放锁,也就是调用完就让给其它线程执行了,但是 notify是不释放锁的,调用之后它还是继续自己的工作,等完成了其它线程才能去执行
setPriority()
设置线程优先级,优先级高的先执行,用法 t1.setPriority(10)
suspend & resume
挂起 & 继续执行,suspend不会释放锁
关键问题
线程同步
线程通信
使用共享变量
调度
Deamon守护线程
在后台默默完成一些系统性服务(如垃圾回收线程、JIT线程),而当java应用只有守护线程,则Java虚拟机就会退出
线程池
Executors & ThreadPoolExecutor
Executors.newSingleThreadExecutor():单个线程
Executors.newFixedThreadPool(5):多个线程
Executors.newCachedThreadPool():带缓存不限线程个数
Executors.newScheduledThreadPool(2)
executorService.scheduleAtFixedRate:任务执行小于 period时间,则周期为 period,若大于,则按任务执行时间为周期
executorService.scheduleWithFixedDelay:周期 = 任务执行时间加 + delay时间
executorService.schedule:延迟 delay时间后开始执行,只执行一次
Executors.newWorkStealingPool():流式线程池
new ThreadPoolExecutor(5, 10, 1, TimeUnit.SECONDS, new LinkedBlockingDeque<>()):手动创建
Future & Callable & FutureTask
状态
RUNNING
运行状态,指可以接受任务执行队列里的任务
SHUTDOWN
调用了 shutdown() 方法,不再接受新任务了,但是队列里的任务得执行完毕
STOP
调用了 shutdownNow() 方法,不再接受新任务,同时抛弃阻塞队列里的所有任务并中断所有正在执行任务
TIDYING
所有任务都执行完毕,在调用 shutdown()/shutdownNow() 中都会尝试更新为这个状态
TERMINATED
终止状态,当执行 terminated() 后会更新为这个状态
参数
corePoolSize
核心线程数,池中线程未达到该数时则会创建新线程;若大或等于 corePoolSize且小于maximumPoolSize并且workQueue满了,再创建新线程
maximumPoolSize
最大线程数,当运行的线程数等于maximumPoolSize,且workQueue满了,则通过handler指定的策略去处理
workQueue(等待队列,池中线程都在忙,则加入worker队列中等待)
ArrayBlockingQueue:队列是有界的,基于数组实现的阻塞队列
LinkedBlockingQueue:队列可以有界,也可以无界。基于链表实现的阻塞队列
SynchronousQueue:不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作将一直处于阻塞状态
PriorityBlockingQueue:带优先级的无界阻塞队列
keepAliveTime
空闲时间,超过 corePoolSize数的那部分线程所允许的空闲时间,这部分线程执行完不会马上销毁,而是等到超过 keepAliveTime指定的时间
threadFactory
线程工厂,用来创建线程且设置线程名称,默认创建相同优先级且非守护的线程
handler策略(线程数达maximumPoolSize & workQueue满)
AbortPolicy(默认)
直接抛出异常
CallerRunsPolicy
用调用者所在线程执行
DiscardOldesPolicy
丢弃阻塞队列中靠前的任务,执行当前
DiscardPolicy
直接丢弃
关闭线程池
shutdown:正执行的会执行完,未执行的会中断
shutdownNow:正执行的会中断,未执行的直接返回
线程池处理机制
判断核心线程是否已满
否:创建新线程
是:判断队列是否已满
否:加入队列
是:判断是否到达最大线程数
否:创建新线程
是:根据 handler策略处理
锁
锁的状态
未锁定
初始状态,没线程访问同步块
偏向锁
简介
并发量小时,当一段同步代码一直被同个线程所访问的时候,则该线程会自动获取锁而不用再去lock和unlock,为了减少获取锁的消耗
原理
判断同步对象头中 Mark Word中线程Id是不是指向当前线程,是则直接进入同步块
如果不是指向当前,则判断Mark Word中“偏向锁标志”,如果非1表示无锁,则把线程Id指向当前进程,进入
如果“偏向锁标志”是1,且出现锁争抢,那么升级锁到 轻量级锁
偏向锁撤销
如果某拥有偏向锁的进程挂了,则jvm在没有字节码执行时先暂停该线程,然后检查其状态,如果判断真的挂了,则把锁置为无锁状态
轻量级锁
简介
偏向锁中因为并发升级而来
原理
获取锁:同步对象头中 Mark Word复制一份到当前线程帧栈空间,再用CAS机制把同步对象的Mark Word更新成指向该空间的指针,若成功则得锁,否则自旋
释放锁:将同步对象的Mark Word替换回原先值(Displace Mark Word),成功则释放锁,否则表示其它线程争用,升级为重量级锁
重量级锁
进入代码块得先获取对象的monitor,退出时释放,如果已被获取则进入monitor 阻塞队列,释放时进行竞争
锁的分类
悲观/互斥/排它/X/写锁(重量级)
按顺序来,一个个排队修改而不允许多人同时修改
乐观/共享/S/读锁(轻量级)
概念:就是CAS的机制
问题:ABA
A读到数据10,B改成9,C再改成10,A最后来修改发现是10就决定去写,但此10非彼10(比如充话费之后花了,后面因为ABA结果又回去了)
自旋锁(轻量级)
在乐观锁的基础上,获取锁失败不会马上放弃而是循环尝试获取,这个重新尝试过程就是自旋;减少线程上下文切换但耗CPU(Atomic类的实现)
分段锁
将数组划分更细颗粒度进行锁控制(ConcurrentHashMap在jdk7中用到机制)
分布式锁
注意点
加锁的时候要注意加的锁是对象级别还是类级别(如static方法、修饰(XXX.class)),很多锁是针对对象级别的,如果处理不好则可能达不到加锁的效果
核心内容
AQS
机制
AbstractQueuedSynchronizer,抽象队列同步器,它是java并发包的核心,它内部有个state变量和加锁线程变量。
一旦有人加锁成功就把 state从 0 变成 1,并且把加锁线程变量改为自己,如果同个线程加锁多次,则state一直累加1;
其他线程加锁时发现state已经不为0,并且线程变量也不是自己,则不能加锁,并把线程放到 AQS内部的等待队列
释放锁只需要把 state减到0,并且把加锁线程变量改为 null
公平 & 非公平
非公平锁:AQS加锁机制中,新线程不管其等待队列等锁的线程,直接去竞争锁
公平锁:AQS加锁机制中,新线程按照排队机制,自动排到等待队列后面
关键字
volatile
简介
它不能完全保证线程安全,被 volatile 修饰的变量,保证了更新之后的值马上刷新到主内存,并让其它线程本地内存中该值失效
特性
保证可见性
主内存马上可见,一旦更新数据则锁住其它读取该变量的缓存行,然后强制刷新数据到主内存,并且 JMM把其它线程的缓存行置为无效,所以读取值时就需到主内存获取最新值(线程间的隐式通信)
保证有序性
防止被指令重排
没有原子性:volatile对操作的原子性并不能保证(比如我们常用的 a=a+1,这样的操作需要先拿值再加再写(assign,store,write)),假如在更新值之前已经拿了值并阻塞了,其它线程拿值并且修改得比较快,然后前面的线程没重新去本地内存取值,造成结果可能就是第二个线程的更新丢失了(它能让其它线程马上可见到相关值的变化,但这只是主内存和其它线程的本地内存可见)
禁止重排规则
内存屏障规则
synchronized
同步
同步代码块(对象 || 类)
利用 monitor来实现,进入时执行 monitorenter,计数器加1,退出时执行 monitorexit,计数器减1,计数器为0则表示锁释放
同步方法
同步方法有一个 ACC_synchronized标志,线程访问需先获取 monitor锁才能执行,完成后释放 monitor锁,异常退出会自动释放
特性
原子性:只要锁没释放,就只有单个线程操作,可以保证原子
有序性:锁住部分是单线程的,遵循 as-if-serial语义,所以保证有序
可见性:JMM关于synchronized的两条语义:①解锁前共享变量最新值需刷到主内存 ②加锁前需清空本地内存中共享变量的值,从主内存中获取
jvm优化
自旋:获锁失败,进入自旋
消除:没竞争则去掉锁
粗化:部分情况下小范围改成整体加锁
注意
某对象有两个方法为 method1() & method2(),假设method1()被 synchronized修饰了,method2()没有,则两个线程可以同时分别访问这两个方法,但是假如method2()也被 synchronized修饰了,那么即使两个线程不是同时访问同一个方法,也需要等待锁,因为锁是对象级别的,里面的方法都属于同个对象
final
规则
final 变量:基本类型的值不可变,引用类型的地址不可变
final 方法:不可被重写,因为编译时已静态绑定,比普通方法要快,普通方法private其实隐式指定final
final 类:不可被继承,成员方法都是隐式指定为final,但成员变量可自己选择是否指定
知识点
可提高性能,jvm会优化
线程安全
匿名类和接口中所有变量都要final
final和abstract是互斥的
final变量即常量,一般要大写
禁止特定的重排序
Lock
LockSupport:针对线程
LockSupport.park():阻塞/等待许可
LockSupport.unpark():唤醒/通过
ReentrantLock
lock.lock()/lock.tryLock(timeout,timeunit)
lock.unlock()
ReentrantReadWriteLock
lock.readLock().lock() & lock.readLock().unlock()
读锁不互斥,可以同时进入
lock.writeLock().lock() & lock.writeLock().unlock()
只要有写锁就只允许单个进入,包括 写-写、写-读、读-写
Condition(配合 lock()、unlock()使用)
condition.await()
就类似 wait(),但是是对于指定condition的绑定,可以有多个;必须放在 lock()之后才能用
condition.signal()
类似 notify(),只唤醒该 condition的数据;;必须放在 lock()之后才能用
Atomic包(无锁化CAS)
AtomicInteger
AtomicIntegerArray
AtomicReference
AtomicStampedReference
AtomicIntegerFieldUpdater
注意
该包保证的是单个操作原子性,假如你在一个方法多次调用加减,而方法又不做同步处理,那结果可能也不是预料的
查看结果时,直接加完 system.out 查看因为打印的顺序问题显示异常,而获取最后结果就可以知道总数是对的
并发工具类
CountDownLatch
计数器,某线程使用await阻塞,当计数器减为0时,该线程才能唤醒继续执行下去;countDownLatch.countDown() & countDownLatch.await();
CyclicBarrier
栅栏,若设置 N个栅栏表示当只有N条线程执行到await方法时才会继续执行,cyclicBarrier.await();
Semaphore
信号量,有几个信号量就表示允许几个线程同时执行,超出的线程会阻塞,限流思想;semaphore.acquire() & semaphore.release();
Exchanger:两个线程数据交换
容器
线程安全
并发类
ConcurrentHashMap
BlockingQueue
CopyOnWriteArrayList
ConcurrentLinkedQueue
同步类
Vector
HashTable
ThreadLocal
线程本地容器,多个线程之间存储的数据不相互影响
线程不安全
Queue
Map
List
HashMap
红黑树
Fork-Join分治编程(jdk1.7)
NIO
jdk8新增
LongAdder
分段CAS:一开始有个base值,然后利用cell数组分段,多个线程分段去更新不同的cell数组值
自动分段迁移:某个线程执行CAS失败了,自动切换到其它cell数组
最后值就是base加上所有的cell中的值
CompletableFuture
StampedLock
解决方案
避免死锁
避免一个线程占用多个资源而要去获取多个锁
避免使用同个锁,采用分段(如ConcurrentHashMap)
避免锁无限等待,可用定时轮询并限制重复次数,失败就放弃;设置超时机制
死锁检测,若检测到死锁,可以释放所有锁并过段时间再试,或者设置优先级让某些线程在死锁时先释放
锁优化
持锁时间
锁粒度
锁消除
锁粗化
锁分离
Lock & synchronized 比较
synchronized
可重入
不可中断
不公平
能自动释放锁
使用 condition可指定 notify哪个线程
使用较方便
lock
可重复
可中断
可选公平
需手动释放锁
不能指定唤醒线程
使用较灵活
保证线程顺序
join
wait
condition
CountDownLatch计数器
生产者 & 消费者
Disruptor框架
实践
生产者 & 消费者
Disruptor框架
Disruptor框架
0 条评论
下一页
为你推荐
查看更多