java高并发编程学习图
2022-05-10 14:32:53 0 举报
AI智能生成
线程安全尤为的重要
作者其他创作
大纲/内容
产生原因与解决
CPU 增加了缓存,以均衡与内存的速度差异
缺点
缓存导致的可见性问题
可见性:一个线程对共享变量的修改,另一个线程能立刻看到
多核时代,每个 CPU 都有自己的缓存,这时 CPU 缓存与内存的数据一致性就没那么容易解决了
解决可见性
volatile关键字
告诉编译器,对这个变量的读写,不能使用 CPU 缓存,必须从内存中读取或者写入
synchronized关键字
强制从内存读取
final 关键字
final 修饰变量时,初衷是告诉编译器:这个变量生而不变,可以可劲儿优化
六项 Happens-Before 规则
程序的顺序性规则
volatile 变量规则
传递性
管程中锁的规则(synchronized)
线程 start() 规则
线程 join() 规则
操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异
缺点
线程切换带来的原子性问题
原子性:把一个或者多个操作在 CPU 执行的过程中不被中断的特性
CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符
持有CPU的进程进行IO读取的时候,会把当前CPU的使用权让出去,待内容读取进内存,操作系统再把休眠的进程唤醒,唤醒了之后再申请获取CPU的使用权
解决原子性
Java 语言提供的锁技术:synchronized
Lock锁
编译程序优化指令执行次序,使得缓存能够得到更加合理地利用
缺点
编译优化带来的有序性问题
编译器为了优化性能,有时候会改变程序中语句的先后顺序
分配一块内存
地址赋值
初始化对象
解决有序性
volatile关键字
编译器生成字节码时,会在volatile写操作的前面和后面分别插入内存屏障(StoreStore屏障|volatile写|StoreLoad屏障)
StoreStore屏障:禁止上面的普通写和下面的volatile写重排序
StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序
synchronized关键字
也有内存屏障
final 关键字
final 修饰变量时,初衷是告诉编译器:这个变量生而不变,可以可劲儿优化
六项 Happens-Before 规则
死锁
产生原因
一组互相竞争资源的线程因互相等待,导致永久阻塞的现象 线程获取不到锁的时候,就会进入阻塞状态,直到有其他线程释放锁,才会被唤醒
产生条件
互斥,共享资源 X 和 Y 只能被一个线程占用
占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X
不可抢占,其他线程不能强行抢占线程 T1 占有的资源
循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待
解决
1、同时获取锁,不是分步获取
2、如果获取不到主动释放已经持有的锁
java.util.concurrent提供tryLock(long, TimeUnit) 方法,在一段时间尝试获取锁
3、按顺序获取锁。同样顺序去获取锁,不会存在循环
4、用 synchronized 实现等待 - 通知机制破坏循坏等待
synchronized 配合 wait()、notify()、notifyAll() 这三个方法就能轻松实现
并发编程注意问题
安全性问题
描述:存在共享数据并且该数据会发生变化,通俗地讲就是有多个线程会同时读写同一数据
解决:斥来解决,统一归为:锁(同一时刻只有一个线程执行)
活跃性问题
描述:某个操作无法执行下去。我们常见的“死锁”就是一种典型的活跃性问题,当然除了死锁外,还有两种情况,分别是“活锁”和“饥饿”
解决:主要是使用公平锁
性能问题
描述:“锁”的过度使用可能导致串行化的范围过大,这样就不能够发挥多线程的优势了
解决
第一,既然使用锁会带来性能问题,那最好的方案自然就是使用无锁的算法和数据结构了
第二,减少锁持有的时间
多线程
线程的生命周期
初始状态(NEW)
指的是线程已经被创建,但是还不允许分配 CPU 执行
可运行状态(RUNNABLE)
指的是线程可以分配 CPU 执行
运行状态(RUNNABLE)
当有空闲的 CPU 时,操作系统会将其分配给一个处于可运行状态的线程,被分配到 CPU 的线程的状态就转换成了运行状态
休眠状态(WAITING/TIMED_WAITING)
运行状态的线程如果调用一个阻塞的 API(例如以阻塞方式读文件)或者等待某个事件(例如条件变量),那么线程的状态就会转换到休眠状态
终止状态(TERMINATED)
线程执行完或者出现异常就会进入终止状态
线程间状态的转换
RUNNABLE 与 BLOCKED 的状态转换
synchronized 的隐式锁
RUNNABLE 与 WAITING 的状态转换
第一种场景,获得 synchronized 隐式锁的线程,调用无参数的 Object.wait() 方法
第二种场景,调用无参数的 Thread.join() 方法
第三种场景,调用 LockSupport.park() 方法
RUNNABLE 与 TIMED_WAITING 的状态转换
调用带超时参数的 Thread.sleep(long millis) 方法
获得 synchronized 隐式锁的线程,调用带超时参数的 Object.wait(long timeout) 方法
调用带超时参数的 Thread.join(long millis) 方法
调用带超时参数的 LockSupport.parkNanos(Object blocker, long deadline) 方法
调用带超时参数的 LockSupport.parkUntil(long deadline) 方法
从 NEW 到 RUNNABLE 状态
只要调用线程对象的 start() 方法就可以了
从 RUNNABLE 到 TERMINATED 状态
调用 interrupt() 方法
调用stop() 方法
创建多少线程合适?
计算方法
线程数 =CPU 核数 *CPU利用率* [ 1 +(I/O 耗时 / CPU 耗时)]
CPU耗时:apm工具可以精确到方法耗时
IO耗时:io相关的方法一般是知道的
CPU使用率是目标值也是确定的
CPU数量是确定的
线程数 = CPU可用核心数/(1 - 阻塞系数)
CPU可用核心数是确定的
阻塞系数=I/O 耗时/(I/O 耗时+CPU 耗时)
QPS的计算
QPS数=线程数*(1000/(I/O耗时+CPU耗时))
单位毫秒
每秒查询率(QPS,Queries-per-second)是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准
局部变量为什么安全?
局部变量的作用域是方法内部,因此放到了调用栈的栈帧里。 在Java中,new出来的对象在堆里,但局部变量在栈里
线程拥有独立栈帧,所以方法内变量相互隔离
当调用方法时,会创建新的栈帧,并压入调用栈;当方法返回时,对应的栈帧就会被自动弹出,栈帧和方法是同生共死的
常用并发同步工具类
Semaphore
ReadWriteLock
允许多个线程同时读共享变量
只允许一个线程写共享变量
如果一个写线程正在执行写操作,此时禁止读线程读共享变量
StampedLock
CountDownLatch
CountDownLatch 主要用来解决一个线程等待多个线程的场景
CountDownLatch 的计数器是不能循环利用
CyclicBarrier
CyclicBarrier 是一组线程之间互相等待
CyclicBarrier 的计数器是可以循环利用
CyclicBarrier 还可以设置回调函数
常用的并发容器
List
CopyOnWriteArrayList
内部维护了一个数组,成员变量 array 就指向这个内部数组,所有的读操作都是基于 array 进行的
读的时候有写,CopyOnWriteArrayList 会将 array 复制一份,然后在新复制处理的数组上执行增加元素的操作,执行完之后再将 array 指向这个新的数组
Map
ConcurrentHashMap
ConcurrentHashMap 的 key 是无序的
key 和 value 都不能为空,否则会抛出NullPointerException这个运行时异常
ConcurrentSkipListMap
ConcurrentSkipListMap 的 key 是有序的
key 和 value 都不能为空,否则会抛出NullPointerException这个运行时异常
Set
CopyOnWriteArraySet
ConcurrentSkipListSet
0 条评论
下一页